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