Skip to content

Commit 6351afb

Browse files
authored
Add TypeTraverser::mapMemoized() (#6652)
1 parent c876606 commit 6351afb

26 files changed

Lines changed: 1068 additions & 51 deletions

‎src/Analyser/MutatingScope.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2199,7 +2199,7 @@ public function enterPropertyHook(
21992199

22002200
private function transformStaticType(Type $type): Type
22012201
{
2202-
return TypeTraverser::map($type, new TransformStaticTypeTraverser($this));
2202+
return TypeTraverser::mapMemoized($type, new TransformStaticTypeTraverser($this));
22032203
}
22042204

22052205
/**

‎src/Analyser/PhpDocsResolver.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $
279279

280280
private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
281281
{
282-
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
282+
return TypeTraverser::mapMemoized($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
283283
if ($type instanceof StaticType) {
284284
$changedType = $type->changeBaseClass($declaringClass);
285285
if ($declaringClass->isFinal() && !$type instanceof ThisType) {

‎src/Parser/TypeTraverserInstanceofVisitor.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpParser\NodeVisitorAbstract;
88
use PHPStan\DependencyInjection\AutowiredService;
99
use PHPStan\Turbo\ShadowedByTurboExtension;
10+
use function in_array;
1011

1112
#[AutowiredService]
1213
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/TypeTraverserInstanceofVisitor.cpp')]
@@ -37,7 +38,7 @@ public function enterNode(Node $node): ?Node
3738
&& $node->class instanceof Node\Name
3839
&& $node->class->toLowerString() === 'phpstan\\type\\typetraverser'
3940
&& $node->name instanceof Node\Identifier
40-
&& $node->name->toLowerString() === 'map'
41+
&& in_array($node->name->toLowerString(), ['map', 'mapmemoized'], true)
4142
) {
4243
$this->depth++;
4344
}
@@ -53,7 +54,7 @@ public function leaveNode(Node $node): ?Node
5354
&& $node->class instanceof Node\Name
5455
&& $node->class->toLowerString() === 'phpstan\\type\\typetraverser'
5556
&& $node->name instanceof Node\Identifier
56-
&& $node->name->toLowerString() === 'map'
57+
&& in_array($node->name->toLowerString(), ['map', 'mapmemoized'], true)
5758
) {
5859
$this->depth--;
5960
}

‎src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass,
143143

144144
private function transformStaticType(Type $type): Type
145145
{
146-
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
146+
return TypeTraverser::mapMemoized($type, function (Type $type, callable $traverse): Type {
147147
if ($type instanceof GenericStaticType) {
148148
$calledOnTypeReflections = $this->calledOnType->getObjectClassReflections();
149149
if (count($calledOnTypeReflections) === 1) {

‎src/Rules/MissingTypehintCheck.php‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use function array_filter;
3131
use function array_keys;
3232
use function array_merge;
33+
use function array_values;
3334
use function count;
3435
use function implode;
3536
use function in_array;
@@ -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::mapMemoized($type, function (Type $type, callable $traverse) use (&$descriptions): Type {
7778
if ($type instanceof TemplateType) {
7879
return $type;
7980
}
@@ -98,17 +99,19 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
9899
return $traverse(new IntersectionType($nonArrayInner));
99100
}
100101
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
101-
$descriptions = array_merge(
102-
$descriptions,
102+
$branchDescriptions = array_merge(
103103
$this->getIterableTypesWithMissingValueTypehint($type->getIf()),
104104
$this->getIterableTypesWithMissingValueTypehint($type->getElse()),
105105
);
106+
foreach ($branchDescriptions as $description) {
107+
$descriptions[$description] = $description;
108+
}
106109

107110
return $type;
108111
}
109112
if ($type->isIterable()->yes()) {
110113
if ($type->isConstantArray()->yes()) {
111-
$type = TypeTraverser::map($type, static function (Type $type, callable $traverse) {
114+
$type = TypeTraverser::mapMemoized($type, static function (Type $type, callable $traverse) {
112115
if ($type instanceof UnionType || $type instanceof IntersectionType) {
113116
return $traverse($type);
114117
}
@@ -125,7 +128,8 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
125128
}
126129
$iterableValue = $type->getIterableValueType();
127130
if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) {
128-
$descriptions[] = sprintf('iterable type %s', $type->describe(VerbosityLevel::typeOnly()));
131+
$description = sprintf('iterable type %s', $type->describe(VerbosityLevel::typeOnly()));
132+
$descriptions[$description] = $description;
129133
}
130134
if ($type instanceof IntersectionType) {
131135
if ($type->isList()->yes()) {
@@ -138,7 +142,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
138142
return $traverse($type);
139143
});
140144

141-
return $descriptions;
145+
return array_values($descriptions);
142146
}
143147

144148
/**
@@ -147,7 +151,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
147151
public function getNonGenericObjectTypesWithGenericClass(Type $type): array
148152
{
149153
$objectTypes = [];
150-
TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$objectTypes): Type {
154+
TypeTraverser::mapMemoized($type, function (Type $type, callable $traverse) use (&$objectTypes): Type {
151155
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
152156
$traverse($type);
153157
return $type;
@@ -197,17 +201,15 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array
197201
$templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount);
198202
}
199203

200-
$objectTypes[] = [
201-
sprintf('%s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName(false)),
202-
$templateTypesList,
203-
];
204+
$name = sprintf('%s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName(false));
205+
$objectTypes[$name . "\0" . $templateTypesList] = [$name, $templateTypesList];
204206
return $type;
205207
}
206208

207209
return $traverse($type);
208210
});
209211

210-
return $objectTypes;
212+
return array_values($objectTypes);
211213
}
212214

213215
/**

‎src/Rules/PhpDoc/UnresolvableTypeHelper.php‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use PHPStan\Type\NeverType;
99
use PHPStan\Type\Type;
1010
use PHPStan\Type\TypeTraverser;
11-
use function array_unique;
1211
use function array_values;
1312

1413
#[AutowiredService]
@@ -20,7 +19,7 @@ public function getUnresolvableType(Type $type): ?UnresolvableTypeResult
2019
{
2120
$containsUnresolvable = false;
2221
$reasons = [];
23-
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$containsUnresolvable, &$reasons): Type {
22+
TypeTraverser::mapMemoized($type, static function (Type $type, callable $traverse) use (&$containsUnresolvable, &$reasons): Type {
2423
$reason = null;
2524
if ($type instanceof ErrorType) {
2625
$containsUnresolvable = true;
@@ -32,7 +31,7 @@ public function getUnresolvableType(Type $type): ?UnresolvableTypeResult
3231
}
3332

3433
if ($reason !== null) {
35-
$reasons[] = $reason;
34+
$reasons[$reason] = $reason;
3635
}
3736

3837
return $containsUnresolvable ? $type : $traverse($type);
@@ -42,7 +41,7 @@ public function getUnresolvableType(Type $type): ?UnresolvableTypeResult
4241
return null;
4342
}
4443

45-
return new UnresolvableTypeResult(array_values(array_unique($reasons)));
44+
return new UnresolvableTypeResult(array_values($reasons));
4645
}
4746

4847
}

‎src/Rules/RuleLevelHelper.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ private function transformCommonType(Type $type): Type
112112
private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array
113113
{
114114
$checkForUnion = $this->checkUnionTypes;
115-
$acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
115+
$acceptedType = TypeTraverser::mapMemoized($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
116116
if ($acceptedType instanceof CallableType) {
117117
if ($acceptedType->isCommonCallable()) {
118118
return $acceptedType;

‎src/Turbo/TurboExtensionEnabler.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
final class TurboExtensionEnabler
3434
{
3535

36-
public const EXPECTED_EXTENSION_VERSION = '33df0ad';
36+
public const EXPECTED_EXTENSION_VERSION = 'daff12b';
3737

3838
private static bool $active = false;
3939

‎src/Type/Generic/TemplateTypeHelper.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public static function resolveToDefaults(Type $type): Type
106106

107107
public static function resolveToBounds(Type $type): Type
108108
{
109-
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
109+
return TypeTraverser::mapMemoized($type, static function (Type $type, callable $traverse): Type {
110110
while ($type instanceof TemplateType) {
111111
$type = $type->getBound();
112112
}

‎src/Type/TypeTraverser.php‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type;
44

55
use PHPStan\Turbo\ShadowedByTurboExtension;
6+
use function spl_object_id;
67

78
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/TypeTraverser.cpp')]
89
final class TypeTraverser
@@ -11,6 +12,15 @@ final class TypeTraverser
1112
/** @var callable(Type $type, callable(Type): Type $traverse): Type */
1213
private $cb;
1314

15+
/**
16+
* Results of mapMemoized() keyed by spl_object_id() of the mapped type,
17+
* null in map(). The mapped type is kept alive next to its result so that
18+
* its id cannot be reused by another object during the traversal.
19+
*
20+
* @var array<int, array{Type, Type}>|null
21+
*/
22+
private ?array $memo = null;
23+
1424
/**
1525
* Map a Type recursively
1626
*
@@ -43,6 +53,27 @@ public static function map(Type $type, TypeTraverserCallable|callable $cb): Type
4353
return $self->mapInternal($type);
4454
}
4555

56+
/**
57+
* Like map(), but the callback is called only once for each Type instance:
58+
* a Type instance occurring repeatedly in the traversed type (e.g. a type
59+
* alias used in many offsets of an array shape) is replaced with the result
60+
* of its first occurrence, without traversing it again.
61+
*
62+
* Only for callbacks whose result and side effects do not depend on
63+
* where in the traversed type, or how many times, the type occurs.
64+
*
65+
* @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb
66+
*/
67+
public static function mapMemoized(Type $type, TypeTraverserCallable|callable $cb): Type
68+
{
69+
$self = new self($cb);
70+
$self->memo = [];
71+
72+
$traverser = $self->mapInternal($type);
73+
$self->memo = null;
74+
return $traverser;
75+
}
76+
4677
/** @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb */
4778
private function __construct(TypeTraverserCallable|callable $cb)
4879
{
@@ -56,7 +87,19 @@ private function __construct(TypeTraverserCallable|callable $cb)
5687
/** @internal */
5788
public function mapInternal(Type $type): Type
5889
{
59-
return ($this->cb)($type, [$this, 'traverseInternal']);
90+
if ($this->memo === null) {
91+
return ($this->cb)($type, [$this, 'traverseInternal']);
92+
}
93+
94+
$id = spl_object_id($type);
95+
if (isset($this->memo[$id])) {
96+
return $this->memo[$id][1];
97+
}
98+
99+
$result = ($this->cb)($type, [$this, 'traverseInternal']);
100+
$this->memo[$id] = [$type, $result];
101+
102+
return $result;
60103
}
61104

62105
/** @internal */

0 commit comments

Comments
 (0)