Skip to content

Resolve late-resolvable types when reading a variable's scope state - #6915

Draft
puggan wants to merge 1 commit into
phpstan:2.3.xfrom
puggan:scope-state-variable-late-resolvable
Draft

puggan wants to merge 1 commit into
phpstan:2.3.xfrom
puggan:scope-state-variable-late-resolvable

Conversation

@puggan

@puggan puggan commented Nov 2, 2026

Copy link
Copy Markdown

Closes phpstan/phpstan#15644

Disclosure: I worked this out with an AI assistant (Claude). The diagnosis, the fix and the test runs below were all done and checked for real.

resolveScopeStateType() resolves late-resolvable types for a tracked expression (TypeUtils::resolveLateResolvableTypes($trackedType)), but returned a variable's type as stored. A parameter typed as a conditional type intersected with an array shape, such as

@param (T is true ? array{recordinfo: …, ...<string, mixed>} : array{recordinfo?: never, ...<string, mixed>})&array{a: string, b: int|''} $row

therefore reached specifyExpressionTypeInPlace() unresolved. Narrowing one of its offsets ($row['a'] ?: null) intersected HasOffsetValueType with the unresolved conditional, and the shape was lost: every later read of $row['b'] was mixed. Before 2.3.0, this path read the variable through getType(), which resolves it.

The variable branch now resolves late-resolvable types too, the same as the tracked-expression branch above it. The same change is mirrored in turbo-ext/src/MutatingScope.cpp.

Testing

  • New tests/PHPStan/Analyser/nsrt/bug-15644.php, the issue's playground snippet with assertType(): fails without the fix (line 18 non-empty-array{recordinfo?: array{id: string}, ...<string, mixed>}, line 19 mixed) and passes with it.
  • Full suite (paratest), PHP 8.5: 22,941 tests OK without the extension, and OK with the extension built from this branch loaded through PHP_INI_SCAN_DIR. The 2 deprecations are ReflectionProperty::setAccessible() in ObjectTypeTest and ForeachHandlerTest, and show up both ways.
  • Turbo, built in php:8.5-cli with make WARN_FLAGS="$(make -s print-warn-flags)": tests/smoke.php ALL OK, bin/side-by-side.php OK, tests/signature-parity.php OK (16,504 members). bin/phpstan analyse --error-format=raw src/Analyser gives identical output with the extension loaded (diagnose: enabled (version 67acd19)) and without it.
  • phpcs (build-cs) and self-analysis of MutatingScope.php are clean.
  • On our own codebase (2.3.4 phar with this line patched, turbo off), the 2,657 errors this regression caused go away. The analysis output for the files where 2.3 found new real problems is unchanged.

I didn't run make lint-turbo (clang-tidy) or make sanitize-turbo locally.

🤖 Generated with Claude Code

resolveScopeStateType() already resolves late-resolvable types for a
tracked expression, but returned a variable's type as stored. A parameter
typed as a conditional type intersected with an array shape therefore
reached specifyExpressionTypeInPlace() unresolved: narrowing one of its
offsets intersected HasOffsetValueType with the unresolved conditional
and dropped the shape. Mirrored in the turbo extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Narrowing an offset on a conditional @param type intersected with an array shape loses the shape (regression in 2.3.0)

1 participant