Skip to content

Commit 4dd8467

Browse files
authored
Implement @pure-unless-callable-is-impure (#3482)
1 parent c1f6ad0 commit 4dd8467

67 files changed

Lines changed: 1417 additions & 60 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/functionMetadata_original.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
<?php declare(strict_types = 1);
22

3+
/**
4+
* Hand-maintained side-effect metadata for built-in functions and methods,
5+
* keyed by lowercase function name or "Class::method". resources/functionMetadata.php
6+
* is generated from this file by bin/generate-function-metadata.php.
7+
*
8+
* Each entry is exactly one of these shapes:
9+
*
10+
* - ['hasSideEffects' => bool]
11+
* false: the call is pure. true: the call has side effects.
12+
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>]
13+
* the call is pure unless one of the listed callable parameters
14+
* (keyed by parameter name) receives an impure callable, e.g. array_map()
15+
* whose only side effects come from its 'callback' argument.
16+
*/
17+
18+
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
319
return [
420
'abs' => ['hasSideEffects' => false],
521
'acos' => ['hasSideEffects' => false],
@@ -20,6 +36,8 @@
2036
'apcu_key_info' => ['hasSideEffects' => true],
2137
'apcu_sma_info' => ['hasSideEffects' => true],
2238
'apcu_store' => ['hasSideEffects' => true],
39+
'array_all' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
40+
'array_any' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
2341
'array_change_key_case' => ['hasSideEffects' => false],
2442
'array_chunk' => ['hasSideEffects' => false],
2543
'array_column' => ['hasSideEffects' => false],
@@ -28,39 +46,44 @@
2846
'array_diff' => ['hasSideEffects' => false],
2947
'array_diff_assoc' => ['hasSideEffects' => false],
3048
'array_diff_key' => ['hasSideEffects' => false],
31-
'array_diff_uassoc' => ['hasSideEffects' => false],
32-
'array_diff_ukey' => ['hasSideEffects' => false],
49+
'array_diff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]],
50+
'array_diff_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_comp_func' => true]],
3351
'array_fill' => ['hasSideEffects' => false],
3452
'array_fill_keys' => ['hasSideEffects' => false],
53+
'array_filter' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
54+
'array_find' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
55+
'array_find_key' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
3556
'array_flip' => ['hasSideEffects' => false],
3657
'array_intersect' => ['hasSideEffects' => false],
3758
'array_intersect_assoc' => ['hasSideEffects' => false],
3859
'array_intersect_key' => ['hasSideEffects' => false],
39-
'array_intersect_uassoc' => ['hasSideEffects' => false],
40-
'array_intersect_ukey' => ['hasSideEffects' => false],
60+
'array_intersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]],
61+
'array_intersect_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]],
4162
'array_key_first' => ['hasSideEffects' => false],
4263
'array_key_last' => ['hasSideEffects' => false],
4364
'array_key_exists' => ['hasSideEffects' => false],
4465
'array_keys' => ['hasSideEffects' => false],
66+
'array_map' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
4567
'array_merge' => ['hasSideEffects' => false],
4668
'array_merge_recursive' => ['hasSideEffects' => false],
4769
'array_pad' => ['hasSideEffects' => false],
4870
'array_pop' => ['hasSideEffects' => true],
4971
'array_product' => ['hasSideEffects' => false],
5072
'array_push' => ['hasSideEffects' => true],
5173
'array_rand' => ['hasSideEffects' => false],
74+
'array_reduce' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
5275
'array_replace' => ['hasSideEffects' => false],
5376
'array_replace_recursive' => ['hasSideEffects' => false],
5477
'array_reverse' => ['hasSideEffects' => false],
5578
'array_shift' => ['hasSideEffects' => true],
5679
'array_slice' => ['hasSideEffects' => false],
5780
'array_sum' => ['hasSideEffects' => false],
58-
'array_udiff' => ['hasSideEffects' => false],
59-
'array_udiff_assoc' => ['hasSideEffects' => false],
60-
'array_udiff_uassoc' => ['hasSideEffects' => false],
61-
'array_uintersect' => ['hasSideEffects' => false],
62-
'array_uintersect_assoc' => ['hasSideEffects' => false],
63-
'array_uintersect_uassoc' => ['hasSideEffects' => false],
81+
'array_udiff' => ['pureUnlessCallableIsImpureParameters' => ['data_comp_func' => true]],
82+
'array_udiff_assoc' => ['pureUnlessCallableIsImpureParameters' => ['key_comp_func' => true]],
83+
'array_udiff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_comp_func' => true, 'key_comp_func' => true]],
84+
'array_uintersect' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true]],
85+
'array_uintersect_assoc' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true]],
86+
'array_uintersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true, 'key_compare_func' => true]],
6487
'array_unique' => ['hasSideEffects' => false],
6588
'array_unshift' => ['hasSideEffects' => true],
6689
'array_values' => ['hasSideEffects' => false],
@@ -81,6 +104,8 @@
81104
'bcround' => ['hasSideEffects' => false],
82105
'bcfloor' => ['hasSideEffects' => false],
83106
'bcceil' => ['hasSideEffects' => false],
107+
'call_user_func' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
108+
'call_user_func_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
84109
// continue functionMap.php, line 424
85110
'chgrp' => ['hasSideEffects' => true],
86111
'chmod' => ['hasSideEffects' => true],
@@ -97,6 +122,8 @@
97122
'file_put_contents' => ['hasSideEffects' => true],
98123
'flock' => ['hasSideEffects' => true],
99124
'fopen' => ['hasSideEffects' => true],
125+
'forward_static_call' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
126+
'forward_static_call_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
100127
'fpassthru' => ['hasSideEffects' => true],
101128
'fputcsv' => ['hasSideEffects' => true],
102129
'fputs' => ['hasSideEffects' => true],
@@ -237,6 +264,7 @@
237264
'output_reset_rewrite_vars' => ['hasSideEffects' => true],
238265
'pclose' => ['hasSideEffects' => true],
239266
'popen' => ['hasSideEffects' => true],
267+
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
240268
'readfile' => ['hasSideEffects' => true],
241269
'rename' => ['hasSideEffects' => true],
242270
'rewind' => ['hasSideEffects' => true],

bin/generate-function-metadata.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,21 @@ public function enterNode(Node $node)
119119
);
120120
}
121121

122-
/** @var array<string, array{hasSideEffects: bool}> $metadata */
122+
/** @var array<string, array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array<string, bool>}> $metadata */
123123
$metadata = require __DIR__ . '/functionMetadata_original.php';
124124
foreach ($visitor->functions as $functionName) {
125125
if (array_key_exists($functionName, $metadata)) {
126-
if ($metadata[$functionName]['hasSideEffects']) {
126+
if (isset($metadata[$functionName]['hasSideEffects']) && $metadata[$functionName]['hasSideEffects']) {
127127
throw new ShouldNotHappenException($functionName);
128128
}
129+
130+
if (isset($metadata[$functionName]['pureUnlessCallableIsImpureParameters'])) {
131+
$metadata[$functionName] = [
132+
'pureUnlessCallableIsImpureParameters' => $metadata[$functionName]['pureUnlessCallableIsImpureParameters'],
133+
];
134+
135+
continue;
136+
}
129137
}
130138
$metadata[$functionName] = ['hasSideEffects' => false];
131139
}
@@ -177,19 +185,47 @@ public function enterNode(Node $node)
177185
* 2) Contribute the functions that have 'hasSideEffects' => true as a modification to bin/functionMetadata_original.php.
178186
* 3) Contribute the #[Pure] functions without side effects to https://github.com/JetBrains/phpstorm-stubs
179187
* 4) Once the PR from 3) is merged, please update the package here and run ./bin/generate-function-metadata.php.
188+
*
189+
* The array is keyed by lowercase function name or "Class::method". Each entry is
190+
* exactly one of:
191+
* - ['hasSideEffects' => bool] - false: pure, true: has side effects.
192+
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>] - pure unless
193+
* one of the listed callable parameters (keyed by parameter name) receives an
194+
* impure callable, e.g. array_map()'s 'callback'.
180195
*/
181196
197+
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
182198
return [
183199
%s
184200
];
185201
php;
186202
$content = '';
203+
$escape = static fn (mixed $value): string => var_export($value, true);
204+
$encodeHasSideEffects = static fn (array $meta) => [$escape('hasSideEffects'), $escape($meta['hasSideEffects'])];
205+
$encodePureUnlessCallableIsImpureParameters = static fn (array $meta) => [
206+
$escape('pureUnlessCallableIsImpureParameters'),
207+
sprintf(
208+
'[%s]',
209+
implode(
210+
' ,',
211+
array_map(
212+
static fn ($key, $param) => sprintf('%s => %s', $escape($key), $escape($param)),
213+
array_keys($meta['pureUnlessCallableIsImpureParameters']),
214+
$meta['pureUnlessCallableIsImpureParameters'],
215+
),
216+
),
217+
),
218+
];
219+
187220
foreach ($metadata as $name => $meta) {
188221
$content .= sprintf(
189222
"\t%s => [%s => %s],\n",
190223
var_export($name, true),
191-
var_export('hasSideEffects', true),
192-
var_export($meta['hasSideEffects'], true),
224+
...match (true) {
225+
isset($meta['hasSideEffects']) => $encodeHasSideEffects($meta),
226+
isset($meta['pureUnlessCallableIsImpureParameters']) => $encodePureUnlessCallableIsImpureParameters($meta),
227+
default => throw new ShouldNotHappenException($escape($meta)),
228+
},
193229
);
194230
}
195231

resources/functionMetadata.php

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,16 @@
1212
* 2) Contribute the functions that have 'hasSideEffects' => true as a modification to bin/functionMetadata_original.php.
1313
* 3) Contribute the #[Pure] functions without side effects to https://github.com/JetBrains/phpstorm-stubs
1414
* 4) Once the PR from 3) is merged, please update the package here and run ./bin/generate-function-metadata.php.
15+
*
16+
* The array is keyed by lowercase function name or "Class::method". Each entry is
17+
* exactly one of:
18+
* - ['hasSideEffects' => bool] - false: pure, true: has side effects.
19+
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>] - pure unless
20+
* one of the listed callable parameters (keyed by parameter name) receives an
21+
* impure callable, e.g. array_map()'s 'callback'.
1522
*/
1623

24+
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
1725
return [
1826
'BackedEnum::from' => ['hasSideEffects' => false],
1927
'BackedEnum::tryFrom' => ['hasSideEffects' => false],
@@ -725,6 +733,8 @@
725733
'apcu_key_info' => ['hasSideEffects' => true],
726734
'apcu_sma_info' => ['hasSideEffects' => true],
727735
'apcu_store' => ['hasSideEffects' => true],
736+
'array_all' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
737+
'array_any' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
728738
'array_change_key_case' => ['hasSideEffects' => false],
729739
'array_chunk' => ['hasSideEffects' => false],
730740
'array_column' => ['hasSideEffects' => false],
@@ -733,43 +743,48 @@
733743
'array_diff' => ['hasSideEffects' => false],
734744
'array_diff_assoc' => ['hasSideEffects' => false],
735745
'array_diff_key' => ['hasSideEffects' => false],
736-
'array_diff_uassoc' => ['hasSideEffects' => false],
737-
'array_diff_ukey' => ['hasSideEffects' => false],
746+
'array_diff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]],
747+
'array_diff_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_comp_func' => true]],
738748
'array_fill' => ['hasSideEffects' => false],
739749
'array_fill_keys' => ['hasSideEffects' => false],
750+
'array_filter' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
751+
'array_find' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
752+
'array_find_key' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
740753
'array_first' => ['hasSideEffects' => false],
741754
'array_flip' => ['hasSideEffects' => false],
742755
'array_intersect' => ['hasSideEffects' => false],
743756
'array_intersect_assoc' => ['hasSideEffects' => false],
744757
'array_intersect_key' => ['hasSideEffects' => false],
745-
'array_intersect_uassoc' => ['hasSideEffects' => false],
746-
'array_intersect_ukey' => ['hasSideEffects' => false],
758+
'array_intersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]],
759+
'array_intersect_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]],
747760
'array_is_list' => ['hasSideEffects' => false],
748761
'array_key_exists' => ['hasSideEffects' => false],
749762
'array_key_first' => ['hasSideEffects' => false],
750763
'array_key_last' => ['hasSideEffects' => false],
751764
'array_keys' => ['hasSideEffects' => false],
752765
'array_last' => ['hasSideEffects' => false],
766+
'array_map' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
753767
'array_merge' => ['hasSideEffects' => false],
754768
'array_merge_recursive' => ['hasSideEffects' => false],
755769
'array_pad' => ['hasSideEffects' => false],
756770
'array_pop' => ['hasSideEffects' => true],
757771
'array_product' => ['hasSideEffects' => false],
758772
'array_push' => ['hasSideEffects' => true],
759773
'array_rand' => ['hasSideEffects' => false],
774+
'array_reduce' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
760775
'array_replace' => ['hasSideEffects' => false],
761776
'array_replace_recursive' => ['hasSideEffects' => false],
762777
'array_reverse' => ['hasSideEffects' => false],
763778
'array_search' => ['hasSideEffects' => false],
764779
'array_shift' => ['hasSideEffects' => true],
765780
'array_slice' => ['hasSideEffects' => false],
766781
'array_sum' => ['hasSideEffects' => false],
767-
'array_udiff' => ['hasSideEffects' => false],
768-
'array_udiff_assoc' => ['hasSideEffects' => false],
769-
'array_udiff_uassoc' => ['hasSideEffects' => false],
770-
'array_uintersect' => ['hasSideEffects' => false],
771-
'array_uintersect_assoc' => ['hasSideEffects' => false],
772-
'array_uintersect_uassoc' => ['hasSideEffects' => false],
782+
'array_udiff' => ['pureUnlessCallableIsImpureParameters' => ['data_comp_func' => true]],
783+
'array_udiff_assoc' => ['pureUnlessCallableIsImpureParameters' => ['key_comp_func' => true]],
784+
'array_udiff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_comp_func' => true ,'key_comp_func' => true]],
785+
'array_uintersect' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true]],
786+
'array_uintersect_assoc' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true]],
787+
'array_uintersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true ,'key_compare_func' => true]],
773788
'array_unique' => ['hasSideEffects' => false],
774789
'array_unshift' => ['hasSideEffects' => true],
775790
'array_values' => ['hasSideEffects' => false],
@@ -803,6 +818,8 @@
803818
'bzerror' => ['hasSideEffects' => false],
804819
'bzerrstr' => ['hasSideEffects' => false],
805820
'bzopen' => ['hasSideEffects' => false],
821+
'call_user_func' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
822+
'call_user_func_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
806823
'ceil' => ['hasSideEffects' => false],
807824
'checkdate' => ['hasSideEffects' => false],
808825
'checkdnsrr' => ['hasSideEffects' => false],
@@ -954,6 +971,8 @@
954971
'fmod' => ['hasSideEffects' => false],
955972
'fnmatch' => ['hasSideEffects' => true],
956973
'fopen' => ['hasSideEffects' => true],
974+
'forward_static_call' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
975+
'forward_static_call_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]],
957976
'fpassthru' => ['hasSideEffects' => true],
958977
'fputcsv' => ['hasSideEffects' => true],
959978
'fputs' => ['hasSideEffects' => true],
@@ -1616,6 +1635,7 @@
16161635
'preg_last_error' => ['hasSideEffects' => true],
16171636
'preg_last_error_msg' => ['hasSideEffects' => true],
16181637
'preg_quote' => ['hasSideEffects' => false],
1638+
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
16191639
'preg_split' => ['hasSideEffects' => false],
16201640
'property_exists' => ['hasSideEffects' => false],
16211641
'quoted_printable_decode' => ['hasSideEffects' => false],
@@ -1757,4 +1777,4 @@
17571777
'zlib_encode' => ['hasSideEffects' => false],
17581778
'zlib_get_coding_type' => ['hasSideEffects' => false],
17591779

