Skip to content

Commit 0bf40df

Browse files
staabmphpstan-bot
authored andcommitted
Fix #15348
1 parent 5ae1e59 commit 0bf40df

10 files changed

Lines changed: 4066 additions & 28 deletions

File tree

‎src/Analyser/Traverser/TransformStaticTypeTraverser.php‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66
use PHPStan\Turbo\ReferencedByTurboExtension;
77
use PHPStan\Type\StaticType;
88
use PHPStan\Type\ThisType;
9+
use PHPStan\Type\Traverser\MemoizingTraverser;
910
use PHPStan\Type\Type;
1011
use PHPStan\Type\TypeTraverserCallable;
1112

1213
#[ReferencedByTurboExtension(key: 'transformStaticTypeTraverser')]
1314
final class TransformStaticTypeTraverser implements TypeTraverserCallable
1415
{
1516

17+
private MemoizingTraverser $memoizingTraverser;
18+
1619
public function __construct(
1720
private readonly Scope $scope,
1821
)
1922
{
23+
$this->memoizingTraverser = new MemoizingTraverser($this->doTraverse(...));
2024
}
2125

2226
/**
@@ -27,6 +31,15 @@ public function traverse(Type $type, callable $traverse): Type
2731
if (!$this->scope->isInClass()) {
2832
return $type;
2933
}
34+
35+
return $this->memoizingTraverser->traverse($type, $traverse);
36+
}
37+
38+
/**
39+
* @param callable(Type): Type $traverse
40+
*/
41+
private function doTraverse(Type $type, callable $traverse): Type
42+
{
3043
if ($type instanceof StaticType) {
3144
$classReflection = $this->scope->getClassReflection();
3245
$changedType = $type->changeBaseClass($classReflection);

‎src/Rules/Classes/LocalTypeAliasesCheck.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\Type\CircularTypeAliasErrorType;
2424
use PHPStan\Type\ErrorType;
2525
use PHPStan\Type\Generic\TemplateType;
26+
use PHPStan\Type\Traverser\MemoizingTraverser;
2627
use PHPStan\Type\Type;
2728
use PHPStan\Type\TypeTraverser;
2829
use PHPStan\Type\VerbosityLevel;
@@ -349,7 +350,7 @@ private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): boo
349350
private function hasErrorType(Type $type, string $aliasName, array &$errors): bool
350351
{
351352
$foundError = false;
352-
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
353+
TypeTraverser::map($type, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
353354
if ($foundError) {
354355
return $type;
355356
}
@@ -371,7 +372,7 @@ private function hasErrorType(Type $type, string $aliasName, array &$errors): bo
371372
}
372373

373374
return $traverse($type);
374-
});
375+
}));
375376

376377
return $foundError;
377378
}

‎src/Rules/Generics/GenericObjectTypeCheck.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Type\Generic\TemplateTypeVariance;
1414
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
1515
use PHPStan\Type\Generic\TypeProjectionHelper;
16+
use PHPStan\Type\Traverser\MemoizingTraverser;
1617
use PHPStan\Type\Type;
1718
use PHPStan\Type\TypeTraverser;
1819
use PHPStan\Type\VerbosityLevel;
@@ -182,7 +183,7 @@ public function check(
182183
private function getGenericTypes(Type $phpDocType): array
183184
{
184185
$genericObjectTypes = [];
185-
TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type {
186+
TypeTraverser::map($phpDocType, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type {
186187
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
187188
$resolvedType = TemplateTypeHelper::resolveToBounds($type);
188189
if (!$resolvedType instanceof GenericObjectType && !$resolvedType instanceof GenericStaticType) {
@@ -194,7 +195,7 @@ private function getGenericTypes(Type $phpDocType): array
194195
}
195196
$traverse($type);
196197
return $type;
197-
});
198+
}));
198199

199200
return $genericObjectTypes;
200201
}

‎src/Rules/MissingTypehintCheck.php‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use PHPStan\Type\IntersectionType;
2323
use PHPStan\Type\MixedType;
2424
use PHPStan\Type\ObjectType;
25+
use PHPStan\Type\Traverser\MemoizingTraverser;
2526
use PHPStan\Type\Type;
2627
use PHPStan\Type\TypeTraverser;
2728
use PHPStan\Type\UnionType;
@@ -73,7 +74,7 @@ public function __construct(
7374
public function getIterableTypesWithMissingValueTypehint(Type $type): array
7475
{
7576
$descriptions = [];
76-
TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$descriptions): Type {
77+
TypeTraverser::map($type, new MemoizingTraverser(function (Type $type, callable $traverse) use (&$descriptions): Type {
7778
if ($type instanceof TemplateType) {
7879
return $type;
7980
}
@@ -108,7 +109,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
108109
}
109110
if ($type->isIterable()->yes()) {
110111
if ($type->isConstantArray()->yes()) {
111-
$type = TypeTraverser::map($type, static function (Type $type, callable $traverse) {
112+
$type = TypeTraverser::map($type, new MemoizingTraverser(static function (Type $type, callable $traverse): Type {
112113
if ($type instanceof UnionType || $type instanceof IntersectionType) {
113114
return $traverse($type);
114115
}
@@ -121,7 +122,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
121122
}
122123

123124
return $traverse($type);
124-
});
125+
}));
125126
}
126127
$iterableValue = $type->getIterableValueType();
127128
if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) {
@@ -136,7 +137,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
136137
}
137138
}
138139
return $traverse($type);
139-
});
140+
}));
140141

141142
return $descriptions;
142143
}
@@ -147,7 +148,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
147148
public function getNonGenericObjectTypesWithGenericClass(Type $type): array
148149
{
149150
$objectTypes = [];
150-
TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$objectTypes): Type {
151+
TypeTraverser::map($type, new MemoizingTraverser(function (Type $type, callable $traverse) use (&$objectTypes): Type {
151152
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
152153
$traverse($type);
153154
return $type;
@@ -205,7 +206,7 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array
205206
}
206207

207208
return $traverse($type);
208-
});
209+
}));
209210

210211
return $objectTypes;
211212
}
@@ -220,7 +221,7 @@ public function getCallablesWithMissingSignature(Type $type): array
220221
}
221222

222223
$result = [];
223-
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type {
224+
TypeTraverser::map($type, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$result): Type {
224225
if (
225226
($type instanceof CallableType && $type->isCommonCallable())
226227
|| ($type instanceof ClosureType && $type->isCommonCallable())
@@ -229,7 +230,7 @@ public function getCallablesWithMissingSignature(Type $type): array
229230
$result[] = $type;
230231
}
231232
return $traverse($type);
232-
});
233+
}));
233234

234235
return $result;
235236
}

‎src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Type\ConditionalTypeForParameter;
1111
use PHPStan\Type\Generic\TemplateType;
1212
use PHPStan\Type\StaticType;
13+
use PHPStan\Type\Traverser\MemoizingTraverser;
1314
use PHPStan\Type\Type;
1415
use PHPStan\Type\TypeTraverser;
1516
use PHPStan\Type\VerbosityLevel;
@@ -30,44 +31,44 @@ public function check(ExtendedParametersAcceptor $acceptor): array
3031
$conditionalTypes = [];
3132
$parametersByName = [];
3233
foreach ($acceptor->getParameters() as $parameter) {
33-
TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
34+
TypeTraverser::map($parameter->getType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
3435
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
3536
$conditionalTypes[] = $type;
3637
}
3738

3839
return $traverse($type);
39-
});
40+
}));
4041

4142
if ($parameter->getOutType() !== null) {
42-
TypeTraverser::map($parameter->getOutType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
43+
TypeTraverser::map($parameter->getOutType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
4344
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
4445
$conditionalTypes[] = $type;
4546
}
4647

4748
return $traverse($type);
48-
});
49+
}));
4950
}
5051

5152
if ($parameter->getClosureThisType() !== null) {
52-
TypeTraverser::map($parameter->getClosureThisType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
53+
TypeTraverser::map($parameter->getClosureThisType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
5354
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
5455
$conditionalTypes[] = $type;
5556
}
5657

5758
return $traverse($type);
58-
});
59+
}));
5960
}
6061

6162
$parametersByName[$parameter->getName()] = $parameter;
6263
}
6364

64-
TypeTraverser::map($acceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
65+
TypeTraverser::map($acceptor->getReturnType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
6566
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
6667
$conditionalTypes[] = $type;
6768
}
6869

6970
return $traverse($type);
70-
});
71+
}));
7172

7273
$errors = [];
7374
foreach ($conditionalTypes as $conditionalType) {
@@ -77,14 +78,14 @@ public function check(ExtendedParametersAcceptor $acceptor): array
7778
continue;
7879
}
7980
$templateTypes = [];
80-
TypeTraverser::map($subjectType, static function (Type $type, callable $traverse) use (&$templateTypes): Type {
81+
TypeTraverser::map($subjectType, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$templateTypes): Type {
8182
if ($type instanceof TemplateType) {
8283
$templateTypes[] = $type;
8384
return $type;
8485
}
8586

8687
return $traverse($type);
87-
});
88+
}));
8889

8990
if (count($templateTypes) === 0) {
9091
$errors[] = RuleErrorBuilder::message(sprintf('Conditional return type uses subject type %s which is not part of PHPDoc @template tags.', $subjectType->describe(VerbosityLevel::typeOnly())))

‎src/Rules/PhpDoc/GenericCallableRuleHelper.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Type\CallableType;
1414
use PHPStan\Type\ClosureType;
1515
use PHPStan\Type\Generic\TemplateTypeScope;
16+
use PHPStan\Type\Traverser\MemoizingTraverser;
1617
use PHPStan\Type\Type;
1718
use PHPStan\Type\TypeTraverser;
1819
use PHPStan\Type\VerbosityLevel;
@@ -46,7 +47,7 @@ public function check(
4647
{
4748
$errors = [];
4849

49-
TypeTraverser::map($callableType, function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) {
50+
TypeTraverser::map($callableType, new MemoizingTraverser(function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) {
5051
if (!($type instanceof CallableType || $type instanceof ClosureType)) {
5152
return $traverse($type);
5253
}
@@ -114,7 +115,7 @@ public function check(
114115
}
115116

116117
return $traverse($type);
117-
});
118+
}));
118119

119120
return $errors;
120121
}

‎src/Rules/RuleLevelHelper.php‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\Type\NeverType;
2020
use PHPStan\Type\NullType;
2121
use PHPStan\Type\StrictMixedType;
22+
use PHPStan\Type\Traverser\MemoizingTraverser;
2223
use PHPStan\Type\Type;
2324
use PHPStan\Type\TypeCombinator;
2425
use PHPStan\Type\TypeTraverser;
@@ -86,7 +87,7 @@ private function transformCommonType(Type $type): Type
8687
return $type;
8788
}
8889

89-
return TypeTraverser::map($type, function (Type $type, callable $traverse) {
90+
return TypeTraverser::map($type, new MemoizingTraverser(function (Type $type, callable $traverse) {
9091
if ($type instanceof TemplateMixedType) {
9192
if ($this->checkExplicitMixed) {
9293
return $type->toStrictMixedType();
@@ -103,7 +104,7 @@ private function transformCommonType(Type $type): Type
103104
}
104105

105106
return $traverse($type);
106-
});
107+
}));
107108
}
108109

109110
/**
@@ -112,7 +113,7 @@ private function transformCommonType(Type $type): Type
112113
private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array
113114
{
114115
$checkForUnion = $this->checkUnionTypes;
115-
$acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
116+
$acceptedType = TypeTraverser::map($acceptedType, new MemoizingTraverser(function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
116117
if ($acceptedType instanceof CallableType) {
117118
if ($acceptedType->isCommonCallable()) {
118119
return $acceptedType;
@@ -175,7 +176,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
175176
}
176177

177178
return $traverse($acceptedType);
178-
});
179+
}));
179180

180181
return [$this->transformCommonType($acceptedType), $checkForUnion];
181182
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Traverser;
4+
5+
use PHPStan\Type\Type;
6+
use PHPStan\Type\TypeTraverserCallable;
7+
use function spl_object_id;
8+
9+
/**
10+
* Calls the wrapped callback only once per distinct Type object within a single
11+
* TypeTraverser::map() call and reuses its result on repeated visits.
12+
*
13+
* Big array shapes built from type aliases share the same inner Type objects many
14+
* times over, so a plain traversal visits orders of magnitude more nodes than there
15+
* are distinct types. Only suitable for callbacks whose result depends on the visited
16+
* type alone — callbacks collecting types into a list see each object once.
17+
*/
18+
final class MemoizingTraverser implements TypeTraverserCallable
19+
{
20+
21+
/** @var array<int, array{Type, Type}> */
22+
private array $results = [];
23+
24+
/**
25+
* @param callable(Type $type, callable(Type): Type $traverse): Type $cb
26+
*/
27+
public function __construct(private $cb)
28+
{
29+
}
30+
31+
/**
32+
* @param callable(Type): Type $traverse
33+
*/
34+
public function traverse(Type $type, callable $traverse): Type
35+
{
36+
$id = spl_object_id($type);
37+
if (isset($this->results[$id])) {
38+
return $this->results[$id][1];
39+
}
40+
41+
$result = ($this->cb)($type, $traverse);
42+
43+
// keeps the visited type alive so that its object id is not reused
44+
$this->results[$id] = [$type, $result];
45+
46+
return $result;
47+
}
48+
49+
}

0 commit comments

Comments
 (0)