Skip to content

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

Merged
ondrejmirtes merged 1 commit into
phpstan:2.3.xfrom
GErpeldinger:fix-self-referencing-template-bound
Oct 24, 2026
Merged

ondrejmirtes merged 1 commit into
phpstan:2.3.xfrom
GErpeldinger:fix-self-referencing-template-bound

Conversation

@GErpeldinger

@GErpeldinger GErpeldinger commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

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.
@ondrejmirtes
ondrejmirtes force-pushed the fix-self-referencing-template-bound branch from 59997a6 to 075bda9 Compare October 24, 2026 20:46
@ondrejmirtes
ondrejmirtes merged commit 7d37329 into phpstan:2.3.x Oct 24, 2026
271 checks passed
@ondrejmirtes

Copy link
Copy Markdown
Member

Thank you.

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

2 participants