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: use function count;
12:
13: final class SpecifiedTypes
14: {
15:
16: /**
17: * Cross-producing alternative forms doubles the term count per conjunction;
18: * past this many terms the entry is widened to a single covering term.
19: */
20: private const ALTERNATIVE_TERMS_LIMIT = 32;
21:
22: private bool $overwrite = false;
23:
24: /** @var array<string, ConditionalExpressionHolder[]> */
25: private array $newConditionalExpressionHolders = [];
26:
27: /**
28: * Deferred boolean-decomposition holders, evaluated against the applying
29: * scope by MutatingScope::applySpecifiedTypes().
30: *
31: * @var list<ConditionalExpressionHolderRecipe>
32: */
33: private array $conditionalExpressionHolderRecipes = [];
34:
35: /**
36: * State-dependent augmentations evaluated against the applying scope by
37: * MutatingScope::applySpecifiedTypes(); their entries join the applied
38: * batch.
39: *
40: * @var list<DeferredSpecifiedTypesAugment>
41: */
42: private array $deferredAugments = [];
43:
44: private ?Expr $rootExpr = null;
45:
46: /**
47: * Alternative-form entries produced by intersectWith() when the two sides
48: * constrain the same expression with different kinds (a sure type in one
49: * branch, a sure-not in the other). Each term (sure, subtract) reads as
50: * `(sure ?? current type) minus subtract`; the entry's value is the union
51: * of its terms, evaluated by MutatingScope::applySpecifiedTypes() against
52: * the subject's type at the application point - the deferred form of what
53: * the old SpecifiedTypes::normalize() computed eagerly with a scope.
54: *
55: * @var array<string, array{Expr, list<array{?Type, ?Type}>}>
56: */
57: private array $alternativeTypes = [];
58:
59: /**
60: * @api
61: * @param array<string, array{Expr, Type}> $sureTypes
62: * @param array<string, array{Expr, Type}> $sureNotTypes
63: */
64: public function __construct(
65: private array $sureTypes = [],
66: private array $sureNotTypes = [],
67: )
68: {
69: }
70:
71: /**
72: * Normally, $sureTypes in truthy context are used to intersect with the pre-existing type.
73: * And $sureNotTypes are used to remove type from the pre-existing type.
74: *
75: * Example: By default, non-empty-string intersected with '' (ConstantStringType) will lead to NeverType.
76: * Because it's not possible to narrow non-empty-string to an empty string.
77: *
78: * In rare cases, a type-specifying extension might want to overwrite the pre-existing types
79: * without taking the pre-existing types into consideration.
80: *
81: * In that case it should also call setAlwaysOverwriteTypes() on
82: * the returned object.
83: *
84: * ! Only do this if you're certain. Otherwise, this is a source of common bugs. !
85: *
86: * @api
87: */
88: public function setAlwaysOverwriteTypes(): self
89: {
90: $self = clone $this;
91: $self->overwrite = true;
92:
93: return $self;
94: }
95:
96: /**
97: * @api
98: */
99: public function setRootExpr(?Expr $rootExpr): self
100: {
101: $self = clone $this;
102: $self->rootExpr = $rootExpr;
103:
104: return $self;
105: }
106:
107: /**
108: * @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
109: */
110: public function setNewConditionalExpressionHolders(array $newConditionalExpressionHolders): self
111: {
112: $self = clone $this;
113: $self->newConditionalExpressionHolders = $newConditionalExpressionHolders;
114:
115: return $self;
116: }
117:
118: /**
119: * @param list<ConditionalExpressionHolderRecipe> $recipes
120: */
121: public function setConditionalExpressionHolderRecipes(array $recipes): self
122: {
123: $self = clone $this;
124: $self->conditionalExpressionHolderRecipes = $recipes;
125:
126: return $self;
127: }
128:
129: /**
130: * @return list<ConditionalExpressionHolderRecipe>
131: */
132: public function getConditionalExpressionHolderRecipes(): array
133: {
134: return $this->conditionalExpressionHolderRecipes;
135: }
136:
137: public function withDeferredAugment(DeferredSpecifiedTypesAugment $augment): self
138: {
139: $self = clone $this;
140: $self->deferredAugments = [...$this->deferredAugments, $augment];
141:
142: return $self;
143: }
144:
145: /**
146: * @return list<DeferredSpecifiedTypesAugment>
147: */
148: public function getDeferredAugments(): array
149: {
150: return $this->deferredAugments;
151: }
152:
153: /**
154: * @api
155: * @return array<string, array{Expr, Type}>
156: */
157: public function getSureTypes(): array
158: {
159: return $this->sureTypes;
160: }
161:
162: /**
163: * @api
164: * @return array<string, array{Expr, Type}>
165: */
166: public function getSureNotTypes(): array
167: {
168: return $this->sureNotTypes;
169: }
170:
171: /**
172: * @return array<string, array{Expr, list<array{?Type, ?Type}>}>
173: */
174: public function getAlternativeTypes(): array
175: {
176: return $this->alternativeTypes;
177: }
178:
179: /**
180: * A copy without conditional-expression holders and holder recipes - for
181: * the boolean-decomposition tails that replace them with freshly built
182: * recipes while keeping everything else (entries, alternatives, augments)
183: * intact.
184: */
185: public function withoutConditionalExpressionHolders(): self
186: {
187: $self = clone $this;
188: $self->newConditionalExpressionHolders = [];
189: $self->conditionalExpressionHolderRecipes = [];
190:
191: return $self;
192: }
193:
194: /**
195: * A copy of this with the other's alternative-form entries - for the
196: * composition tails that rebuild a SpecifiedTypes from the sure/sure-not
197: * slots and must not drop the merged alternatives.
198: */
199: public function withAlternativeTypesOf(self $other): self
200: {
201: $self = new self($this->sureTypes, $this->sureNotTypes);
202: $self->alternativeTypes = $other->alternativeTypes;
203: $self->overwrite = $this->overwrite;
204: $self->newConditionalExpressionHolders = $this->newConditionalExpressionHolders;
205: $self->rootExpr = $this->rootExpr;
206:
207: return $self;
208: }
209:
210: public function shouldOverwrite(): bool
211: {
212: return $this->overwrite;
213: }
214:
215: /**
216: * @return array<string, ConditionalExpressionHolder[]>
217: */
218: public function getNewConditionalExpressionHolders(): array
219: {
220: return $this->newConditionalExpressionHolders;
221: }
222:
223: public function getRootExpr(): ?Expr
224: {
225: return $this->rootExpr;
226: }
227:
228: public function removeExpr(string $exprString): self
229: {
230: $self = clone $this;
231: unset($self->sureTypes[$exprString]);
232: unset($self->sureNotTypes[$exprString]);
233: unset($self->alternativeTypes[$exprString]);
234:
235: return $self;
236: }
237:
238: /**
239: * The either-branch merge: the result holds when at least one side holds
240: * (the falsey narrowing of `&&`, the truthy narrowing of `||`). Same-kind
241: * constraints merge exactly (sure: union of values, sure-not: intersection
242: * of removed types); an expression constrained with different kinds on the
243: * two sides becomes an alternative-form entry - `(sure ?? current) minus
244: * subtract` per side, united at the application point. An expression
245: * constrained on only one side is unconstrained in the merge.
246: *
247: * @api
248: */
249: public function intersectWith(SpecifiedTypes $other): self
250: {
251: $sureTypeUnion = [];
252: $sureNotTypeUnion = [];
253: $alternativeUnion = [];
254: $rootExpr = self::mergeRootExpr($this->rootExpr, $other->rootExpr);
255:
256: $keys = [];
257: foreach ([$this->sureTypes, $this->sureNotTypes, $this->alternativeTypes, $other->sureTypes, $other->sureNotTypes, $other->alternativeTypes] as $map) {
258: foreach ($map as $exprString => $entry) {
259: $keys[$exprString] = $entry[0];
260: }
261: }
262:
263: foreach ($keys as $exprString => $exprNode) {
264: $thisTerms = $this->collectTerms($exprString);
265: $otherTerms = $other->collectTerms($exprString);
266: if ($thisTerms === null || $otherTerms === null) {
267: // unconstrained on one side - unconstrained in the merge
268: continue;
269: }
270:
271: $terms = array_merge($thisTerms, $otherTerms);
272: $sures = [];
273: $subtracts = [];
274: $pureSure = true;
275: $pureSureNot = true;
276: foreach ($terms as [$sure, $subtract]) {
277: if ($sure === null) {
278: $pureSure = false;
279: } else {
280: $sures[] = $sure;
281: }
282: if ($subtract === null) {
283: $pureSureNot = false;
284: } else {
285: $subtracts[] = $subtract;
286: }
287: if ($sure === null || $subtract === null) {
288: continue;
289: }
290:
291: $pureSure = false;
292: $pureSureNot = false;
293: }
294:
295: if ($pureSure) {
296: $sureTypeUnion[$exprString] = [$exprNode, TypeCombinator::union(...$sures)];
297: } elseif ($pureSureNot) {
298: $merged = TypeCombinator::intersect(...$subtracts);
299: if ($merged instanceof NeverType) {
300: // removing never removes nothing - a vacuous constraint
301: continue;
302: }
303: $sureNotTypeUnion[$exprString] = [$exprNode, $merged];
304: } else {
305: $alternativeUnion[$exprString] = [$exprNode, $terms];
306: }
307: }
308:
309: $result = new self($sureTypeUnion, $sureNotTypeUnion);
310: $result->alternativeTypes = $alternativeUnion;
311: if ($this->overwrite && $other->overwrite) {
312: $result = $result->setAlwaysOverwriteTypes();
313: }
314:
315: return $result->setRootExpr($rootExpr);
316: }
317:
318: /**
319: * This side's constraint on the expression as alternative-form terms, or
320: * null when unconstrained. A sure and a sure-not on the same key are one
321: * term (the sure with the sure-not removed) - both constraints hold here.
322: *
323: * @return list<array{?Type, ?Type}>|null
324: */
325: private function collectTerms(string|int $exprString): ?array
326: {
327: if (isset($this->alternativeTypes[$exprString])) {
328: $terms = $this->alternativeTypes[$exprString][1];
329: // sure/sureNot on the same key as an alternative entry: fold them
330: // into every term (they hold in addition to the alternatives)
331: if (isset($this->sureTypes[$exprString]) || isset($this->sureNotTypes[$exprString])) {
332: $extraSure = $this->sureTypes[$exprString][1] ?? null;
333: $extraSubtract = $this->sureNotTypes[$exprString][1] ?? null;
334: $folded = [];
335: foreach ($terms as [$sure, $subtract]) {
336: if ($extraSure !== null) {
337: $sure = $sure === null ? $extraSure : TypeCombinator::intersect($sure, $extraSure);
338: }
339: if ($extraSubtract !== null) {
340: $subtract = $subtract === null ? $extraSubtract : TypeCombinator::union($subtract, $extraSubtract);
341: }
342: $folded[] = [$sure, $subtract];
343: }
344:
345: return $folded;
346: }
347:
348: return $terms;
349: }
350:
351: $sure = $this->sureTypes[$exprString][1] ?? null;
352: $subtract = $this->sureNotTypes[$exprString][1] ?? null;
353: if ($sure === null && $subtract === null) {
354: return null;
355: }
356:
357: return [[$sure, $subtract]];
358: }
359:
360: /**
361: * The both-sides-hold merge of two alternative forms. An entry's value is
362: * the union of its terms, so conjoining two entries distributes over both
363: * lists: every pair of terms contributes `(sureA and sureB) minus (subtractA
364: * or subtractB)`, the same folding collectTerms() does for a sure/sure-not
365: * pair. Pairs whose sure types cannot hold together drop out.
366: *
367: * @param list<array{?Type, ?Type}> $terms
368: * @param list<array{?Type, ?Type}> $otherTerms
369: * @return list<array{?Type, ?Type}>
370: */
371: private static function conjoinTerms(array $terms, array $otherTerms): array
372: {
373: $conjoined = [];
374: foreach ($terms as [$sure, $subtract]) {
375: foreach ($otherTerms as [$otherSure, $otherSubtract]) {
376: if ($sure === null) {
377: $mergedSure = $otherSure;
378: } elseif ($otherSure === null) {
379: $mergedSure = $sure;
380: } else {
381: $mergedSure = TypeCombinator::intersect($sure, $otherSure);
382: }
383:
384: if ($subtract === null) {
385: $mergedSubtract = $otherSubtract;
386: } elseif ($otherSubtract === null) {
387: $mergedSubtract = $subtract;
388: } else {
389: $mergedSubtract = TypeCombinator::union($subtract, $otherSubtract);
390: }
391:
392: if ($mergedSure !== null) {
393: if ($mergedSubtract !== null) {
394: // a fixed base with a subtraction is just the narrower base -
395: // folding it keeps the term list free of redundant pairs
396: $mergedSure = TypeCombinator::remove($mergedSure, $mergedSubtract);
397: $mergedSubtract = null;
398: }
399: if ($mergedSure instanceof NeverType) {
400: continue;
401: }
402: }
403:
404: $conjoined[] = [$mergedSure, $mergedSubtract];
405: }
406: }
407:
408: if ($conjoined === []) {
409: // every pair was impossible - so is the conjunction
410: return [[new NeverType(), null]];
411: }
412:
413: $conjoined = self::dedupeTerms($conjoined);
414: if (count($conjoined) > self::ALTERNATIVE_TERMS_LIMIT) {
415: return [self::widenTerms($conjoined)];
416: }
417:
418: return $conjoined;
419: }
420:
421: /**
422: * A single term covering the union of all of them - the safety net that
423: * stops a chain of conjoined alternative forms from growing its
424: * cross-product without bound. Widening a narrowing only loses precision.
425: *
426: * @param non-empty-list<array{?Type, ?Type}> $terms
427: * @return array{?Type, ?Type}
428: */
429: private static function widenTerms(array $terms): array
430: {
431: $sures = [];
432: $subtracts = [];
433: foreach ($terms as [$sure, $subtract]) {
434: if ($sure === null) {
435: // null reads as the subject's type at the application point,
436: // which every term is narrowed to anyway
437: $sures = null;
438: } elseif ($sures !== null) {
439: $sures[] = $sure;
440: }
441:
442: if ($subtract === null) {
443: $subtracts = null;
444: } elseif ($subtracts !== null) {
445: $subtracts[] = $subtract;
446: }
447: }
448:
449: return [
450: $sures === null ? null : TypeCombinator::union(...$sures),
451: $subtracts === null ? null : TypeCombinator::intersect(...$subtracts),
452: ];
453: }
454:
455: /**
456: * @param list<array{?Type, ?Type}> $terms
457: * @return list<array{?Type, ?Type}>
458: */
459: private static function dedupeTerms(array $terms): array
460: {
461: $deduped = [];
462: foreach ($terms as [$sure, $subtract]) {
463: foreach ($deduped as [$seenSure, $seenSubtract]) {
464: if (($sure === null) !== ($seenSure === null)) {
465: continue;
466: }
467: if (($subtract === null) !== ($seenSubtract === null)) {
468: continue;
469: }
470: if ($sure !== null && $seenSure !== null && !$sure->equals($seenSure)) {
471: continue;
472: }
473: if ($subtract !== null && $seenSubtract !== null && !$subtract->equals($seenSubtract)) {
474: continue;
475: }
476:
477: continue 2;
478: }
479:
480: $deduped[] = [$sure, $subtract];
481: }
482:
483: return $deduped;
484: }
485:
486: /** @api */
487: public function unionWith(SpecifiedTypes $other): self
488: {
489: $sureTypeUnion = $this->sureTypes + $other->sureTypes;
490: $sureNotTypeUnion = $this->sureNotTypes + $other->sureNotTypes;
491: $rootExpr = self::mergeRootExpr($this->rootExpr, $other->rootExpr);
492:
493: foreach ($this->sureTypes as $exprString => [$exprNode, $type]) {
494: if (!isset($other->sureTypes[$exprString])) {
495: continue;
496: }
497:
498: $sureTypeUnion[$exprString] = [
499: $exprNode,
500: TypeCombinator::intersect($type, $other->sureTypes[$exprString][1]),
501: ];
502: }
503:
504: foreach ($this->sureNotTypes as $exprString => [$exprNode, $type]) {
505: if (!isset($other->sureNotTypes[$exprString])) {
506: continue;
507: }
508:
509: $sureNotTypeUnion[$exprString] = [
510: $exprNode,
511: TypeCombinator::union($type, $other->sureNotTypes[$exprString][1]),
512: ];
513: }
514:
515: $alternativeUnion = $this->alternativeTypes;
516: foreach ($other->alternativeTypes as $exprString => [$exprNode, $otherTerms]) {
517: if (!isset($alternativeUnion[$exprString])) {
518: $alternativeUnion[$exprString] = [$exprNode, $otherTerms];
519: continue;
520: }
521:
522: $alternativeUnion[$exprString] = [
523: $alternativeUnion[$exprString][0],
524: self::conjoinTerms($alternativeUnion[$exprString][1], $otherTerms),
525: ];
526: }
527:
528: $result = new self($sureTypeUnion, $sureNotTypeUnion);
529: $result->alternativeTypes = $alternativeUnion;
530: if ($this->overwrite || $other->overwrite) {
531: $result = $result->setAlwaysOverwriteTypes();
532: }
533:
534: $conditionalExpressionHolders = $this->newConditionalExpressionHolders;
535: foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
536: if (!array_key_exists($exprString, $conditionalExpressionHolders)) {
537: $conditionalExpressionHolders[$exprString] = $holders;
538: } else {
539: $conditionalExpressionHolders[$exprString] = array_merge($conditionalExpressionHolders[$exprString], $holders);
540: }
541: }
542: $result->newConditionalExpressionHolders = $conditionalExpressionHolders;
543: $result->conditionalExpressionHolderRecipes = array_merge($this->conditionalExpressionHolderRecipes, $other->conditionalExpressionHolderRecipes);
544: $result->deferredAugments = array_merge($this->deferredAugments, $other->deferredAugments);
545:
546: return $result->setRootExpr($rootExpr);
547: }
548:
549: /**
550: * The n-ary both-sides-hold merge - the truthy narrowing of a flattened
551: * `&&` chain, the falsey narrowing of a flattened `||` chain. Same result
552: * as folding unionWith() over the list, but each expression's constraints
553: * are combined in one pass instead of being rebuilt per arm, which is what
554: * lets the flattened chain paths stay linear in the number of arms.
555: *
556: * @param list<self> $typesList
557: */
558: public static function unionAll(array $typesList): self
559: {
560: /** @var array<string, array{Expr, list<Type>}> $surePerExpr */
561: $surePerExpr = [];
562: /** @var array<string, array{Expr, list<Type>}> $sureNotPerExpr */
563: $sureNotPerExpr = [];
564: /** @var array<string, array{Expr, list<array{?Type, ?Type}>}> $alternatives */
565: $alternatives = [];
566: $overwrite = false;
567: $rootExpr = null;
568: $conditionalExpressionHolders = [];
569: $recipes = [];
570: $augments = [];
571:
572: foreach ($typesList as $types) {
573: foreach ($types->sureTypes as $exprString => [$exprNode, $type]) {
574: $surePerExpr[$exprString][0] = $exprNode;
575: $surePerExpr[$exprString][1][] = $type;
576: }
577: foreach ($types->sureNotTypes as $exprString => [$exprNode, $type]) {
578: $sureNotPerExpr[$exprString][0] = $exprNode;
579: $sureNotPerExpr[$exprString][1][] = $type;
580: }
581: foreach ($types->alternativeTypes as $exprString => [$exprNode, $terms]) {
582: if (!isset($alternatives[$exprString])) {
583: $alternatives[$exprString] = [$exprNode, $terms];
584: continue;
585: }
586:
587: $alternatives[$exprString][1] = self::conjoinTerms($alternatives[$exprString][1], $terms);
588: }
589:
590: $overwrite = $overwrite || $types->overwrite;
591: $rootExpr = self::mergeRootExpr($rootExpr, $types->rootExpr);
592:
593: foreach ($types->newConditionalExpressionHolders as $exprString => $holders) {
594: if (!array_key_exists($exprString, $conditionalExpressionHolders)) {
595: $conditionalExpressionHolders[$exprString] = $holders;
596: } else {
597: $conditionalExpressionHolders[$exprString] = array_merge($conditionalExpressionHolders[$exprString], $holders);
598: }
599: }
600: $recipes = array_merge($recipes, $types->conditionalExpressionHolderRecipes);
601: $augments = array_merge($augments, $types->deferredAugments);
602: }
603:
604: $sureTypes = [];
605: foreach ($surePerExpr as $exprString => [$exprNode, $types]) {
606: $sureTypes[$exprString] = [$exprNode, TypeCombinator::intersect(...$types)];
607: }
608: $sureNotTypes = [];
609: foreach ($sureNotPerExpr as $exprString => [$exprNode, $types]) {
610: $sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
611: }
612:
613: $result = new self($sureTypes, $sureNotTypes);
614: $result->alternativeTypes = $alternatives;
615: if ($overwrite) {
616: $result = $result->setAlwaysOverwriteTypes();
617: }
618: $result->newConditionalExpressionHolders = $conditionalExpressionHolders;
619: $result->conditionalExpressionHolderRecipes = $recipes;
620: $result->deferredAugments = $augments;
621:
622: return $result->setRootExpr($rootExpr);
623: }
624:
625: private static function mergeRootExpr(?Expr $rootExprA, ?Expr $rootExprB): ?Expr
626: {
627: if ($rootExprA === $rootExprB) {
628: return $rootExprA;
629: }
630:
631: if ($rootExprA === null || $rootExprB === null) {
632: return $rootExprA ?? $rootExprB;
633: }
634:
635: return null;
636: }
637:
638: }
639: