Skip to content

Do not cache isGeneric() or the class PHPDoc while the class name scope is created - #6726

Open
GErpeldinger wants to merge 2 commits into
phpstan:2.3.xfrom
GErpeldinger:fix-self-referencing-template-bound
Open

GErpeldinger wants to merge 2 commits into
phpstan:2.3.xfrom
GErpeldinger:fix-self-referencing-template-bound

Conversation

@GErpeldinger

@GErpeldinger GErpeldinger commented Oct 10, 2026 •

Copy link
Copy Markdown

Closes phpstan/phpstan#15448

A template bound that names the class itself (@template T of Error|ErrorIterator) leads FileTypeMapper::getNameScope() back to ClassReflection::isGeneric() for the same class, through TypeCombinator::union() and ObjectType::getEnumCases(). The template tags are not resolved yet at that moment, so isGeneric() cached false, and getResolvedPhpDoc() cached a PHPDoc without template tags. Both stayed wrong for the rest of the process.

FileTypeMapper now tracks the classes whose name scope is being created. ClassReflection does not cache these two values while its class is in that set. The same change is made in turbo-ext/src/ClassReflection.cpp.

  • Tests: the two new cases in WrongVariableNameInVarTagRuleTest fail before the fix: class declared in the analysed file, and class only used from it. The full suite passes with and without the extension. make phpstan, make cs, and the turbo smoke test, signature parity and side-by-side pass.

  • testBug10049: one more error, a missing type on SimpleEntity. It comes from how PHPStan approximates @template SELF of SimpleEntity<SELF>, a bound that refers to its own template: the result is SimpleEntity<SimpleEntity>, and the inner SimpleEntity has no type. The wrong cache hid it before.

  • Performance: no slowdown. Symfony 8.2 src/, cold cache, before and after in alternating order, 4 to 5 runs each (the first run after a container start is left out), same 2,113 errors in every run:

    before after
    without turbo 29.4 – 30.8 s 28.6 – 29.5 s
    with turbo 16.9 – 17.7 s 16.1 – 16.9 s

    phpbench: +1.0% total time, smaller than the noise between two runs of the same code (+1.9%).

I do not know C++. Claude (AI) wrote the turbo change, and I could not review it. It was checked with: the regression tests failing with the unmodified extension and passing with the new one, the full suite with the extension loaded, the smoke test, signature parity, side-by-side, and the Symfony runs above.

🤖 AI-written description.

…pe is created

A template bound that names the class itself (@template T of Error|ErrorIterator)
leads FileTypeMapper::getNameScope() back to ClassReflection::isGeneric() for the
same class, through TypeCombinator::union() and ObjectType::getEnumCases(). The
template tags are not resolved yet at that moment, so isGeneric() cached false,
and getResolvedPhpDoc() cached a PHPDoc without template tags. Both stayed wrong
for the rest of the process.

FileTypeMapper now tracks the classes whose name scope is being created, and
ClassReflection does not cache these two values while its class is in that set.
The same change is made in turbo-ext/src/ClassReflection.cpp.
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.

Class with a template bound that names the class itself is sometimes treated as non-generic

1 participant