|
6 | 6 | use PhpParser\Node\Expr; |
7 | 7 | use PhpParser\Node\Expr\ArrowFunction; |
8 | 8 | use PhpParser\Node\Expr\Closure; |
9 | | -use PhpParser\Node\Expr\ConstFetch; |
10 | 9 | use PhpParser\Node\Expr\Error; |
11 | 10 | use PhpParser\Node\Expr\FuncCall; |
| 11 | +use PhpParser\Node\Expr\MethodCall; |
| 12 | +use PhpParser\Node\Expr\StaticCall; |
12 | 13 | use PhpParser\Node\Expr\Variable; |
13 | 14 | use PhpParser\Node\Name; |
14 | | -use PhpParser\Node\Scalar\String_; |
15 | 15 | use PhpParser\Node\Stmt\Return_; |
16 | 16 | use PHPStan\Analyser\MutatingScope; |
17 | 17 | use PHPStan\Analyser\Scope; |
18 | 18 | use PHPStan\Reflection\FunctionReflection; |
| 19 | +use PHPStan\Reflection\ReflectionProvider; |
19 | 20 | use PHPStan\ShouldNotHappenException; |
20 | 21 | use PHPStan\Type\ArrayType; |
21 | 22 | use PHPStan\Type\BenevolentUnionType; |
22 | 23 | use PHPStan\Type\Constant\ConstantArrayType; |
23 | 24 | use PHPStan\Type\Constant\ConstantArrayTypeBuilder; |
24 | 25 | use PHPStan\Type\Constant\ConstantBooleanType; |
| 26 | +use PHPStan\Type\Constant\ConstantIntegerType; |
25 | 27 | use PHPStan\Type\DynamicFunctionReturnTypeExtension; |
26 | 28 | use PHPStan\Type\ErrorType; |
27 | 29 | use PHPStan\Type\MixedType; |
|
35 | 37 | use function count; |
36 | 38 | use function in_array; |
37 | 39 | use function is_string; |
38 | | -use function strtolower; |
| 40 | +use function sprintf; |
39 | 41 | use function substr; |
40 | 42 |
|
41 | | -final class ArrayFilterFunctionReturnTypeReturnTypeExtension implements DynamicFunctionReturnTypeExtension |
| 43 | +final class ArrayFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension |
42 | 44 | { |
43 | 45 |
|
| 46 | + private const USE_BOTH = 1; |
| 47 | + private const USE_KEY = 2; |
| 48 | + private const USE_ITEM = 3; |
| 49 | + |
| 50 | + public function __construct(private ReflectionProvider $reflectionProvider) |
| 51 | + { |
| 52 | + } |
| 53 | + |
44 | 54 | public function isFunctionSupported(FunctionReflection $functionReflection): bool |
45 | 55 | { |
46 | 56 | return $functionReflection->getName() === 'array_filter'; |
@@ -72,70 +82,69 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, |
72 | 82 | ]); |
73 | 83 | } |
74 | 84 |
|
75 | | - if ($callbackArg === null || ($callbackArg instanceof ConstFetch && strtolower($callbackArg->name->getParts()[0]) === 'null')) { |
| 85 | + if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) { |
76 | 86 | return TypeCombinator::union( |
77 | 87 | ...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()), |
78 | 88 | ); |
79 | 89 | } |
80 | 90 |
|
81 | | - if ($flagArg === null) { |
82 | | - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { |
83 | | - $statement = $callbackArg->stmts[0]; |
84 | | - if ($statement instanceof Return_ && $statement->expr !== null) { |
85 | | - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $statement->expr); |
86 | | - } |
87 | | - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { |
88 | | - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, null, $callbackArg->expr); |
89 | | - } elseif ($callbackArg instanceof String_) { |
90 | | - $funcName = self::createFunctionName($callbackArg->value); |
91 | | - if ($funcName === null) { |
92 | | - return new ErrorType(); |
93 | | - } |
94 | | - |
95 | | - $itemVar = new Variable('item'); |
96 | | - $expr = new FuncCall($funcName, [new Arg($itemVar)]); |
97 | | - return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, null, $expr); |
98 | | - } |
| 91 | + $mode = $this->determineMode($flagArg, $scope); |
| 92 | + if ($mode === null) { |
| 93 | + return new ArrayType($keyType, $itemType); |
99 | 94 | } |
100 | 95 |
|
101 | | - if ($flagArg instanceof ConstFetch && $flagArg->name->getParts()[0] === 'ARRAY_FILTER_USE_KEY') { |
102 | | - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { |
103 | | - $statement = $callbackArg->stmts[0]; |
104 | | - if ($statement instanceof Return_ && $statement->expr !== null) { |
105 | | - return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $statement->expr); |
| 96 | + if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { |
| 97 | + $statement = $callbackArg->stmts[0]; |
| 98 | + if ($statement instanceof Return_ && $statement->expr !== null) { |
| 99 | + if ($mode === self::USE_ITEM) { |
| 100 | + $keyVar = null; |
| 101 | + $itemVar = $callbackArg->params[0]->var; |
| 102 | + } elseif ($mode === self::USE_KEY) { |
| 103 | + $keyVar = $callbackArg->params[0]->var; |
| 104 | + $itemVar = null; |
| 105 | + } elseif ($mode === self::USE_BOTH) { |
| 106 | + $keyVar = $callbackArg->params[1]->var ?? null; |
| 107 | + $itemVar = $callbackArg->params[0]->var; |
106 | 108 | } |
107 | | - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { |
108 | | - return $this->filterByTruthyValue($scope, null, $arrayArgType, $callbackArg->params[0]->var, $callbackArg->expr); |
109 | | - } elseif ($callbackArg instanceof String_) { |
110 | | - $funcName = self::createFunctionName($callbackArg->value); |
111 | | - if ($funcName === null) { |
112 | | - return new ErrorType(); |
113 | | - } |
114 | | - |
115 | | - $keyVar = new Variable('key'); |
116 | | - $expr = new FuncCall($funcName, [new Arg($keyVar)]); |
117 | | - return $this->filterByTruthyValue($scope, null, $arrayArgType, $keyVar, $expr); |
| 109 | + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $statement->expr); |
118 | 110 | } |
119 | | - } |
| 111 | + } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { |
| 112 | + if ($mode === self::USE_ITEM) { |
| 113 | + $keyVar = null; |
| 114 | + $itemVar = $callbackArg->params[0]->var; |
| 115 | + } elseif ($mode === self::USE_KEY) { |
| 116 | + $keyVar = $callbackArg->params[0]->var; |
| 117 | + $itemVar = null; |
| 118 | + } elseif ($mode === self::USE_BOTH) { |
| 119 | + $keyVar = $callbackArg->params[1]->var ?? null; |
| 120 | + $itemVar = $callbackArg->params[0]->var; |
| 121 | + } |
| 122 | + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $callbackArg->expr); |
| 123 | + } elseif ( |
| 124 | + ($callbackArg instanceof FuncCall || $callbackArg instanceof MethodCall || $callbackArg instanceof StaticCall) |
| 125 | + && $callbackArg->isFirstClassCallable() |
| 126 | + ) { |
| 127 | + [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); |
| 128 | + $expr = clone $callbackArg; |
| 129 | + $expr->args = $args; |
| 130 | + return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); |
| 131 | + } else { |
| 132 | + $constantStrings = $scope->getType($callbackArg)->getConstantStrings(); |
| 133 | + if (count($constantStrings) > 0) { |
| 134 | + $results = []; |
| 135 | + [$args, $itemVar, $keyVar] = $this->createDummyArgs($mode); |
| 136 | + |
| 137 | + foreach ($constantStrings as $constantString) { |
| 138 | + $funcName = self::createFunctionName($constantString->getValue()); |
| 139 | + if ($funcName === null) { |
| 140 | + $results[] = new ErrorType(); |
| 141 | + continue; |
| 142 | + } |
120 | 143 |
|
121 | | - if ($flagArg instanceof ConstFetch && $flagArg->name->getParts()[0] === 'ARRAY_FILTER_USE_BOTH') { |
122 | | - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { |
123 | | - $statement = $callbackArg->stmts[0]; |
124 | | - if ($statement instanceof Return_ && $statement->expr !== null) { |
125 | | - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $statement->expr); |
126 | | - } |
127 | | - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { |
128 | | - return $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $arrayArgType, $callbackArg->params[1]->var ?? null, $callbackArg->expr); |
129 | | - } elseif ($callbackArg instanceof String_) { |
130 | | - $funcName = self::createFunctionName($callbackArg->value); |
131 | | - if ($funcName === null) { |
132 | | - return new ErrorType(); |
| 144 | + $expr = new FuncCall($funcName, $args); |
| 145 | + $results[] = $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); |
133 | 146 | } |
134 | | - |
135 | | - $itemVar = new Variable('item'); |
136 | | - $keyVar = new Variable('key'); |
137 | | - $expr = new FuncCall($funcName, [new Arg($itemVar), new Arg($keyVar)]); |
138 | | - return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr); |
| 147 | + return TypeCombinator::union(...$results); |
139 | 148 | } |
140 | 149 | } |
141 | 150 |
|
@@ -280,4 +289,63 @@ private static function createFunctionName(string $funcName): ?Name |
280 | 289 | return new Name($funcName); |
281 | 290 | } |
282 | 291 |
|
| 292 | + /** |
| 293 | + * @param self::USE_* $mode |
| 294 | + * @return array{list<Arg>, ?Variable, ?Variable} |
| 295 | + */ |
| 296 | + private function createDummyArgs(int $mode): array |
| 297 | + { |
| 298 | + if ($mode === self::USE_ITEM) { |
| 299 | + $itemVar = new Variable('item'); |
| 300 | + $keyVar = null; |
| 301 | + $args = [new Arg($itemVar)]; |
| 302 | + } elseif ($mode === self::USE_KEY) { |
| 303 | + $itemVar = null; |
| 304 | + $keyVar = new Variable('key'); |
| 305 | + $args = [new Arg($keyVar)]; |
| 306 | + } elseif ($mode === self::USE_BOTH) { |
| 307 | + $itemVar = new Variable('item'); |
| 308 | + $keyVar = new Variable('key'); |
| 309 | + $args = [new Arg($itemVar), new Arg($keyVar)]; |
| 310 | + } |
| 311 | + return [$args, $itemVar, $keyVar]; |
| 312 | + } |
| 313 | + |
| 314 | + /** |
| 315 | + * @param non-empty-string $constantName |
| 316 | + */ |
| 317 | + private function getConstant(string $constantName): int |
| 318 | + { |
| 319 | + $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); |
| 320 | + $valueType = $constant->getValueType(); |
| 321 | + if (!$valueType instanceof ConstantIntegerType) { |
| 322 | + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); |
| 323 | + } |
| 324 | + |
| 325 | + return $valueType->getValue(); |
| 326 | + } |
| 327 | + |
| 328 | + /** |
| 329 | + * @return self::USE_*|null |
| 330 | + */ |
| 331 | + private function determineMode(?Expr $flagArg, Scope $scope): ?int |
| 332 | + { |
| 333 | + if ($flagArg === null) { |
| 334 | + return self::USE_ITEM; |
| 335 | + } |
| 336 | + |
| 337 | + $flagValues = $scope->getType($flagArg)->getConstantScalarValues(); |
| 338 | + if (count($flagValues) !== 1) { |
| 339 | + return null; |
| 340 | + } |
| 341 | + |
| 342 | + if ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_KEY')) { |
| 343 | + return self::USE_KEY; |
| 344 | + } elseif ($flagValues[0] === $this->getConstant('ARRAY_FILTER_USE_BOTH')) { |
| 345 | + return self::USE_BOTH; |
| 346 | + } |
| 347 | + |
| 348 | + return null; |
| 349 | + } |
| 350 | + |
283 | 351 | } |
0 commit comments