Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -973,7 +973,25 @@ private function resolveType(string $exprString, Expr $node): Type
&& !$node instanceof Expr\ArrowFunction
&& $this->hasExpressionType($node)->yes()
) {
return $this->expressionTypes[$exprString]->getType();
$storedType = $this->expressionTypes[$exprString]->getType();

if ($node instanceof FuncCall) {
// Stored expression types for function calls can become stale
// when arguments are narrowed after scope merging.
// Intersect with the dynamically computed type to stay correct.
$this->resolvedTypes[$exprString] = $storedType;
foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) {
if (!$exprHandler->supports($node)) {
continue;
}
$dynamicType = $exprHandler->resolveType($this, $node);
unset($this->resolvedTypes[$exprString]);
return TypeCombinator::intersect($storedType, $dynamicType);
}
unset($this->resolvedTypes[$exprString]);
}

return $storedType;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of re-calculating types at type-resolving time, couldn't we make sure expressionTypes are properly updated when evaluating the AST?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Here's a summary of the changes made to address staabm's review:

Review feedback: Instead of re-calculating types at type-resolving time, update expressionTypes during AST evaluation.

What changed:

  1. Reverted resolveType() back to its original behavior - no more special FuncCall handling at type resolution time.

  2. Added FuncCall type updating in filterBySpecifiedTypes() - when type narrowing is applied from conditions (e.g., $arr !== [] narrows $arr), the method now detects stored FuncCall expression types whose arguments were actually narrowed and updates them by intersecting the stored type with the freshly computed dynamic type.

Key design decisions:

  • Intersection, not invalidation: Simply removing stale FuncCall types would lose precision from conditions on the function result (e.g., count($arr) === 3 gives 3, not int<1, max>). Intersecting preserves narrowing from both sources.
  • Type change guard: Only updates FuncCall entries when the argument's type actually changed, avoiding unnecessary recomputation when narrowing is redundant.
  • Skip specified/conditional entries: FuncCall expressions that were directly specified or updated by conditional expressions are left alone.

Test results: All 11,855 tests pass, PHPStan self-analysis passes with no errors.

}

/** @var ExprHandler<Expr> $exprHandler */
Expand Down
52 changes: 52 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13750.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Bug13750;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/** @param array<mixed> $arr */
public function forgetCount(array $arr): void
{
if (count($arr) > 2) {
assertType('non-empty-array<mixed>', $arr);
assertType('int<3, max>', count($arr));
}
assertType('array<mixed>', $arr);
assertType('int<0, max>', count($arr));
if (count($arr, COUNT_RECURSIVE) > 2) {
assertType('non-empty-array<mixed>', $arr);
assertType('int<1, max>', count($arr));
}
}

/** @param array<mixed> $arr */
public function forgetCountDifferentNarrowing(array $arr): void
{
if (count($arr) > 2) {
}
if ($arr !== []) {
assertType('int<1, max>', count($arr));
}
}

/** @param array<mixed> $arr */
public function sizeofAfterSizeof(array $arr): void
{
if (sizeof($arr) > 2) {
}
if ($arr !== []) {
assertType('int<1, max>', sizeof($arr));
}
}

public function strlenAfterStrlen(string $str): void
{
if (strlen($str) > 5) {
}
if ($str !== '') {
assertType('int<1, max>', strlen($str));
}
}
}
Loading