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: /**
137: * The result without reasons - the shared instance of createYes(), createNo() or createMaybe().
138: *
139: * @api
140: */
141: public static function createFromTrinaryLogic(TrinaryLogic $result): self
142: {
143: if ($result->yes()) {
144: return self::createYes();
145: }
146: if ($result->no()) {
147: return self::createNo();
148: }
149: return self::createMaybe();
150: }
151:
152: /**
153: * @param list<string> $reasons
154: * @param list<Closure(): string> $lazyReasons
155: */
156: private static function create(TrinaryLogic $result, array $reasons, array $lazyReasons): self
157: {
158: if ($reasons === [] && $lazyReasons === []) {
159: return self::createFromTrinaryLogic($result);
160: }
161: return new self($result, $reasons, $lazyReasons);
162: }
163:
164: public function toAcceptsResult(): AcceptsResult
165: {
166: return new AcceptsResult($this->result, $this->getReasons());
167: }
168:
169: public function and(self ...$others): self
170: {
171: $results = [];
172: $reasons = [];
173: $lazyReasons = [];
174: foreach ($others as $other) {
175: $results[] = $other->result;
176: if ($other->reasons !== []) {
177: $reasons[] = $other->reasons;
178: }
179: if ($other->lazyReasons === []) {
180: continue;
181: }
182:
183: $lazyReasons[] = $other->lazyReasons;
184: }
185:
186: if ($this->reasons === [] && $reasons === [] && $this->lazyReasons === [] && $lazyReasons === []) {
187: return self::createFromTrinaryLogic($this->result->and(...$results));
188: }
189:
190: return self::create(
191: $this->result->and(...$results),
192: array_values(array_unique(array_merge($this->reasons, ...$reasons))),
193: array_merge($this->lazyReasons, ...$lazyReasons),
194: );
195: }
196:
197: public function or(self ...$others): self
198: {
199: $results = [];
200: $reasons = [];
201: $lazyReasons = [];
202: foreach ($others as $other) {
203: $results[] = $other->result;
204: if ($other->reasons !== []) {
205: $reasons[] = $other->reasons;
206: }
207: if ($other->lazyReasons === []) {
208: continue;
209: }
210:
211: $lazyReasons[] = $other->lazyReasons;
212: }
213:
214: if ($this->reasons === [] && $reasons === [] && $this->lazyReasons === [] && $lazyReasons === []) {
215: return self::createFromTrinaryLogic($this->result->or(...$results));
216: }
217:
218: return self::create(
219: $this->result->or(...$results),
220: array_values(array_unique(array_merge($this->reasons, ...$reasons))),
221: array_merge($this->lazyReasons, ...$lazyReasons),
222: );
223: }
224:
225: /** @param callable(string): string $cb */
226: public function decorateReasons(callable $cb): self
227: {
228: $reasons = [];
229: foreach ($this->reasons as $reason) {
230: $reasons[] = $cb($reason);
231: }
232:
233: $lazyReasons = [];
234: foreach ($this->lazyReasons as $lazyReason) {
235: $lazyReasons[] = static fn (): string => $cb($lazyReason());
236: }
237:
238: return self::create($this->result, $reasons, $lazyReasons);
239: }
240:
241: /** @see TrinaryLogic::extremeIdentity() */
242: public static function extremeIdentity(self ...$operands): self
243: {
244: if ($operands === []) {
245: throw new ShouldNotHappenException();
246: }
247:
248: $results = [];
249: $reasons = [];
250: $lazyReasons = [];
251: foreach ($operands as $operand) {
252: $results[] = $operand->result;
253: foreach ($operand->reasons as $reason) {
254: $reasons[] = $reason;
255: }
256: foreach ($operand->lazyReasons as $lazyReason) {
257: $lazyReasons[] = $lazyReason;
258: }
259: }
260:
261: return self::create(TrinaryLogic::extremeIdentity(...$results), array_values(array_unique($reasons)), $lazyReasons);
262: }
263:
264: /** @see TrinaryLogic::maxMin() */
265: public static function maxMin(self ...$operands): self
266: {
267: if ($operands === []) {
268: throw new ShouldNotHappenException();
269: }
270:
271: $results = [];
272: $reasons = [];
273: $lazyReasons = [];
274: foreach ($operands as $operand) {
275: $results[] = $operand->result;
276: foreach ($operand->reasons as $reason) {
277: $reasons[] = $reason;
278: }
279: foreach ($operand->lazyReasons as $lazyReason) {
280: $lazyReasons[] = $lazyReason;
281: }
282: }
283:
284: return self::create(TrinaryLogic::maxMin(...$results), array_values(array_unique($reasons)), $lazyReasons);
285: }
286:
287: /**
288: * @template T
289: * @param T[] $objects
290: * @param callable(T): self $callback
291: */
292: public static function lazyMaxMin(
293: array $objects,
294: callable $callback,
295: ): self
296: {
297: $reasons = [];
298: $lazyReasons = [];
299: $hasNo = false;
300: foreach ($objects as $object) {
301: $isSuperTypeOf = $callback($object);
302: if ($isSuperTypeOf->result->yes()) {
303: return $isSuperTypeOf;
304: } elseif ($isSuperTypeOf->result->no()) {
305: $hasNo = true;
306: }
307:
308: foreach ($isSuperTypeOf->reasons as $reason) {
309: $reasons[] = $reason;
310: }
311: foreach ($isSuperTypeOf->lazyReasons as $lazyReason) {
312: $lazyReasons[] = $lazyReason;
313: }
314: }
315:
316: return self::create(
317: $hasNo ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(),
318: array_values(array_unique($reasons)),
319: $lazyReasons,
320: );
321: }
322:
323: public function negate(): self
324: {
325: return self::create($this->result->negate(), $this->reasons, $this->lazyReasons);
326: }
327:
328: public function describe(): string
329: {
330: return $this->result->describe();
331: }
332:
333: }
334: