Skip to content

Skip the generic object type search on types without classes - #6673

Open
Kocal wants to merge 1 commit into
phpstan:2.3.xfrom
Kocal:generic-object-type-check-skip-traversal
Open

Kocal wants to merge 1 commit into
phpstan:2.3.xfrom
Kocal:generic-object-type-check-skip-traversal

Conversation

@Kocal

@Kocal Kocal commented Oct 4, 2026

Copy link
Copy Markdown

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 #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() 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 + #6652 + #6671 + #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

`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
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.

2 participants