1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\ShouldNotHappenException;
6: use PHPStan\TrinaryLogic;
7: use PHPStan\Turbo\ShadowedByTurboExtension;
8: use function array_merge;
9: use function array_unique;
10: use function array_values;
11:
12: /**
13: * Result of a Type::accepts() check — whether one type accepts another.
14: *
15: * Wraps a TrinaryLogic result together with human-readable reasons explaining
16: * why the acceptance failed. These reasons are surfaced in PHPStan error messages
17: * to help developers understand type mismatches.
18: *
19: * For example, when checking if `int` accepts `string`, the result would be No
20: * with a reason like "string is not a subtype of int".
21: *
22: * The `accepts()` method is used to check assignability — whether a value of one
23: * type can be assigned to a variable/parameter of another type. This is stricter
24: * than `isSuperTypeOf()` because it accounts for PHPStan's rule level and
25: * generics variance.
26: *
27: * @api
28: */
29: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/AcceptsResult.cpp')]
30: final class AcceptsResult
31: {
32:
33: private static self $YES;
34:
35: private static self $MAYBE;
36:
37: private static self $NO;
38:
39: /**
40: * @api
41: * @param list<string> $reasons Human-readable explanations of why acceptance failed
42: */
43: public function __construct(
44: public readonly TrinaryLogic $result,
45: public readonly array $reasons,
46: )
47: {
48: }
49:
50: /**
51: * @phpstan-assert-if-true =false $this->no()
52: * @phpstan-assert-if-true =false $this->maybe()
53: */
54: public function yes(): bool
55: {
56: return $this->result->yes();
57: }
58:
59: /**
60: * @phpstan-assert-if-true =false $this->no()
61: * @phpstan-assert-if-true =false $this->yes()
62: */
63: public function maybe(): bool
64: {
65: return $this->result->maybe();
66: }
67:
68: /**
69: * @phpstan-assert-if-true =false $this->maybe()
70: * @phpstan-assert-if-true =false $this->yes()
71: */
72: public function no(): bool
73: {
74: return $this->result->no();
75: }
76:
77: public static function createYes(): self
78: {
79: return self::$YES ??= new self(TrinaryLogic::createYes(), []);
80: }
81:
82: /** @param list<string> $reasons */
83: public static function createNo(array $reasons = []): self
84: {
85: if ($reasons === []) {
86: return self::$NO ??= new self(TrinaryLogic::createNo(), $reasons);
87: }
88: return new self(TrinaryLogic::createNo(), $reasons);
89: }
90:
91: public static function createMaybe(): self
92: {
93: return self::$MAYBE ??= new self(TrinaryLogic::createMaybe(), []);
94: }
95:
96: public static function createFromBoolean(bool $value): self
97: {
98: if ($value === true) {
99: return self::createYes();
100: }
101: return self::createNo();
102: }
103:
104: public function and(self $other): self
105: {
106: return new self(
107: $this->result->and($other->result),
108: array_values(array_unique(array_merge($this->reasons, $other->reasons))),
109: );
110: }
111:
112: public function or(self $other): self
113: {
114: return new self(
115: $this->result->or($other->result),
116: array_values(array_unique(array_merge($this->reasons, $other->reasons))),
117: );
118: }
119:
120: /** @param callable(string): string $cb */
121: public function decorateReasons(callable $cb): self
122: {
123: $reasons = [];
124: foreach ($this->reasons as $reason) {
125: $reasons[] = $cb($reason);
126: }
127:
128: return new self($this->result, $reasons);
129: }
130:
131: /** @see TrinaryLogic::extremeIdentity() */
132: public static function extremeIdentity(self ...$operands): self
133: {
134: if ($operands === []) {
135: throw new ShouldNotHappenException();
136: }
137:
138: $results = [];
139: $reasons = [];
140: foreach ($operands as $operand) {
141: $results[] = $operand->result;
142: foreach ($operand->reasons as $reason) {
143: $reasons[] = $reason;
144: }
145: }
146:
147: return new self(TrinaryLogic::extremeIdentity(...$results), array_values(array_unique($reasons)));
148: }
149:
150: /** @see TrinaryLogic::maxMin() */
151: public static function maxMin(self ...$operands): self
152: {
153: if ($operands === []) {
154: throw new ShouldNotHappenException();
155: }
156:
157: $results = [];
158: $reasons = [];
159: foreach ($operands as $operand) {
160: $results[] = $operand->result;
161: foreach ($operand->reasons as $reason) {
162: $reasons[] = $reason;
163: }
164: }
165:
166: return new self(TrinaryLogic::maxMin(...$results), array_values(array_unique($reasons)));
167: }
168:
169: /**
170: * @template T
171: * @param T[] $objects
172: * @param callable(T): self $callback
173: */
174: public static function lazyMaxMin(
175: array $objects,
176: callable $callback,
177: ): self
178: {
179: $reasons = [];
180: $hasNo = false;
181: foreach ($objects as $object) {
182: $isAcceptedBy = $callback($object);
183: if ($isAcceptedBy->result->yes()) {
184: return $isAcceptedBy;
185: } elseif ($isAcceptedBy->result->no()) {
186: $hasNo = true;
187: }
188:
189: foreach ($isAcceptedBy->reasons as $reason) {
190: $reasons[] = $reason;
191: }
192: }
193:
194: return new self(
195: $hasNo ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe(),
196: array_values(array_unique($reasons)),
197: );
198: }
199:
200: }
201: