Skip to content

Commit a7628e2

Browse files
authored
Split ArrayFilterFunctionReturnTypeExtension to Helper
1 parent 5f064dd commit a7628e2

File tree

4 files changed

+346
-322
lines changed

4 files changed

+346
-322
lines changed

conf/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,9 @@ services:
12071207
tags:
12081208
- phpstan.broker.dynamicFunctionReturnTypeExtension
12091209

1210+
-
1211+
class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeHelper
1212+
12101213
-
12111214
class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeExtension
12121215
tags:

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,7 @@ parameters:
13051305
-
13061306
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#"
13071307
count: 1
1308-
path: src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php
1308+
path: src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php
13091309

13101310
-
13111311
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#"

src/Type/Php/ArrayFilterFunctionReturnTypeExtension.php

Lines changed: 2 additions & 321 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,16 @@
22

33
namespace PHPStan\Type\Php;
44

5-
use PhpParser\Node\Arg;
6-
use PhpParser\Node\Expr;
7-
use PhpParser\Node\Expr\ArrowFunction;
8-
use PhpParser\Node\Expr\Closure;
9-
use PhpParser\Node\Expr\Error;
105
use PhpParser\Node\Expr\FuncCall;
11-
use PhpParser\Node\Expr\MethodCall;
12-
use PhpParser\Node\Expr\StaticCall;
13-
use PhpParser\Node\Expr\Variable;
14-
use PhpParser\Node\Name;
15-
use PhpParser\Node\Stmt\Return_;
16-
use PHPStan\Analyser\MutatingScope;
176
use PHPStan\Analyser\Scope;
187
use PHPStan\Reflection\FunctionReflection;
19-
use PHPStan\Reflection\ReflectionProvider;
20-
use PHPStan\ShouldNotHappenException;
21-
use PHPStan\Type\ArrayType;
22-
use PHPStan\Type\BenevolentUnionType;
23-
use PHPStan\Type\Constant\ConstantArrayType;
24-
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
25-
use PHPStan\Type\Constant\ConstantBooleanType;
26-
use PHPStan\Type\Constant\ConstantIntegerType;
278
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
28-
use PHPStan\Type\ErrorType;
29-
use PHPStan\Type\MixedType;
30-
use PHPStan\Type\NeverType;
31-
use PHPStan\Type\NullType;
32-
use PHPStan\Type\StaticTypeFactory;
339
use PHPStan\Type\Type;
34-
use PHPStan\Type\TypeCombinator;
35-
use PHPStan\Type\TypeUtils;
36-
use function array_map;
37-
use function count;
38-
use function in_array;
39-
use function is_string;
40-
use function sprintf;
41-
use function substr;
4210

4311
final class ArrayFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
4412
{
4513

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)
14+
public function __construct(private ArrayFilterFunctionReturnTypeHelper $arrayFilterFunctionReturnTypeHelper)
5115
{
5216
}
5317

@@ -62,290 +26,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6226
$callbackArg = $functionCall->getArgs()[1]->value ?? null;
6327
$flagArg = $functionCall->getArgs()[2]->value ?? null;
6428