1760-
];
1780+
];

src/Analyser/ExprHandler/NewHandler.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
3333
use PHPStan\Node\MethodReturnStatementsNode;
3434
use PHPStan\Parser\NewAssignedToPropertyVisitor;
35+
use PHPStan\Reflection\Callables\SimpleImpurePoint;
3536
use PHPStan\Reflection\ClassReflection;
3637
use PHPStan\Reflection\Dummy\DummyConstructorReflection;
3738
use PHPStan\Reflection\ExtendedParametersAcceptor;
@@ -258,6 +259,13 @@ private function processConstructorReflection(string $className, New_ $expr, Mut
258259
if ($constructorReflection !== null) {
259260
if (!$constructorReflection->hasSideEffects()->no()) {
260261
$certain = $constructorReflection->isPure()->no();
262+
$verdict = SimpleImpurePoint::resolvePureUnlessCallableIsImpureVerdict($parametersAcceptor, $scope, $expr->getArgs());
263+
if ($verdict !== null && $verdict->yes()) {
264+
return [$constructorReflection, $classReflection, $parametersAcceptor, $impurePoints];
265+
}
266+
if ($verdict !== null && $verdict->no()) {
267+
$certain = true;
268+
}
261269
$impurePoints[] = new ImpurePoint(
262270
$scope,
263271
$expr,

src/Analyser/MutatingScope.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,7 @@ public function enterTrait(ClassReflection $traitReflection): self
15551555
* @param Type[] $parameterOutTypes
15561556
* @param array<string, bool> $immediatelyInvokedCallableParameters
15571557
* @param array<string, Type> $phpDocClosureThisTypeParameters
1558+
* @param array<string, bool> $phpDocPureUnlessCallableIsImpureParameters
15581559
*/
15591560
public function enterClassMethod(
15601561
Node\Stmt\ClassMethod $classMethod,
@@ -1576,6 +1577,7 @@ public function enterClassMethod(
15761577
array $phpDocClosureThisTypeParameters = [],
15771578
bool $isConstructor = false,
15781579
?ResolvedPhpDocBlock $resolvedPhpDocBlock = null,
1580+
array $phpDocPureUnlessCallableIsImpureParameters = [],
15791581
): self
15801582
{
15811583
if (!$this->isInClass()) {
@@ -1611,6 +1613,7 @@ public function enterClassMethod(
16111613
array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters),
16121614
$isConstructor,
16131615
$this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)),
1616+
$phpDocPureUnlessCallableIsImpureParameters,
16141617
),
16151618
!$classMethod->isStatic(),
16161619
);
@@ -1700,6 +1703,7 @@ public function enterPropertyHook(
17001703
[],
17011704
false,
17021705
$this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)),
1706+
[],
17031707
),
17041708
true,
17051709
);
@@ -1776,6 +1780,7 @@ private function getParameterAttributes(ClassMethod|Function_|PropertyHook $func
17761780
* @param Type[] $parameterOutTypes
17771781
* @param array<string, bool> $immediatelyInvokedCallableParameters
17781782
* @param array<string, Type> $phpDocClosureThisTypeParameters
1783+
* @param array<string, bool> $pureUnlessCallableIsImpureParameters
17791784
*/
17801785
public function enterFunction(
17811786
Node\Stmt\Function_ $function,
@@ -1793,6 +1798,7 @@ public function enterFunction(
17931798
array $parameterOutTypes = [],
17941799
array $immediatelyInvokedCallableParameters = [],
17951800
array $phpDocClosureThisTypeParameters = [],
1801+
array $pureUnlessCallableIsImpureParameters = [],
17961802
): self
17971803
{
17981804
return $this->enterFunctionLike(
@@ -1818,6 +1824,7 @@ public function enterFunction(
18181824
$immediatelyInvokedCallableParameters,
18191825
$phpDocClosureThisTypeParameters,
18201826
$this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)),
1827+
$pureUnlessCallableIsImpureParameters,
18211828
),
18221829
false,
18231830
);

0 commit comments

Comments
 (0)