Skip to content

Commit fba232d

Browse files
phpstan-botclaude
andcommitted
Move FuncCall type update from resolveType() to filterBySpecifiedTypes()
Address review feedback: instead of re-calculating FuncCall types at type-resolving time, update stored expression types during AST evaluation when their arguments are narrowed. In filterBySpecifiedTypes(), after processing type specifications and conditional expressions, detect FuncCall entries whose arguments' types actually changed. For each, temporarily remove the stored type, compute the dynamic type from the narrowed arguments, then store the intersection. This preserves narrowing from both sources: conditions on the function result (e.g. count($arr) === 3) and conditions on the arguments (e.g. $arr !== []). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 98dad9b commit fba232d

File tree

1 file changed

+46
-19
lines changed

1 file changed

+46
-19
lines changed

src/Analyser/MutatingScope.php

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -973,25 +973,7 @@ private function resolveType(string $exprString, Expr $node): Type
973973
&& !$node instanceof Expr\ArrowFunction
974974
&& $this->hasExpressionType($node)->yes()
975975
) {
976-
$storedType = $this->expressionTypes[$exprString]->getType();
977-
978-
if ($node instanceof FuncCall) {
979-
// Stored expression types for function calls can become stale
980-
// when arguments are narrowed after scope merging.
981-
// Intersect with the dynamically computed type to stay correct.
982-
$this->resolvedTypes[$exprString] = $storedType;
983-
foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) {
984-
if (!$exprHandler->supports($node)) {
985-
continue;
986-
}
987-
$dynamicType = $exprHandler->resolveType($this, $node);
988-
unset($this->resolvedTypes[$exprString]);
989-
return TypeCombinator::intersect($storedType, $dynamicType);
990-
}
991-
unset($this->resolvedTypes[$exprString]);
992-
}
993-
994-
return $storedType;
976+
return $this->expressionTypes[$exprString]->getType();
995977
}
996978

997979
/** @var ExprHandler<Expr> $exprHandler */
@@ -3297,6 +3279,51 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32973279
}
32983280
}
32993281

3282+
// Update stored FuncCall expression types whose arguments were narrowed.
3283+
// The stored type may be stale (from scope merging) while the argument
3284+
// types have been freshly narrowed. Intersect with the dynamically
3285+
// computed type so both sources of narrowing are preserved.
3286+
$funcCallsToUpdate = [];
3287+
foreach ($scope->expressionTypes as $exprString => $exprTypeHolder) {
3288+
if (array_key_exists($exprString, $specifiedExpressions) || array_key_exists($exprString, $conditions)) {
3289+
continue;
3290+
}
3291+
$expr = $exprTypeHolder->getExpr();
3292+
if (!$expr instanceof FuncCall) {
3293+
continue;
3294+
}
3295+
foreach ($expr->getArgs() as $arg) {
3296+
$argKey = $this->getNodeKey($arg->value);
3297+
if (!array_key_exists($argKey, $specifiedExpressions)) {
3298+
continue;
3299+
}
3300+
$oldArgType = array_key_exists($argKey, $this->expressionTypes)
3301+
? $this->expressionTypes[$argKey]->getType()
3302+
: null;
3303+
$newArgType = array_key_exists($argKey, $scope->expressionTypes)
3304+
? $scope->expressionTypes[$argKey]->getType()
3305+
: null;
3306+
if ($oldArgType !== null && $newArgType !== null && $oldArgType->equals($newArgType)) {
3307+
continue;
3308+
}
3309+
$funcCallsToUpdate[$exprString] = $exprTypeHolder;
3310+
break;
3311+
}
3312+
}
3313+
3314+
foreach ($funcCallsToUpdate as $exprString => $exprTypeHolder) {
3315+
$storedType = $exprTypeHolder->getType();
3316+
unset($scope->expressionTypes[$exprString]);
3317+
unset($scope->nativeExpressionTypes[$exprString]);
3318+
unset($scope->resolvedTypes[$exprString]);
3319+
$dynamicType = $scope->getType($exprTypeHolder->getExpr());
3320+
$scope->expressionTypes[$exprString] = new ExpressionTypeHolder(
3321+
$exprTypeHolder->getExpr(),
3322+
TypeCombinator::intersect($storedType, $dynamicType),
3323+
$exprTypeHolder->getCertainty(),
3324+
);
3325+
}
3326+
33003327
return $scope->scopeFactory->create(
33013328
$scope->context,
33023329
$scope->isDeclareStrictTypes(),

0 commit comments

Comments
 (0)