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