1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node\Expr;
6: use PHPStan\Type\Type;
7: use PHPStan\Type\TypeCombinator;
8: use function array_key_exists;
9: use function array_merge;
10:
11: final class SpecifiedTypes
12: {
13:
14: private bool $overwrite = false;
15:
16: /** @var array<string, ConditionalExpressionHolder[]> */
17: private array $newConditionalExpressionHolders = [];
18:
19: private ?Expr $rootExpr = null;
20:
21: /**
22: * @api
23: * @param array<string, array{Expr, Type}> $sureTypes
24: * @param array<string, array{Expr, Type}> $sureNotTypes
25: */
26: public function __construct(
27: private array $sureTypes = [],
28: private array $sureNotTypes = [],
29: )
30: {
31: }
32:
33: /**
34: * Normally, $sureTypes in truthy context are used to intersect with the pre-existing type.
35: * And $sureNotTypes are used to remove type from the pre-existing type.
36: *
37: * Example: By default, non-empty-string intersected with '' (ConstantStringType) will lead to NeverType.
38: * Because it's not possible to narrow non-empty-string to an empty string.
39: *
40: * In rare cases, a type-specifying extension might want to overwrite the pre-existing types
41: * without taking the pre-existing types into consideration.
42: *
43: * In that case it should also call setAlwaysOverwriteTypes() on
44: * the returned object.
45: *
46: * ! Only do this if you're certain. Otherwise, this is a source of common bugs. !
47: *
48: * @api
49: */
50: public function setAlwaysOverwriteTypes(): self
51: {
52: $self = new self($this->sureTypes, $this->sureNotTypes);
53: $self->overwrite = true;
54: $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
55: $self->rootExpr = $this->rootExpr;
56:
57: return $self;
58: }
59:
60: /**
61: * @api
62: */
63: public function setRootExpr(?Expr $rootExpr): self
64: {
65: $self = new self($this->sureTypes, $this->sureNotTypes);
66: $self->overwrite = $this->overwrite;
67: $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
68: $self->rootExpr = $rootExpr;
69:
70: return $self;
71: }
72:
73: /**
74: * @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
75: */
76: public function setNewConditionalExpressionHolders(array $newConditionalExpressionHolders): self
77: {
78: $self = new self($this->sureTypes, $this->sureNotTypes);
79: $self->overwrite = $this->overwrite;
80: $self->newConditionalExpressionHolders = $newConditionalExpressionHolders;
81: $self->rootExpr = $this->rootExpr;
82:
83: return $self;
84: }
85:
86: /**
87: * @api
88: * @return array<string, array{Expr, Type}>
89: */
90: public function getSureTypes(): array
91: {
92: return $this->sureTypes;
93: }
94:
95: /**
96: * @api
97: * @return array<string, array{Expr, Type}>
98: */
99: public function getSureNotTypes(): array
100: {
101: return $this->sureNotTypes;
102: }
103:
104: public function shouldOverwrite(): bool
105: {
106: return $this->overwrite;
107: }
108:
109: /**
110: * @return array<string, ConditionalExpressionHolder[]>
111: */
112: public function getNewConditionalExpressionHolders(): array
113: {
114: return $this->newConditionalExpressionHolders;
115: }
116:
117: public function getRootExpr(): ?Expr
118: {
119: return $this->rootExpr;
120: }
121:
122: public function removeExpr(string $exprString): self
123: {
124: $sureTypes = $this->sureTypes;
125: $sureNotTypes = $this->sureNotTypes;
126: unset($sureTypes[$exprString]);
127: unset($sureNotTypes[$exprString]);
128:
129: $self = new self($sureTypes, $sureNotTypes);
130: $self->overwrite = $this->overwrite;
131: $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
132: $self->rootExpr = $this->rootExpr;
133:
134: return $self;
135: }
136:
137: /** @api */
138: public function intersectWith(SpecifiedTypes $other): self
139: {
140: $sureTypeUnion = [];
141: $sureNotTypeUnion = [];
142: $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr);
143:
144: foreach ($this->sureTypes as $exprString => [$exprNode, $type]) {
145: if (!isset($other->sureTypes[$exprString])) {
146: continue;
147: }
148:
149: $sureTypeUnion[$exprString] = [
150: $exprNode,
151: TypeCombinator::union($type, $other->sureTypes[$exprString][1]),
152: ];
153: }
154:
155: foreach ($this->sureNotTypes as $exprString => [$exprNode, $type]) {
156: if (!isset($other->sureNotTypes[$exprString])) {
157: continue;
158: }
159:
160: $sureNotTypeUnion[$exprString] = [
161: $exprNode,
162: TypeCombinator::intersect($type, $other->sureNotTypes[$exprString][1]),
163: ];
164: }
165:
166: $result = new self($sureTypeUnion, $sureNotTypeUnion);
167: if ($this->overwrite && $other->overwrite) {
168: $result = $result->setAlwaysOverwriteTypes();
169: }
170:
171: return $result->setRootExpr($rootExpr);
172: }
173:
174: /** @api */
175: public function unionWith(SpecifiedTypes $other): self
176: {
177: $sureTypeUnion = $this->sureTypes + $other->sureTypes;
178: $sureNotTypeUnion = $this->sureNotTypes + $other->sureNotTypes;
179: $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr);
180:
181: foreach ($this->sureTypes as $exprString => [$exprNode, $type]) {
182: if (!isset($other->sureTypes[$exprString])) {
183: continue;
184: }
185:
186: $sureTypeUnion[$exprString] = [
187: $exprNode,
188: TypeCombinator::intersect($type, $other->sureTypes[$exprString][1]),
189: ];
190: }
191:
192: foreach ($this->sureNotTypes as $exprString => [$exprNode, $type]) {
193: if (!isset($other->sureNotTypes[$exprString])) {
194: continue;
195: }
196:
197: $sureNotTypeUnion[$exprString] = [
198: $exprNode,
199: TypeCombinator::union($type, $other->sureNotTypes[$exprString][1]),
200: ];
201: }
202:
203: $result = new self($sureTypeUnion, $sureNotTypeUnion);
204: if ($this->overwrite || $other->overwrite) {
205: $result = $result->setAlwaysOverwriteTypes();
206: }
207:
208: $conditionalExpressionHolders = $this->newConditionalExpressionHolders;
209: foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
210: if (!array_key_exists($exprString, $conditionalExpressionHolders)) {
211: $conditionalExpressionHolders[$exprString] = $holders;
212: } else {
213: $conditionalExpressionHolders[$exprString] = array_merge($conditionalExpressionHolders[$exprString], $holders);
214: }
215: }
216: $result->newConditionalExpressionHolders = $conditionalExpressionHolders;
217:
218: return $result->setRootExpr($rootExpr);
219: }
220:
221: public function normalize(Scope $scope): self
222: {
223: $sureTypes = $this->sureTypes;
224:
225: foreach ($this->sureNotTypes as $exprString => [$exprNode, $sureNotType]) {
226: if (!isset($sureTypes[$exprString])) {
227: $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($scope->getType($exprNode), $sureNotType)];
228: continue;
229: }
230:
231: $sureTypes[$exprString][1] = TypeCombinator::remove($sureTypes[$exprString][1], $sureNotType);
232: }
233:
234: $result = new self($sureTypes, []);
235: if ($this->overwrite) {
236: $result = $result->setAlwaysOverwriteTypes();
237: }
238:
239: return $result->setRootExpr($this->rootExpr);
240: }
241:
242: private function mergeRootExpr(?Expr $rootExprA, ?Expr $rootExprB): ?Expr
243: {
244: if ($rootExprA === $rootExprB) {
245: return $rootExprA;
246: }
247:
248: if ($rootExprA === null || $rootExprB === null) {
249: return $rootExprA ?? $rootExprB;
250: }
251:
252: return null;
253: }
254:
255: }
256: