1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node\Expr;
6: use PHPStan\Type\NeverType;
7: use PHPStan\Type\Type;
8: use PHPStan\Type\TypeCombinator;
9: use function array_key_exists;
10: use function array_merge;
11:
12: final class SpecifiedTypes
13: {
14:
15: private bool $overwrite = false;
16:
17: /** @var array<string, ConditionalExpressionHolder[]> */
18: private array $newConditionalExpressionHolders = [];
19:
20: /**
21: * Deferred boolean-decomposition holders, evaluated against the applying
22: * scope by MutatingScope::filterBySpecifiedTypes().
23: *
24: * @var list<ConditionalExpressionHolderRecipe>
25: */
26: private array $conditionalExpressionHolderRecipes = [];
27:
28: /**
29: * State-dependent augmentations evaluated against the applying scope by
30: * MutatingScope::filterBySpecifiedTypes(); their entries join the applied
31: * batch.
32: *
33: * @var list<DeferredSpecifiedTypesAugment>
34: */
35: private array $deferredAugments = [];
36:
37: private ?Expr $rootExpr = null;
38:
39: /**
40: * Alternative-form entries produced by intersectWith() when the two sides
41: * constrain the same expression with different kinds (a sure type in one
42: * branch, a sure-not in the other). Each term (sure, subtract) reads as
43: * `(sure ?? current type) minus subtract`; the entry's value is the union
44: * of its terms, evaluated by MutatingScope::applySpecifiedTypes() against
45: * the subject's type at the application point - the deferred form of what
46: * the old SpecifiedTypes::normalize() computed eagerly with a scope.
47: *
48: * @var array<string, array{Expr, list<array{?Type, ?Type}>}>
49: */
50: private array $alternativeTypes = [];
51:
52: /**
53: * @api
54: * @param array<string, array{Expr, Type}> $sureTypes
55: * @param array<string, array{Expr, Type}> $sureNotTypes
56: */
57: public function __construct(
58: private array $sureTypes = [],
59: private array $sureNotTypes = [],
60: )
61: {
62: }
63:
64: /**
65: * Normally, $sureTypes in truthy context are used to intersect with the pre-existing type.
66: * And $sureNotTypes are used to remove type from the pre-existing type.
67: *
68: * Example: By default, non-empty-string intersected with '' (ConstantStringType) will lead to NeverType.
69: * Because it's not possible to narrow non-empty-string to an empty string.
70: *
71: * In rare cases, a type-specifying extension might want to overwrite the pre-existing types
72: * without taking the pre-existing types into consideration.
73: *
74: * In that case it should also call setAlwaysOverwriteTypes() on
75: * the returned object.
76: *
77: * ! Only do this if you're certain. Otherwise, this is a source of common bugs. !
78: *
79: * @api
80: */
81: public function setAlwaysOverwriteTypes(): self
82: {
83: $self = clone $this;
84: $self->overwrite = true;
85:
86: return $self;
87: }
88:
89: /**
90: * @api
91: */
92: public function setRootExpr(?Expr $rootExpr): self
93: {
94: $self = clone $this;
95: $self->rootExpr = $rootExpr;
96:
97: return $self;
98: }
99:
100: /**
101: * @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
102: */
103: public function setNewConditionalExpressionHolders(array $newConditionalExpressionHolders): self
104: {
105: $self = clone $this;
106: $self->newConditionalExpressionHolders = $newConditionalExpressionHolders;
107:
108: return $self;
109: }
110:
111: /**
112: * @param list<ConditionalExpressionHolderRecipe> $recipes
113: */
114: public function setConditionalExpressionHolderRecipes(array $recipes): self
115: {
116: $self = clone $this;
117: $self->conditionalExpressionHolderRecipes = $recipes;
118:
119: return $self;
120: }
121:
122: /**
123: * @return list<ConditionalExpressionHolderRecipe>
124: */
125: public function getConditionalExpressionHolderRecipes(): array
126: {
127: return $this->conditionalExpressionHolderRecipes;
128: }
129:
130: public function withDeferredAugment(DeferredSpecifiedTypesAugment $augment): self
131: {
132: $self = clone $this;
133: $self->deferredAugments = [...$this->deferredAugments, $augment];
134:
135: return $self;
136: }
137:
138: /**
139: * @return list<DeferredSpecifiedTypesAugment>
140: */
141: public function getDeferredAugments(): array
142: {
143: return $this->deferredAugments;
144: }
145:
146: /**
147: * @api
148: * @return array<string, array{Expr, Type}>
149: */
150: public function getSureTypes(): array
151: {
152: return $this->sureTypes;
153: }
154:
155: /**
156: * @api
157: * @return array<string, array{Expr, Type}>
158: */
159: public function getSureNotTypes(): array
160: {
161: return $this->sureNotTypes;
162: }
163:
164: /**
165: * @return array<string, array{Expr, list<array{?Type, ?Type}>}>
166: */
167: public function getAlternativeTypes(): array
168: {
169: return $this->alternativeTypes;
170: }
171:
172: /**
173: * A copy without conditional-expression holders and holder recipes - for
174: * the boolean-decomposition tails that replace them with freshly built
175: * recipes while keeping everything else (entries, alternatives, augments)
176: * intact.
177: */
178: public function withoutConditionalExpressionHolders(): self
179: {
180: $self = clone $this;
181: $self->newConditionalExpressionHolders = [];
182: $self->conditionalExpressionHolderRecipes = [];
183:
184: return $self;
185: }
186:
187: /**
188: * A copy of this with the other's alternative-form entries - for the
189: * composition tails that rebuild a SpecifiedTypes from the sure/sure-not
190: * slots and must not drop the merged alternatives.
191: */
192: public function withAlternativeTypesOf(self $other): self
193: {
194: $self = new self($this->sureTypes, $this->sureNotTypes);
195: $self->alternativeTypes = $other->alternativeTypes;
196: $self->overwrite = $this->overwrite;
197: $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
198: $self->rootExpr = $this->rootExpr;
199:
200: return $self;
201: }
202:
203: public function shouldOverwrite(): bool
204: {
205: return $this->overwrite;
206: }
207:
208: /**
209: * @return array<string, ConditionalExpressionHolder[]>
210: */
211: public function getNewConditionalExpressionHolders(): array
212: {
213: return $this->newConditionalExpressionHolders;
214: }
215:
216: public function getRootExpr(): ?Expr
217: {
218: return $this->rootExpr;
219: }
220:
221: public function removeExpr(string $exprString): self
222: {
223: $self = clone $this;
224: unset($self->sureTypes[$exprString]);
225: unset($self->sureNotTypes[$exprString]);
226: unset($self->alternativeTypes[$exprString]);
227:
228: return $self;
229: }
230:
231: /**
232: * The either-branch merge: the result holds when at least one side holds
233: * (the falsey narrowing of `&&`, the truthy narrowing of `||`). Same-kind
234: * constraints merge exactly (sure: union of values, sure-not: intersection
235: * of removed types); an expression constrained with different kinds on the
236: * two sides becomes an alternative-form entry - `(sure ?? current) minus
237: * subtract` per side, united at the application point. An expression
238: * constrained on only one side is unconstrained in the merge.
239: *
240: * @api
241: */
242: public function intersectWith(SpecifiedTypes $other): self
243: {
244: $sureTypeUnion = [];
245: $sureNotTypeUnion = [];
246: $alternativeUnion = [];
247: $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr);
248:
249: $keys = [];
250: foreach ([$this->sureTypes, $this->sureNotTypes, $this->alternativeTypes, $other->sureTypes, $other->sureNotTypes, $other->alternativeTypes] as $map) {
251: foreach ($map as $exprString => $entry) {
252: $keys[$exprString] = $entry[0];
253: }
254: }
255:
256: foreach ($keys as $exprString => $exprNode) {
257: $thisTerms = $this->collectTerms($exprString);
258: $otherTerms = $other->collectTerms($exprString);
259: if ($thisTerms === null || $otherTerms === null) {
260: // unconstrained on one side - unconstrained in the merge
261: continue;
262: }
263:
264: $terms = array_merge($thisTerms, $otherTerms);
265: $sures = [];
266: $subtracts = [];
267: $pureSure = true;
268: $pureSureNot = true;
269: foreach ($terms as [$sure, $subtract]) {
270: if ($sure === null) {
271: $pureSure = false;
272: } else {
273: $sures[] = $sure;
274: }
275: if ($subtract === null) {
276: $pureSureNot = false;
277: } else {
278: $subtracts[] = $subtract;
279: }
280: if ($sure === null || $subtract === null) {
281: continue;
282: }
283:
284: $pureSure = false;
285: $pureSureNot = false;
286: }
287:
288: if ($pureSure) {
289: $sureTypeUnion[$exprString] = [$exprNode, TypeCombinator::union(...$sures)];
290: } elseif ($pureSureNot) {
291: $merged = TypeCombinator::intersect(...$subtracts);
292: if ($merged instanceof NeverType) {
293: // removing never removes nothing - a vacuous constraint
294: continue;
295: }
296: $sureNotTypeUnion[$exprString] = [$exprNode, $merged];
297: } else {
298: $alternativeUnion[$exprString] = [$exprNode, $terms];
299: }
300: }
301:
302: $result = new self($sureTypeUnion, $sureNotTypeUnion);
303: $result->alternativeTypes = $alternativeUnion;
304: if ($this->overwrite && $other->overwrite) {
305: $result = $result->setAlwaysOverwriteTypes();
306: }
307:
308: return $result->setRootExpr($rootExpr);
309: }
310:
311: /**
312: * This side's constraint on the expression as alternative-form terms, or
313: * null when unconstrained. A sure and a sure-not on the same key are one
314: * term (the sure with the sure-not removed) - both constraints hold here.
315: *
316: * @return list<array{?Type, ?Type}>|null
317: */
318: private function collectTerms(string|int $exprString): ?array
319: {
320: if (isset($this->alternativeTypes[$exprString])) {
321: $terms = $this->alternativeTypes[$exprString][1];
322: // sure/sureNot on the same key as an alternative entry: fold them
323: // into every term (they hold in addition to the alternatives)
324: if (isset($this->sureTypes[$exprString]) || isset($this->sureNotTypes[$exprString])) {
325: $extraSure = $this->sureTypes[$exprString][1] ?? null;
326: $extraSubtract = $this->sureNotTypes[$exprString][1] ?? null;
327: $folded = [];
328: foreach ($terms as [$sure, $subtract]) {
329: if ($extraSure !== null) {
330: $sure = $sure === null ? $extraSure : TypeCombinator::intersect($sure, $extraSure);
331: }
332: if ($extraSubtract !== null) {
333: $subtract = $subtract === null ? $extraSubtract : TypeCombinator::union($subtract, $extraSubtract);
334: }
335: $folded[] = [$sure, $subtract];
336: }
337:
338: return $folded;
339: }
340:
341: return $terms;
342: }
343:
344: $sure = $this->sureTypes[$exprString][1] ?? null;
345: $subtract = $this->sureNotTypes[$exprString][1] ?? null;
346: if ($sure === null && $subtract === null) {
347: return null;
348: }
349:
350: return [[$sure, $subtract]];
351: }
352:
353: /** @api */
354: public function unionWith(SpecifiedTypes $other): self
355: {
356: $sureTypeUnion = $this->sureTypes + $other->sureTypes;
357: $sureNotTypeUnion = $this->sureNotTypes + $other->sureNotTypes;
358: $rootExpr = $this->mergeRootExpr($this->rootExpr, $other->rootExpr);
359:
360: foreach ($this->sureTypes as $exprString => [$exprNode, $type]) {
361: if (!isset($other->sureTypes[$exprString])) {
362: continue;
363: }
364:
365: $sureTypeUnion[$exprString] = [
366: $exprNode,
367: TypeCombinator::intersect($type, $other->sureTypes[$exprString][1]),
368: ];
369: }
370:
371: foreach ($this->sureNotTypes as $exprString => [$exprNode, $type]) {
372: if (!isset($other->sureNotTypes[$exprString])) {
373: continue;
374: }
375:
376: $sureNotTypeUnion[$exprString] = [
377: $exprNode,
378: TypeCombinator::union($type, $other->sureNotTypes[$exprString][1]),
379: ];
380: }
381:
382: $result = new self($sureTypeUnion, $sureNotTypeUnion);
383: $result->alternativeTypes = $this->alternativeTypes + $other->alternativeTypes;
384: if ($this->overwrite || $other->overwrite) {
385: $result = $result->setAlwaysOverwriteTypes();
386: }
387:
388: $conditionalExpressionHolders = $this->newConditionalExpressionHolders;
389: foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
390: if (!array_key_exists($exprString, $conditionalExpressionHolders)) {
391: $conditionalExpressionHolders[$exprString] = $holders;
392: } else {
393: $conditionalExpressionHolders[$exprString] = array_merge($conditionalExpressionHolders[$exprString], $holders);
394: }
395: }
396: $result->newConditionalExpressionHolders = $conditionalExpressionHolders;
397: $result->conditionalExpressionHolderRecipes = array_merge($this->conditionalExpressionHolderRecipes, $other->conditionalExpressionHolderRecipes);
398: $result->deferredAugments = array_merge($this->deferredAugments, $other->deferredAugments);
399:
400: return $result->setRootExpr($rootExpr);
401: }
402:
403: private function mergeRootExpr(?Expr $rootExprA, ?Expr $rootExprB): ?Expr
404: {
405: if ($rootExprA === $rootExprB) {
406: return $rootExprA;
407: }
408:
409: if ($rootExprA === null || $rootExprB === null) {
410: return $rootExprA ?? $rootExprB;
411: }
412:
413: return null;
414: }
415:
416: }
417: