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: /** @api */
123: public function intersectWith(SpecifiedTypes $other): self
124: {
125: $sureTypeUnion = [];
126: $sureNotTypeUnion = [];
127: $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr);
128:
129: foreach ($this->sureTypes as $exprString => [$exprNode, $type]) {
130: if (!isset($other->sureTypes[$exprString])) {
131: continue;
132: }
133:
134: $sureTypeUnion[$exprString] = [
135: $exprNode,
136: TypeCombinator::union($type, $other->sureTypes[$exprString][1]),
137: ];
138: }
139:
140: foreach ($this->sureNotTypes as $exprString => [$exprNode, $type]) {
141: if (!isset($other->sureNotTypes[$exprString])) {
142: continue;
143: }
144:
145: $sureNotTypeUnion[$exprString] = [
146: $exprNode,
147: TypeCombinator::intersect($type, $other->sureNotTypes[$exprString][1]),
148: ];
149: }
150:
151: $result = new self($sureTypeUnion, $sureNotTypeUnion);
152: if ($this->overwrite && $other->overwrite) {
153: $result = $result->setAlwaysOverwriteTypes();
154: }
155:
156: return $result->setRootExpr($rootExpr);
157: }
158:
159: /** @api */
160: public function unionWith(SpecifiedTypes $other): self
161: {
162: $sureTypeUnion = $this->sureTypes + $other->sureTypes;
163: $sureNotTypeUnion = $this->sureNotTypes + $other->sureNotTypes;
164: $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr);
165:
166: foreach ($this->sureTypes as $exprString => [$exprNode, $type]) {
167: if (!isset($other->sureTypes[$exprString])) {
168: continue;
169: }
170:
171: $sureTypeUnion[$exprString] = [
172: $exprNode,
173: TypeCombinator::intersect($type, $other->sureTypes[$exprString][1]),
174: ];
175: }
176:
177: foreach ($this->sureNotTypes as $exprString => [$exprNode, $type]) {
178: if (!isset($other->sureNotTypes[$exprString])) {
179: continue;
180: }
181:
182: $sureNotTypeUnion[$exprString] = [
183: $exprNode,
184: TypeCombinator::union($type, $other->sureNotTypes[$exprString][1]),
185: ];
186: }
187:
188: $result = new self($sureTypeUnion, $sureNotTypeUnion);
189: if ($this->overwrite || $other->overwrite) {
190: $result = $result->setAlwaysOverwriteTypes();
191: }
192:
193: $conditionalExpressionHolders = $this->newConditionalExpressionHolders;
194: foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
195: if (!array_key_exists($exprString, $conditionalExpressionHolders)) {
196: $conditionalExpressionHolders[$exprString] = $holders;
197: } else {
198: $conditionalExpressionHolders[$exprString] = array_merge($conditionalExpressionHolders[$exprString], $holders);
199: }
200: }
201: $result->newConditionalExpressionHolders = $conditionalExpressionHolders;
202:
203: return $result->setRootExpr($rootExpr);
204: }
205:
206: public function normalize(Scope $scope): self
207: {
208: $sureTypes = $this->sureTypes;
209:
210: foreach ($this->sureNotTypes as $exprString => [$exprNode, $sureNotType]) {
211: if (!isset($sureTypes[$exprString])) {
212: $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($scope->getType($exprNode), $sureNotType)];
213: continue;
214: }
215:
216: $sureTypes[$exprString][1] = TypeCombinator::remove($sureTypes[$exprString][1], $sureNotType);
217: }
218:
219: $result = new self($sureTypes, []);
220: if ($this->overwrite) {
221: $result = $result->setAlwaysOverwriteTypes();
222: }
223:
224: return $result->setRootExpr($this->rootExpr);
225: }
226:
227: private function mergeRootExpr(?Expr $rootExprA, ?Expr $rootExprB): ?Expr
228: {
229: if ($rootExprA === $rootExprB) {
230: return $rootExprA;
231: }
232:
233: if ($rootExprA === null || $rootExprB === null) {
234: return $rootExprA ?? $rootExprB;
235: }
236:
237: return null;
238: }
239:
240: }
241: