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