Skip to content

Commit baad7b6

Browse files
committed
Skip the generic object type search on types without classes
`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
1 parent b0e7a99 commit baad7b6

3 files changed

Lines changed: 58 additions & 0 deletions

File tree

‎src/Rules/Generics/GenericObjectTypeCheck.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ public function check(
181181
*/
182182
private function getGenericTypes(Type $phpDocType): array
183183
{
184+
// a generic type references its class, unless it hides in a template default or a late-resolvable type
185+
if ($phpDocType->getReferencedClasses() === [] && !$phpDocType->hasTemplateOrLateResolvableType()) {
186+
return [];
187+
}
188+
184189
$genericObjectTypes = [];
185190
TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type {
186191
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {

‎tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,4 +548,22 @@ public function testBug11510(): void
548548
$this->analyse([__DIR__ . '/data/bug-11510.php'], []);
549549
}
550550

551+
public function testGenericObjectTypeInTemplateDefault(): void
552+
{
553+
$this->analyse([__DIR__ . '/data/generic-object-type-template-default.php'], [
554+
[
555+
'Generic type GenericObjectTypeTemplateDefault\Box<int, string, bool> in PHPDoc tag @param for parameter $value specifies 3 template types, but class GenericObjectTypeTemplateDefault\Box supports only 2: TKey, TValue',
556+
23,
557+
],
558+
[
559+
'Generic type GenericObjectTypeTemplateDefault\Box<int, string, bool> in PHPDoc tag @return specifies 3 template types, but class GenericObjectTypeTemplateDefault\Box supports only 2: TKey, TValue',
560+
23,
561+
],
562+
[
563+
'Generic type GenericObjectTypeTemplateDefault\Box<int, string, bool> in PHPDoc tag @param for parameter $value specifies 3 template types, but class GenericObjectTypeTemplateDefault\Box supports only 2: TKey, TValue',
564+
31,
565+
],
566+
]);
567+
}
568+
551569
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace GenericObjectTypeTemplateDefault;
4+
5+
/**
6+
* @template TKey
7+
* @template TValue
8+
*/
9+
class Box
10+
{
11+
}
12+
13+
/**
14+
* @template T = Box<int, string, bool>
15+
*/
16+
class Holder
17+
{
18+
19+
/**
20+
* @param T $value
21+
* @return T
22+
*/
23+
public function pass($value)
24+
{
25+
return $value;
26+
}
27+
28+
/**
29+
* @param T $value
30+
*/
31+
public function take($value): void
32+
{
33+
}
34+
35+
}

0 commit comments

Comments
 (0)