65-
if ($arrayArg === null) {
66-
return new ArrayType(new MixedType(), new MixedType());
67-
}
68-
69-
$arrayArgType = $scope->getType($arrayArg);
70-
$arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType);
71-
$keyType = $arrayArgType->getIterableKeyType();
72-
$itemType = $arrayArgType->getIterableValueType();
73-
74-
if ($itemType instanceof NeverType || $keyType instanceof NeverType) {
75-
return new ConstantArrayType([], []);
76-
}
77-
78-
if ($arrayArgType instanceof MixedType) {
79-
return new BenevolentUnionType([
80-
new ArrayType(new MixedType(), new MixedType()),
81-
new NullType(),
82-
]);
83-
}
84-
85-
if ($callbackArg === null || $scope->getType($callbackArg)->isNull()->yes()) {
86-
return TypeCombinator::union(
87-
...array_map([$this, 'removeFalsey'], $arrayArgType->getArrays()),
88-
);
89-
}
90-
91-
$mode = $this->determineMode($flagArg, $scope);
92-
if ($mode === null) {
93-
return new ArrayType($keyType, $itemType);
94-
}
95-
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;
108-
}
109-
return $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $statement->expr);
110-
}
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-
}
143-
144-
$expr = new FuncCall($funcName, $args);
145-
$results[] = $this->filterByTruthyValue($scope, $itemVar, $arrayArgType, $keyVar, $expr);
146-
}
147-
return TypeCombinator::union(...$results);
148-
}
149-
}
150-
151-
return new ArrayType($keyType, $itemType);
152-
}
153-
154-
public function removeFalsey(Type $type): Type
155-
{
156-
$falseyTypes = StaticTypeFactory::falsey();
157-
158-
if (count($type->getConstantArrays()) > 0) {
159-
$result = [];
160-
foreach ($type->getConstantArrays() as $constantArray) {
161-
$keys = $constantArray->getKeyTypes();
162-
$values = $constantArray->getValueTypes();
163-
164-
$builder = ConstantArrayTypeBuilder::createEmpty();
165-
166-
foreach ($values as $offset => $value) {
167-
$isFalsey = $falseyTypes->isSuperTypeOf($value);
168-
169-
if ($isFalsey->maybe()) {
170-
$builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true);
171-
} elseif ($isFalsey->no()) {
172-
$builder->setOffsetValueType($keys[$offset], $value, $constantArray->isOptionalKey($offset));
173-
}
174-
}
175-
176-
$result[] = $builder->getArray();
177-
}
178-
179-
return TypeCombinator::union(...$result);
180-
}
181-
182-
$keyType = $type->getIterableKeyType();
183-
$valueType = $type->getIterableValueType();
184-
185-
$valueType = TypeCombinator::remove($valueType, $falseyTypes);
186-
187-
if ($valueType instanceof NeverType) {
188-
return new ConstantArrayType([], []);
189-
}
190-
191-
return new ArrayType($keyType, $valueType);
192-
}
193-
194-
private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $arrayType, Error|Variable|null $keyVar, Expr $expr): Type
195-
{
196-
if (!$scope instanceof MutatingScope) {
197-
throw new ShouldNotHappenException();
198-
}
199-
200-
$constantArrays = $arrayType->getConstantArrays();
201-
if (count($constantArrays) > 0) {
202-
$results = [];
203-
foreach ($constantArrays as $constantArray) {
204-
$builder = ConstantArrayTypeBuilder::createEmpty();
205-
$optionalKeys = $constantArray->getOptionalKeys();
206-
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
207-
$itemType = $constantArray->getValueTypes()[$i];
208-
[$newKeyType, $newItemType, $optional] = $this->processKeyAndItemType($scope, $keyType, $itemType, $itemVar, $keyVar, $expr);
209-
$optional = $optional || in_array($i, $optionalKeys, true);
210-
if ($newKeyType instanceof NeverType || $newItemType instanceof NeverType) {
211-
continue;
212-
}
213-
if ($itemType->equals($newItemType) && $keyType->equals($newKeyType)) {
214-
$builder->setOffsetValueType($keyType, $itemType, $optional);
215-
continue;
216-
}
217-
218-
$builder->setOffsetValueType($newKeyType, $newItemType, true);
219-
}
220-
221-
$results[] = $builder->getArray();
222-
}
223-
224-
return TypeCombinator::union(...$results);
225-
}
226-
227-
[$newKeyType, $newItemType] = $this->processKeyAndItemType($scope, $arrayType->getIterableKeyType(), $arrayType->getIterableValueType(), $itemVar, $keyVar, $expr);
228-
229-
if ($newItemType instanceof NeverType || $newKeyType instanceof NeverType) {
230-
return new ConstantArrayType([], []);
231-
}
232-
233-
return new ArrayType($newKeyType, $newItemType);
234-
}
235-
236-
/**
237-
* @return array{Type, Type, bool}
238-
*/
239-
private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type $itemType, Error|Variable|null $itemVar, Error|Variable|null $keyVar, Expr $expr): array
240-
{
241-
$itemVarName = null;
242-
if ($itemVar !== null) {
243-
if (!$itemVar instanceof Variable || !is_string($itemVar->name)) {
244-
throw new ShouldNotHappenException();
245-
}
246-
$itemVarName = $itemVar->name;
247-
$scope = $scope->assignVariable($itemVarName, $itemType, new MixedType());
248-
}
249-
250-
$keyVarName = null;
251-
if ($keyVar !== null) {
252-
if (!$keyVar instanceof Variable || !is_string($keyVar->name)) {
253-
throw new ShouldNotHappenException();
254-
}
255-
$keyVarName = $keyVar->name;
256-
$scope = $scope->assignVariable($keyVarName, $keyType, new MixedType());
257-
}
258-
259-
$booleanResult = $scope->getType($expr)->toBoolean();
260-
if ($booleanResult->isFalse()->yes()) {
261-
return [new NeverType(), new NeverType(), false];
262-
}
263-
264-
$scope = $scope->filterByTruthyValue($expr);
265-
266-
return [
267-
$keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType,
268-
$itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType,
269-
!$booleanResult instanceof ConstantBooleanType,
270-
];
271-
}
272-
273-
private static function createFunctionName(string $funcName): ?Name
274-
{
275-
if ($funcName === '') {
276-
return null;
277-
}
278-
279-
if ($funcName[0] === '\\') {
280-
$funcName = substr($funcName, 1);
281-
282-
if ($funcName === '') {
283-
return null;
284-
}
285-
286-
return new Name\FullyQualified($funcName);
287-
}
288-
289-
return new Name($funcName);
290-
}
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;
29+
return $this->arrayFilterFunctionReturnTypeHelper->getType($scope, $arrayArg, $callbackArg, $flagArg);
34930
}
35031

35132
}

0 commit comments

Comments
 (0)