|
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