Skip to content

Commit e569fe8

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents a19973f + 3c7977a commit e569fe8

3 files changed

Lines changed: 233 additions & 3 deletions

File tree

src/Analyser/ExprHandler/FuncCallHandler.php

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Analyser\ExprHandler;
44

55
use Closure;
6+
use PhpParser\Node;
67
use PhpParser\Node\Arg;
78
use PhpParser\Node\Expr;
89
use PhpParser\Node\Expr\FuncCall;
@@ -28,6 +29,7 @@
2829
use PHPStan\DependencyInjection\AutowiredService;
2930
use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
3031
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
32+
use PHPStan\Node\ClosureReturnStatementsNode;
3133
use PHPStan\Node\Expr\NativeTypeExpr;
3234
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
3335
use PHPStan\Node\Expr\TypeExpr;
@@ -61,13 +63,15 @@
6163
use PHPStan\Type\TypeTraverser;
6264
use PHPStan\Type\UnionType;
6365
use Throwable;
66+
use function array_fill;
6467
use function array_filter;
6568
use function array_map;
6669
use function array_merge;
6770
use function array_slice;
6871
use function array_values;
6972
use function count;
7073
use function in_array;
74+
use function is_string;
7175
use function sprintf;
7276
use function str_starts_with;
7377

@@ -201,13 +205,89 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
201205
}
202206
}
203207

204-
$argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context);
208+
/** @var array{Type, Type}|null $arrayWalkValueTypes */
209+
$arrayWalkValueTypes = null;
210+
$arrayWalkArrayArg = null;
211+
$arrayWalkOriginalArrayType = null;
212+
$arrayWalkOriginalArrayNativeType = null;
213+
$nodeCallbackForArgs = $nodeCallback;
214+
if (
215+
$functionReflection !== null
216+
&& $functionReflection->getName() === 'array_walk'
217+
&& count($normalizedExpr->getArgs()) >= 2
218+
) {
219+
$callbackArg = $normalizedExpr->getArgs()[1]->value;
220+
$firstParamName = null;
221+
222+
if (
223+
$callbackArg instanceof Expr\Closure
224+
&& isset($callbackArg->params[0])
225+
&& $callbackArg->params[0]->byRef
226+
&& $callbackArg->params[0]->var instanceof Variable
227+
&& is_string($callbackArg->params[0]->var->name)
228+
) {
229+
$firstParamName = $callbackArg->params[0]->var->name;
230+
}
231+
232+
if ($firstParamName !== null) {
233+
$arrayWalkArrayArg = $normalizedExpr->getArgs()[0]->value;
234+
$arrayWalkOriginalArrayType = $scope->getType($arrayWalkArrayArg);
235+
$arrayWalkOriginalArrayNativeType = $scope->getNativeType($arrayWalkArrayArg);
236+
237+
$nodeCallbackForArgs = static function (Node $node, Scope $scope) use ($nodeCallback, $callbackArg, $firstParamName, &$arrayWalkValueTypes): void {
238+
if ($node instanceof ClosureReturnStatementsNode && $node->getClosureExpr() === $callbackArg) {
239+
$types = [];
240+
$nativeTypes = [];
241+
$stmtResult = $node->getStatementResult();
242+
foreach ($stmtResult->getExitPoints() as $exitPoint) {
243+
$exitScope = $exitPoint->getScope();
244+
if (!$exitScope->hasVariableType($firstParamName)->yes()) {
245+
continue;
246+
}
247+
248+
$types[] = $exitScope->getVariableType($firstParamName);
249+
$nativeTypes[] = $exitScope->getNativeType(new Variable($firstParamName));
250+
}
251+
if (!$stmtResult->isAlwaysTerminating()) {
252+
$stmtScope = $stmtResult->getScope();
253+
if ($stmtScope->hasVariableType($firstParamName)->yes()) {
254+
$types[] = $stmtScope->getVariableType($firstParamName);
255+
$nativeTypes[] = $stmtScope->getNativeType(new Variable($firstParamName));
256+
}
257+
}
258+
if (count($types) > 0) {
259+
$arrayWalkValueTypes = [
260+
TypeCombinator::union(...$types),
261+
TypeCombinator::union(...$nativeTypes),
262+
];
263+
}
264+
}
265+
$nodeCallback($node, $scope);
266+
};
267+
}
268+
}
269+
270+
$argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context);
205271
$scope = $argsResult->getScope();
206272
$hasYield = $argsResult->hasYield();
207273
$throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints());
208274
$impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints());
209275
$isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating();
210276

277+
if ($arrayWalkValueTypes !== null && $arrayWalkArrayArg !== null) {
278+
$newArrayType = $this->getArrayWalkResultType($arrayWalkOriginalArrayType, $arrayWalkValueTypes[0]);
279+
$newArrayNativeType = $this->getArrayWalkResultType($arrayWalkOriginalArrayNativeType, $arrayWalkValueTypes[1]);
280+
281+
$scope = $nodeScopeResolver->processVirtualAssign(
282+
$scope,
283+
$storage,
284+
$stmt,
285+
$arrayWalkArrayArg,
286+
new NativeTypeExpr($newArrayType, $newArrayNativeType),
287+
$nodeCallback,
288+
)->getScope();
289+
}
290+
211291
if ($normalizedExpr->name instanceof Expr) {
212292
$nameType = $scope->getType($normalizedExpr->name);
213293
if (
@@ -705,6 +785,31 @@ private function getArraySortDoNotPreserveListFunctionType(Type $type): Type
705785
});
706786
}
707787

788+
private function getArrayWalkResultType(Type $arrayType, Type $newValueType): Type
789+
{
790+
return TypeTraverser::map($arrayType, static function (Type $type, callable $traverse) use ($newValueType): Type {
791+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
792+
return $traverse($type);
793+
}
794+
795+
if ($type instanceof ConstantArrayType) {
796+
return new ConstantArrayType(
797+
$type->getKeyTypes(),
798+
array_fill(0, count($type->getValueTypes()), $newValueType),
799+
$type->getNextAutoIndexes(),
800+
$type->getOptionalKeys(),
801+
$type->isList(),
802+
);
803+
}
804+
805+
if (!$type instanceof ArrayType) {
806+
return $type;
807+
}
808+
809+
return new ArrayType($type->getKeyType(), $newValueType);
810+
});
811+
}
812+
708813
public function resolveType(MutatingScope $scope, Expr $expr): Type
709814
{
710815
if ($expr->name instanceof Expr) {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14525;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function basicArrayWalk(): void
8+
{
9+
$array = ['a' => 1, 'b' => 2];
10+
array_walk($array, function (&$value, $key): void {
11+
$value = (string) $value;
12+
});
13+
assertType("array{a: '1'|'2', b: '1'|'2'}", $array);
14+
}
15+
16+
function arrayWalkGeneric(): void
17+
{
18+
/** @var array<string, int> $array */
19+
$array = ['a' => 1, 'b' => 2];
20+
array_walk($array, function (&$value, $key): void {
21+
$value = (string) $value;
22+
});
23+
assertType("array<string, lowercase-string&numeric-string&uppercase-string>", $array);
24+
}
25+
26+
function arrayWalkNoModification(): void
27+
{
28+
/** @var array<string, int> $array */
29+
$array = ['a' => 1, 'b' => 2];
30+
array_walk($array, function (&$value, $key): void {
31+
echo $value;
32+
});
33+
assertType("array<string, int>", $array);
34+
}
35+
36+
function arrayWalkConditionalModification(): void
37+
{
38+
/** @var array<string, int> $array */
39+
$array = ['a' => 1, 'b' => 2];
40+
array_walk($array, function (&$value, string $key): void {
41+
if ($key === 'a') {
42+
$value = 'modified';
43+
return;
44+
}
45+
});
46+
assertType("array<string, 'modified'|int>", $array);
47+
}
48+
49+
function arrayWalkWithoutByRef(): void
50+
{
51+
/** @var array<string, int> $array */
52+
$array = ['a' => 1, 'b' => 2];
53+
array_walk($array, function ($value, $key): void {
54+
$value = (string) $value;
55+
});
56+
assertType("array<string, int>", $array);
57+
}
58+
59+
function arrayWalkNonEmptyArray(): void
60+
{
61+
/** @var non-empty-array<string, int> $array */
62+
$array = ['a' => 1];
63+
array_walk($array, function (&$value): void {
64+
$value = (string) $value;
65+
});
66+
assertType("non-empty-array<string, lowercase-string&numeric-string&uppercase-string>", $array);
67+
}
68+
69+
function arrayWalkList(): void
70+
{
71+
/** @var list<int> $list */
72+
$list = [1, 2, 3];
73+
array_walk($list, function (&$value): void {
74+
$value = (string) $value;
75+
});
76+
assertType("list<lowercase-string&numeric-string&uppercase-string>", $list);
77+
}
78+
79+
function arrayWalkAlwaysTerminating(): void
80+
{
81+
/** @var array<string, int> $array */
82+
$array = ['a' => 1, 'b' => 2];
83+
array_walk($array, function (&$value): void {
84+
$value = (string) $value;
85+
return;
86+
});
87+
assertType("array<string, lowercase-string&numeric-string&uppercase-string>", $array);
88+
}
89+
90+
function arrayWalkNestedArray(): void
91+
{
92+
$array = ['a' => ['x' => 1, 'y' => 2], 'b' => ['z' => 3]];
93+
array_walk($array, function (&$value): void {
94+
$value = count($value);
95+
});
96+
assertType("array{a: 1|2, b: 1|2}", $array);
97+
}
98+
99+
function arrayWalkWithNestedClosure(): void
100+
{
101+
/** @var array<string, int> $array */
102+
$array = ['a' => 1, 'b' => 2];
103+
array_walk($array, function (&$value): void {
104+
$inner = array_map(function ($x) {
105+
return $x * 2;
106+
}, [1, 2, 3]);
107+
$value = (string) $value;
108+
});
109+
assertType("array<string, lowercase-string&numeric-string&uppercase-string>", $array);
110+
}
111+
112+
function arrayWalkWithNestedClosureByRef(): void
113+
{
114+
/** @var array<string, int> $array */
115+
$array = ['a' => 1, 'b' => 2];
116+
array_walk($array, function (&$value): void {
117+
$capture = null;
118+
$fn = function () use (&$capture): void {
119+
$capture = 'hello';
120+
};
121+
$fn();
122+
$value = (string) $value;
123+
});
124+
assertType("array<string, lowercase-string&numeric-string&uppercase-string>", $array);
125+
}

tests/PHPStan/Rules/Arrays/data/bug-7469.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ function doFoo() {
3232
array_walk($data['languages'], static function (&$item) {
3333
$item = strtolower(trim($item));
3434
});
35-
assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('languages', non-empty-list<string>)", $data);
35+
assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('languages', non-empty-list<lowercase-string>)", $data);
3636

3737
$data['videoOnline'] = normalizePrice($data['videoOnline']);
3838
$data['videoTvc'] = normalizePrice($data['videoTvc']);
3939
$data['radio'] = normalizePrice($data['radio']);
4040

4141
$data['invoicing'] = $data['invoicing'] === 'ANO';
42-
assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('invoicing', bool)&hasOffsetValue('languages', non-empty-list<string>)&hasOffsetValue('radio', mixed)&hasOffsetValue('videoOnline', mixed)&hasOffsetValue('videoTvc', mixed)", $data);
42+
assertType("non-empty-array<'address'|'bankAccount'|'birthDate'|'email'|'firstName'|'ic'|'invoicing'|'invoicingAddress'|'languages'|'lastName'|'note'|'phone'|'radio'|'videoOnline'|'videoTvc'|'voiceExample', mixed>&hasOffsetValue('invoicing', bool)&hasOffsetValue('languages', non-empty-list<lowercase-string>)&hasOffsetValue('radio', mixed)&hasOffsetValue('videoOnline', mixed)&hasOffsetValue('videoTvc', mixed)", $data);
4343
}
4444

4545
function normalizePrice($value)

0 commit comments

Comments
 (0)