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: * ```php
34: * TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
35: * if ($type instanceof UnionType || $type instanceof IntersectionType) {
36: * // Traverse inner types
37: * return $traverse($type);
38: * }
39: * if ($type instanceof ConstantStringType) {
40: * // Replaces the current type, and don't traverse
41: * return new ObjectType($type->getValue());
42: * }
43: * // Replaces the current type, and don't traverse
44: * return new MixedType();
45: * });
46: * ```
47: *
48: * @api
49: * @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb
50: */
51: public static function map(Type $type, TypeTraverserCallable|callable $cb): Type
52: {
53: $self = new self($cb);
54:
55: return $self->mapInternal($type);
56: }
57:
58: /**
59: * Like map(), but the callback is called only once for each Type instance:
60: * a Type instance occurring repeatedly in the traversed type (e.g. a type
61: * alias used in many offsets of an array shape) is replaced with the result
62: * of its first occurrence, without traversing it again.
63: *
64: * Only for callbacks whose result and side effects do not depend on
65: * where in the traversed type, or how many times, the type occurs.
66: *
67: * @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb
68: */
69: public static function mapMemoized(Type $type, TypeTraverserCallable|callable $cb): Type
70: {
71: $self = new self($cb);
72: $self->memo = [];
73:
74: $traverser = $self->mapInternal($type);
75: $self->memo = null;
76: return $traverser;
77: }
78:
79: /** @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb */
80: private function __construct(TypeTraverserCallable|callable $cb)
81: {
82: if ($cb instanceof TypeTraverserCallable) {
83: $this->cb = static fn (Type $type, callable $traverse): Type => $cb->traverse($type, $traverse);
84: } else {
85: $this->cb = $cb;
86: }
87: }
88:
89: /** @internal */
90: public function mapInternal(Type $type): Type
91: {
92: if ($this->memo === null) {
93: return ($this->cb)($type, [$this, 'traverseInternal']);
94: }
95:
96: $id = spl_object_id($type);
97: if (isset($this->memo[$id])) {
98: return $this->memo[$id][1];
99: }
100:
101: $result = ($this->cb)($type, [$this, 'traverseInternal']);
102: $this->memo[$id] = [$type, $result];
103:
104: return $result;
105: }
106:
107: /** @internal */
108: public function traverseInternal(Type $type): Type
109: {
110: return $type->traverse([$this, 'mapInternal']);
111: }
112:
113: }
114: