|
3 | 3 | namespace PHPStan\Analyser\ExprHandler; |
4 | 4 |
|
5 | 5 | use Closure; |
| 6 | +use PhpParser\Node; |
6 | 7 | use PhpParser\Node\Arg; |
7 | 8 | use PhpParser\Node\Expr; |
8 | 9 | use PhpParser\Node\Expr\FuncCall; |
|
28 | 29 | use PHPStan\DependencyInjection\AutowiredService; |
29 | 30 | use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; |
30 | 31 | use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; |
| 32 | +use PHPStan\Node\ClosureReturnStatementsNode; |
31 | 33 | use PHPStan\Node\Expr\NativeTypeExpr; |
32 | 34 | use PHPStan\Node\Expr\PossiblyImpureCallExpr; |
33 | 35 | use PHPStan\Node\Expr\TypeExpr; |
|
61 | 63 | use PHPStan\Type\TypeTraverser; |
62 | 64 | use PHPStan\Type\UnionType; |
63 | 65 | use Throwable; |
| 66 | +use function array_fill; |
64 | 67 | use function array_filter; |
65 | 68 | use function array_map; |
66 | 69 | use function array_merge; |
67 | 70 | use function array_slice; |
68 | 71 | use function array_values; |
69 | 72 | use function count; |
70 | 73 | use function in_array; |
| 74 | +use function is_string; |
71 | 75 | use function sprintf; |
72 | 76 | use function str_starts_with; |
73 | 77 |
|
@@ -201,13 +205,89 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex |
201 | 205 | } |
202 | 206 | } |
203 | 207 |
|
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); |
205 | 271 | $scope = $argsResult->getScope(); |
206 | 272 | $hasYield = $argsResult->hasYield(); |
207 | 273 | $throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints()); |
208 | 274 | $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); |
209 | 275 | $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); |
210 | 276 |
|
| 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 | + |
211 | 291 | if ($normalizedExpr->name instanceof Expr) { |
212 | 292 | $nameType = $scope->getType($normalizedExpr->name); |
213 | 293 | if ( |
@@ -705,6 +785,31 @@ private function getArraySortDoNotPreserveListFunctionType(Type $type): Type |
705 | 785 | }); |
706 | 786 | } |
707 | 787 |
|
| 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 | + |
708 | 813 | public function resolveType(MutatingScope $scope, Expr $expr): Type |
709 | 814 | { |
710 | 815 | if ($expr->name instanceof Expr) { |
|
0 commit comments