1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Turbo\ShadowedByTurboExtension;
6: use function spl_object_id;
7:
8: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/TypeTraverser.cpp')]
9: final class TypeTraverser
10: {
11:
12: /** @var callable(Type $type, callable(Type): Type $traverse): Type */
13: private $cb;
14:
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:
24: /**
25: * Map a Type recursively
26: *
27: * For every Type instance, the callback can return a new Type, and/or
28: * decide to traverse inner types or to ignore them.
29: *
30: * The following example converts constant strings to objects, while
31: * preserving unions and intersections:
32: *
33: * TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
34: * if ($type instanceof UnionType || $type instanceof IntersectionType) {
35: * // Traverse inner types
36: * return $traverse($type);
37: * }
38: * if ($type instanceof ConstantStringType) {
39: * // Replaces the current type, and don't traverse
40: * return new ObjectType($type->getValue());
41: * }
42: * // Replaces the current type, and don't traverse
43: * return new MixedType();
44: * });
45: *
46: * @api
47: * @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb
48: */
49: public static function map(Type $type, TypeTraverserCallable|callable $cb): Type
50: {
51: $self = new self($cb);
52:
53: return $self->mapInternal($type);
54: }
55:
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:
77: /** @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb */
78: private function __construct(TypeTraverserCallable|callable $cb)
79: {
80: if ($cb instanceof TypeTraverserCallable) {
81: $this->cb = static fn (Type $type, callable $traverse): Type => $cb->traverse($type, $traverse);
82: } else {
83: $this->cb = $cb;
84: }
85: }
86:
87: /** @internal */
88: public function mapInternal(Type $type): Type
89: {
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;
103: }
104:
105: /** @internal */
106: public function traverseInternal(Type $type): Type
107: {
108: return $type->traverse([$this, 'mapInternal']);
109: }
110:
111: }
112: