Conversation
`GenericObjectTypeCheck::getGenericTypes()` walks the whole PHPDoc type with `TypeTraverser::map()` to collect every `GenericObjectType` and `GenericStaticType`. It now returns early when the type references no class (`getReferencedClasses() === []`) and `hasTemplateOrLateResolvableType()` is false. A generic object type always references its class, so the first check alone would miss nothing from that side. The second condition covers what `getReferencedClasses()` does not see: a template type's default (e.g. `@template T = Box<int, string, bool>`, then `@param T`) and late-resolvable types; both make `hasTemplateOrLateResolvableType()` true. When the traversal does run, nothing changes: it still collects every occurrence with `map()`, so this is independent from phpstan#6652. This matters on the symfony/ux-css array shape (847 keys, 144 of them a nested shape alias, no class anywhere), where the traversal was visiting the shared nested shape once per offset using it. With phpstan#6671, both `getReferencedClasses()` and `hasTemplateOrLateResolvableType()` are memoized on `ConstantArrayType`, so the check is nearly free there. `IncompatiblePhpDocTypeRuleTest::testGenericObjectTypeInTemplateDefault` covers the template default case. It passes before and after this change, and fails if the guard only checks `getReferencedClasses()`. As a safety check, I temporarily logged every traversal that found a generic type while the guard would have skipped it: 0 cases out of 1623 traversals that found one, over the full test suite and PHPStan's self-analysis. Numbers from https://github.com/Kocal/sf-ux-css-phpstan-reproducer with `phpstan-nested.neon`, `--debug`, 6 interleaved runs, first discarded: | Scenario | Before | After | | --- | --- | --- | | 2.3.x without turbo | 12.98-13.21 s | 12.79-12.85 s | | 2.3.x + phpstan#6652 + phpstan#6671 + phpstan#6672 without turbo | 2.14-2.26 s | 1.66-1.71 s | | 2.3.x with turbo | 5.90-6.07 s | 5.65-5.69 s | On ordinary code the guard costs nothing measurable: PHPStan analysing its own `src/Type` (about 28 s) calls it 683 times, the guard costs about 1 ms in total and skips about 0.75 ms of traversal. `make tests` passes with and without the turbo extension, `make phpstan` and `make cs` pass. Refs phpstan/phpstan#15348
This was referenced Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GenericObjectTypeCheck::getGenericTypes()walks the whole PHPDoc type withTypeTraverser::map()to collect everyGenericObjectTypeandGenericStaticType. It now returns early when the type references no class (getReferencedClasses() === []) andhasTemplateOrLateResolvableType()is false. A generic object type always references its class, so the first check alone would miss nothing from that side. The second condition covers whatgetReferencedClasses()does not see: a template type's default (e.g.@template T = Box<int, string, bool>, then@param T) and late-resolvable types; both makehasTemplateOrLateResolvableType()true. When the traversal does run, nothing changes: it still collects every occurrence withmap(), so this is independent from #6652.This matters on the symfony/ux-css array shape (847 keys, 144 of them a nested shape alias, no class anywhere), where the traversal was visiting the shared nested shape once per offset using it. With #6671, both
getReferencedClasses()andhasTemplateOrLateResolvableType()are memoized onConstantArrayType, so the check is nearly free there.IncompatiblePhpDocTypeRuleTest::testGenericObjectTypeInTemplateDefaultcovers the template default case. It passes before and after this change, and fails if the guard only checksgetReferencedClasses().As a safety check, I temporarily logged every traversal that found a generic type while the guard would have skipped it: 0 cases out of 1623 traversals that found one, over the full test suite and PHPStan's self-analysis.
Numbers from https://github.com/Kocal/sf-ux-css-phpstan-reproducer with
phpstan-nested.neon,--debug, 6 interleaved runs, first discarded:On ordinary code the guard costs nothing measurable: PHPStan analysing its own
src/Type(about 28 s) calls it 683 times, the guard costs about 1 ms in total and skips about 0.75 ms of traversal.make testspasses with and without the turbo extension,make phpstanandmake cspass.Refs phpstan/phpstan#15348