Skip to content

Commit ca7deb3

Browse files
committed
Hardcode array_filter/array_map handling in ParametersAcceptorSelector, not NodeScopeResolver + Scope
1 parent bc97698 commit ca7deb3

File tree

6 files changed

+133
-52
lines changed

6 files changed

+133
-52
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,11 @@
9090
use PHPStan\Reflection\Native\NativeMethodReflection;
9191
use PHPStan\Reflection\ParametersAcceptor;
9292
use PHPStan\Reflection\ParametersAcceptorSelector;
93-
use PHPStan\Reflection\PassedByReference;
94-
use PHPStan\Reflection\Php\DummyParameter;
9593
use PHPStan\Reflection\Php\PhpMethodReflection;
9694
use PHPStan\Reflection\ReflectionProvider;
9795
use PHPStan\TrinaryLogic;
9896
use PHPStan\Type\Accessory\NonEmptyArrayType;
9997
use PHPStan\Type\ArrayType;
100-
use PHPStan\Type\CallableType;
10198
use PHPStan\Type\ClosureType;
10299
use PHPStan\Type\Constant\ConstantArrayType;
103100
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
@@ -3156,47 +3153,6 @@ private function processArgs(
31563153
$scope = $scope->assignVariable($argValue->name, new MixedType());
31573154
}
31583155
}
3159-
3160-
if ($calleeReflection instanceof FunctionReflection) {
3161-
if (
3162-
$i === 0
3163-
&& $calleeReflection->getName() === 'array_map'
3164-
&& isset($args[1])
3165-
) {
3166-
$parameterType = new CallableType([
3167-
new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
3168-
], new MixedType(), false);
3169-
}
3170-
3171-
if (
3172-
$i === 1
3173-
&& $calleeReflection->getName() === 'array_filter'
3174-
&& isset($args[0])
3175-
) {
3176-
if (isset($args[2])) {
3177-
$mode = $scope->getType($args[2]->value);
3178-
if ($mode instanceof ConstantIntegerType) {
3179-
if ($mode->getValue() === ARRAY_FILTER_USE_KEY) {
3180-
$arrayFilterParameters = [
3181-
new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null),
3182-
];
3183-
} elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) {
3184-
$arrayFilterParameters = [
3185-
new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
3186-
new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null),
3187-
];
3188-
}
3189-
}
3190-
}
3191-
$parameterType = new CallableType(
3192-
$arrayFilterParameters ?? [
3193-
new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
3194-
],
3195-
new MixedType(),
3196-
false
3197-
);
3198-
}
3199-
}
32003156
}
32013157

32023158
$originalScope = $scope;

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
namespace PHPStan\Reflection;
44

5+
use PhpParser\Node\Expr\FuncCall;
6+
use PhpParser\Node\Name;
57
use PHPStan\Analyser\Scope;
68
use PHPStan\Reflection\Native\NativeParameterReflection;
9+
use PHPStan\Reflection\Php\DummyParameter;
710
use PHPStan\TrinaryLogic;
11+
use PHPStan\Type\CallableType;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
813
use PHPStan\Type\Generic\TemplateTypeMap;
914
use PHPStan\Type\MixedType;
1015
use PHPStan\Type\TypeCombinator;
@@ -43,6 +48,87 @@ public static function selectFromArgs(
4348
{
4449
$types = [];
4550
$unpack = false;
51+
if (count($args) > 0 && count($parametersAcceptors) > 0) {
52+
$functionName = null;
53+
$argParent = $args[0]->getAttribute('parent');
54+
if ($argParent instanceof FuncCall && $argParent->name instanceof Name) {
55+
$functionName = $argParent->name->toLowerString();
56+
}
57+
if (
58+
$functionName === 'array_map'
59+
&& isset($args[1])
60+
) {
61+
$acceptor = $parametersAcceptors[0];
62+
$parameters = $acceptor->getParameters();
63+
$parameters[0] = new NativeParameterReflection(
64+
$parameters[0]->getName(),
65+
$parameters[0]->isOptional(),
66+
new CallableType([
67+
new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
68+
], new MixedType(), false),
69+
$parameters[0]->passedByReference(),
70+
$parameters[0]->isVariadic(),
71+
$parameters[0]->getDefaultValue()
72+
);
73+
$parametersAcceptors = [
74+
new FunctionVariant(
75+
$acceptor->getTemplateTypeMap(),
76+
$acceptor->getResolvedTemplateTypeMap(),
77+
$parameters,
78+
$acceptor->isVariadic(),
79+
$acceptor->getReturnType()
80+
),
81+
];
82+
}
83+
84+
if (
85+
$functionName === 'array_filter'
86+
&& isset($args[0])
87+
) {
88+
if (isset($args[2])) {
89+
$mode = $scope->getType($args[2]->value);
90+
if ($mode instanceof ConstantIntegerType) {
91+
if ($mode->getValue() === ARRAY_FILTER_USE_KEY) {
92+
$arrayFilterParameters = [
93+
new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null),
94+
];
95+
} elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) {
96+
$arrayFilterParameters = [
97+
new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
98+
new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null),
99+
];
100+
}
101+
}
102+
}
103+
104+
$acceptor = $parametersAcceptors[0];
105+
$parameters = $acceptor->getParameters();
106+
$parameters[1] = new NativeParameterReflection(
107+
$parameters[1]->getName(),
108+
$parameters[1]->isOptional(),
109+
new CallableType(
110+
$arrayFilterParameters ?? [
111+
new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
112+
],
113+
new MixedType(),
114+
false
115+
),
116+
$parameters[1]->passedByReference(),
117+
$parameters[1]->isVariadic(),
118+
$parameters[1]->getDefaultValue()
119+
);
120+
$parametersAcceptors = [
121+
new FunctionVariant(
122+
$acceptor->getTemplateTypeMap(),
123+
$acceptor->getResolvedTemplateTypeMap(),
124+
$parameters,
125+
$acceptor->isVariadic(),
126+
$acceptor->getReturnType()
127+
),
128+
];
129+
}
130+
}
131+
46132
foreach ($args as $arg) {
47133
$type = $scope->getType($arg->value);
48134
if ($arg->unpack) {

src/Type/Php/ArrayMapFunctionReturnTypeExtension.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Type\ArrayType;
1111
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1212
use PHPStan\Type\MixedType;
13+
use PHPStan\Type\NeverType;
1314
use PHPStan\Type\Type;
1415
use PHPStan\Type\TypeCombinator;
1516
use PHPStan\Type\TypeUtils;
@@ -30,12 +31,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3031

3132
$valueType = new MixedType();
3233
$callableType = $scope->getType($functionCall->args[0]->value);
33-
if (!$callableType->isCallable()->no()) {
34-
$valueType = ParametersAcceptorSelector::selectFromArgs(
35-
$scope,
36-
$functionCall->args,
37-
$callableType->getCallableParametersAcceptors($scope)
38-
)->getReturnType();
34+
if ($callableType->isCallable()->yes()) {
35+
$valueType = new NeverType();
36+
foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) {
37+
$valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType());
38+
}
3939
}
4040

4141
$mappedArrayType = new ArrayType(

tests/PHPStan/Analyser/data/bug-4097.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,26 @@ class Bar {}
1111
*/
1212
class SnapshotRepository
1313
{
14+
15+
/** @var T[] */
16+
private $entities;
17+
18+
/**
19+
* @param T[] $entities
20+
*/
21+
public function __construct(array $entities)
22+
{
23+
$this->entities = $entities;
24+
}
25+
1426
/**
1527
* @return \Traversable<Snapshot>
1628
*/
1729
public function findAllSnapshots(): \Traversable
1830
{
1931
yield from \array_map(
2032
\Closure::fromCallable([$this, 'buildSnapshot']),
21-
[]
33+
$this->entities
2234
);
2335
}
2436

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
class CallToFunctionParametersRuleTest extends \PHPStan\Testing\RuleTestCase
1616
{
1717

18+
/** @var bool */
19+
private $checkExplicitMixed = false;
20+
1821
protected function getRule(): \PHPStan\Rules\Rule
1922
{
2023
$broker = $this->createReflectionProvider();
2124
return new CallToFunctionParametersRule(
2225
$broker,
23-
new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(true), true, true, true, true, true)
26+
new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(true), true, true, true, true, true)
2427
);
2528
}
2629

@@ -811,4 +814,10 @@ public function testProcOpen(): void
811814
]);
812815
}
813816

817+
public function testBug5609(): void
818+
{
819+
$this->checkExplicitMixed = true;
820+
$this->analyse([__DIR__ . '/data/bug-5609.php'], []);
821+
}
822+
814823
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Bug5609;
4+
5+
class Foo
6+
{
7+
8+
/** @var \stdClass[] */
9+
private $entities;
10+
11+
public function doFoo()
12+
{
13+
array_filter($this->entities, function (\stdClass $std): bool {
14+
return true;
15+
});
16+
}
17+
18+
}

0 commit comments

Comments
 (0)