1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use Closure;
6: use PHPStan\ShouldNotHappenException;
7: use PHPStan\TrinaryLogic;
8: use PHPStan\Turbo\ShadowedByTurboExtension;
9: use function array_map;
10: use function array_merge;
11: use function array_unique;
12: use function array_values;
13:
14: /**
15: * Result of a Type::isSuperTypeOf() check — whether one type is a supertype of another.
16: *
17: * Wraps a TrinaryLogic result together with human-readable reasons explaining the
18: * relationship. This is the primary mechanism for comparing types in PHPStan's type system.
19: *
20: * `isSuperTypeOf()` answers: "Can all values of type B also be values of type A?"
21: * For example:
22: * - `(new StringType())->isSuperTypeOf(new ConstantStringType('hello'))` → Yes
23: * - `(new IntegerType())->isSuperTypeOf(new StringType())` → No
24: * - `(new StringType())->isSuperTypeOf(new MixedType())` → Maybe
25: *
26: * This is distinct from `accepts()` which also considers rule levels and PHPDoc context.
27: * Use `isSuperTypeOf()` for type-theoretic comparisons and `accepts()` for assignability checks.
28: *
29: * Reasons can also be provided lazily (as `Closure(): string`) via $lazyReasons. This lets a
30: * type comparison attach an expensive-to-build explanation (e.g. one that calls describe() on
31: * large array shapes) without paying for it on the hot path — the closure only runs when
32: * getReasons() is called, i.e. when the reason is actually rendered.
33: *
34: * Can be converted to AcceptsResult via toAcceptsResult().
35: *
36: * @api
37: */
38: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/IsSuperTypeOfResult.cpp')]
39: final class IsSuperTypeOfResult
40: {
41:
42: private static self $YES;
43:
44: private static self $MAYBE;
45:
46: private static self $NO;
47:
48: /**
49: * @api
50: * @param list<string> $reasons Human-readable explanations of the type relationship
51: * @param list<Closure(): string> $lazyReasons Reasons built on demand, see the class docblock
52: */
53: public function __construct(
54: public readonly TrinaryLogic $result,
55: public readonly array $reasons,
56: public readonly array $lazyReasons = [],
57: )
58: {
59: }
60:
61: /**
62: * @phpstan-assert-if-true =false $this->no()
63: * @phpstan-assert-if-true =false $this->maybe()
64: */
65: public function yes(): bool
66: {
67: return $this->result->yes();
68: }
69:
70: /**
71: * @phpstan-assert-if-true =false $this->no()
72: * @phpstan-assert-if-true =false $this->yes()
73: */
74: public function maybe(): bool
75: {
76: return $this->result->maybe();
77: }
78:
79: /**
80: * @phpstan-assert-if-true =false $this->maybe()
81: * @phpstan-assert-if-true =false $this->yes()
82: */
83: public function no(): bool
84: {
85: return $this->result->no();
86: }
87:
88: /**
89: * All reasons with the lazy ones materialized. Prefer this over reading $reasons directly
90: * when the reasons are going to be rendered.
91: *
92: * @return list<string>
93: */
94: public function getReasons(): array
95: {
96: if ($this->lazyReasons === []) {
97: return $this->reasons;
98: }
99:
100: return array_values(array_unique(array_merge(
101: $this->reasons,
102: array_map(static fn (Closure $cb): string => $cb(), $this->lazyReasons),
103: )));
104: }
105:
106: public static function createYes(): self
107: {
108: return self::$YES ??= new self(TrinaryLogic::createYes(), []);
109: }
110:
111: /**
112: * @param list<string> $reasons
113: * @param list<Closure(): string> $lazyReasons
114: */
115: public static function createNo(array $reasons = [], array $lazyReasons = []): self
116: {
117: if ($reasons === [] && $lazyReasons === []) {
118: return self::$NO ??= new self(TrinaryLogic::createNo(), $reasons);
119: }
120: return new self(TrinaryLogic::createNo(), $reasons, $lazyReasons);
121: }
122:
123: public static function createMaybe(): self
124: {
125: return self::$MAYBE ??= new self(TrinaryLogic::createMaybe(), []);
126: }
127:
128: public static function createFromBoolean(bool $value): self
129: {
130: if ($value === true) {
131: return self::createYes();
132: }
133: return self::createNo();
134: }
135:
136: public function toAcceptsResult(): AcceptsResult
137: {
138: return new AcceptsResult($this->result, $this->getReasons());
139: }
140:
141: public function and(self ...$others): self
142: {
143: $results = [];
144: $reasons = [];
145: $lazyReasons = [];
146: foreach ($others as $other) {
147: $results[] = $other->result;
148: $reasons[] = $other->reasons;
149: $lazyReasons[] = $other->lazyReasons;
150: }
151:
152: return new self(
153: $this->result->and(...$results),
154: array_values(array_unique(array_merge($this->reasons, ...$reasons))),
155: array_merge($this->lazyReasons, ...$lazyReasons),
156: );
157: }
158:
159: public function or(self ...$others): self
160: {
161: $results = [];
162: $reasons = [];
163: $lazyReasons = [];
164: foreach ($others as $other) {
165: $results[] = $other->result;
166: $reasons[] = $other->reasons;
167: $lazyReasons[] = $other->lazyReasons;
168: }
169:
170: return new self(
171: $this->result->or(...$results),
172: array_values(array_unique(array_merge($this->reasons, ...$reasons))),
173: array_merge($this->lazyReasons, ...$lazyReasons),
174: );
175: }
176:
177: /** @param callable(string): string $cb */
178: public function decorateReasons(callable $cb): self
179: {
180: $reasons = [];
181: foreach ($this->reasons as $reason) {
182: $reasons[] = $cb($reason);
183: }
184:
185: $lazyReasons = [];
186: foreach ($this->lazyReasons as $lazyReason) {
187: $lazyReasons[] = static fn (): string => $cb($lazyReason());
188: }
189:
190: return new self($this->result, $reasons, $lazyReasons);
191: }
192:
193: /** @see TrinaryLogic::extremeIdentity() */
194: public static function extremeIdentity(self ...$operands): self
195: {
196: if ($operands === []) {
197: throw new ShouldNotHappenException();
198: }
199:
200: $results = [];
201: $reasons = [];
202: $lazyReasons = [];
203: foreach ($operands as $operand) {
204: $results[] = $operand->result;
205: foreach ($operand->reasons as $reason) {
206: $reasons[] = $reason;
207: }
208: foreach ($operand->lazyReasons as $lazyReason) {
209: $lazyReasons[] = $lazyReason;
210: }
211: }
212:
213: return new self(TrinaryLogic::extremeIdentity(...$results), array_values(array_unique($reasons)), $lazyReasons);
214: }
215:
216: /** @see TrinaryLogic::maxMin() */
217: public static function maxMin(self ...$operands): self
218: {
219: if ($operands === []) {
220: throw new ShouldNotHappenException();
221: }
222:
223: $results = [];
224: $reasons = [];
225: $lazyReasons = [];
226: foreach ($operands as $operand) {
227: $results[] = $operand->result;
228: foreach ($operand->reasons as $reason) {
229: $reasons[] = $reason;
230: }
231: foreach ($operand->lazyReasons as $lazyReason) {
232: $lazyReasons[] = $lazyReason;
233: }
234: }
235:
236: return new self(TrinaryLogic::maxMin(...$results), array_values(array_unique($reasons)), $lazyReasons);
237: }
238:
239: /**
240: * @template T
241: * @param T[] $objects
242: * @param callable(T): self $callback
243: */
244: public static function lazyMaxMin(
245: array $objects,
246: callable $callback,
247: ): self
248: {
249: $reasons = [];
250: $lazyReasons = [];
251: $hasNo = false;
252: foreach ($objects as $object) {
253: $isSuperTypeOf = $callback($object);
254: if ($isSuperTypeOf->result->yes()) {
255: return $isSuperTypeOf;
256: } elseif ($isSuperTypeOf->result->no()) {
257: $hasNo = true;
258: }
259:
260: foreach ($isSuperTypeOf->reasons as $reason) {
261: $reasons[] = $reason;
262: }
263: foreach ($isSuperTypeOf->lazyReasons as $lazyReason) {
264: $lazyReasons[] = $lazyReason;
265: }
266: }
267:
268: return new self(
269: $hasNo ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(),
270: array_values(array_unique($reasons)),
271: $lazyReasons,
272: );
273: }
274:
275: public function negate(): self
276: {
277: return new self($this->result->negate(), $this->reasons, $this->lazyReasons);
278: }
279:
280: public function describe(): string
281: {
282: return $this->result->describe();
283: }
284:
285: }
286: