1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use Closure;
6: use PhpParser\Node\Expr;
7: use PHPStan\Type\NeverType;
8: use PHPStan\Type\Type;
9: use PHPStan\Type\TypeCombinator;
10: use function array_key_exists;
11: use function array_merge;
12: use function count;
13:
14: final class SpecifiedTypes
15: {
16:
17: /** @var (Closure(TypeSpecifierContext, bool): self)|null */
18: private static ?Closure $emptySpecifyCallback = null;
19:
20: /**
21: * Cross-producing alternative forms doubles the term count per conjunction;
22: * past this many terms the entry is widened to a single covering term.
23: */
24: private const ALTERNATIVE_TERMS_LIMIT = 32;
25:
26: private bool $overwrite = false;
27:
28: /** @var array<string, ConditionalExpressionHolder[]> */
29: private array $newConditionalExpressionHolders = [];
30:
31: /**
32: * Deferred boolean-decomposition holders, evaluated against the applying
33: * scope by MutatingScope::applySpecifiedTypes().
34: *
35: * @var list<ConditionalExpressionHolderRecipe>
36: */
37: private array $conditionalExpressionHolderRecipes = [];
38:
39: /**
40: * State-dependent augmentations evaluated against the applying scope by
41: * MutatingScope::applySpecifiedTypes(); their entries join the applied
42: * batch.
43: *
44: * @var list<DeferredSpecifiedTypesAugment>
45: */
46: private array $deferredAugments = [];
47:
48: private ?Expr $rootExpr = null;
49:
50: /**
51: * Alternative-form entries produced by intersectWith() when the two sides
52: * constrain the same expression with different kinds (a sure type in one
53: * branch, a sure-not in the other). Each term (sure, subtract) reads as
54: * `(sure ?? current type) minus subtract`; the entry's value is the union
55: * of its terms, evaluated by MutatingScope::applySpecifiedTypes() against
56: * the subject's type at the application point - the deferred form of what
57: * the old SpecifiedTypes::normalize() computed eagerly with a scope.
58: *
59: * @var array<string, array{Expr, list<array{?Type, ?Type}>}>
60: */
61: private array $alternativeTypes = [];
62:
63: /**
64: * @api
65: * @param array<string, array{Expr, Type}> $sureTypes
66: * @param array<string, array{Expr, Type}> $sureNotTypes
67: */
68: public function __construct(
69: private array $sureTypes = [],
70: private array $sureNotTypes = [],
71: )
72: {
73: }
74:
75: /**
76: * A shared no-narrowing specify callback for results whose expression never
77: * narrows anything (literals, virtual write nodes) - one process-wide
78: * closure instead of one allocation per created ExpressionResult.
79: *
80: * @return Closure(TypeSpecifierContext, bool): self
81: */
82: public static function emptySpecifyCallback(): Closure
83: {
84: return self::$emptySpecifyCallback ??= static fn (): self => new self();
85: }
86:
87: /**
88: * Normally, $sureTypes in truthy context are used to intersect with the pre-existing type.
89: * And $sureNotTypes are used to remove type from the pre-existing type.
90: *
91: * Example: By default, non-empty-string intersected with '' (ConstantStringType) will lead to NeverType.
92: * Because it's not possible to narrow non-empty-string to an empty string.
93: *
94: * In rare cases, a type-specifying extension might want to overwrite the pre-existing types
95: * without taking the pre-existing types into consideration.
96: *
97: * In that case it should also call setAlwaysOverwriteTypes() on
98: * the returned object.
99: *
100: * ! Only do this if you're certain. Otherwise, this is a source of common bugs. !
101: *
102: * @api
103: */
104: public function setAlwaysOverwriteTypes(): self
105: {
106: $self = clone $this;
107: $self->overwrite = true;
108:
109: return $self;
110: }
111:
112: /**
113: * @api
114: */
115: public function setRootExpr(?Expr $rootExpr): self
116: {
117: $self = clone $this;
118: $self->rootExpr = $rootExpr;
119:
120: return $self;
121: }
122:
123: /**
124: * @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
125: */
126: public function setNewConditionalExpressionHolders(array $newConditionalExpressionHolders): self
127: {
128: $self = clone $this;
129: $self->newConditionalExpressionHolders = $newConditionalExpressionHolders;
130:
131: return $self;
132: }
133:
134: /**
135: * @param list<ConditionalExpressionHolderRecipe> $recipes
136: */
137: public function setConditionalExpressionHolderRecipes(array $recipes): self
138: {
139: $self = clone $this;
140: $self->conditionalExpressionHolderRecipes = $recipes;
141:
142: return $self;
143: }
144:
145: /**
146: * @return list<ConditionalExpressionHolderRecipe>
147: */
148: public function getConditionalExpressionHolderRecipes(): array
149: {
150: return $this->conditionalExpressionHolderRecipes;
151: }
152:
153: public function withDeferredAugment(DeferredSpecifiedTypesAugment $augment): self
154: {
155: $self = clone $this;
156: $self->deferredAugments = [...$this->deferredAugments, $augment];
157:
158: return $self;
159: }
160:
161: /**
162: * @return list<DeferredSpecifiedTypesAugment>
163: */
164: public function getDeferredAugments(): array
165: {
166: return $this->deferredAugments;
167: }
168:
169: /**
170: * @api
171: * @return array<string, array{Expr, Type}>
172: */
173: public function getSureTypes(): array
174: {
175: return $this->sureTypes;
176: }
177:
178: /**
179: * @api
180: * @return array<string, array{Expr, Type}>
181: */
182: public function getSureNotTypes(): array
183: {
184: return $this->sureNotTypes;
185: }
186:
187: /**
188: * @return array<string, array{Expr, list<array{?Type, ?Type}>}>
189: */
190: public function getAlternativeTypes(): array
191: {
192: return $this->alternativeTypes;
193: }
194:
195: /**
196: * A copy without conditional-expression holders and holder recipes - for
197: * the boolean-decomposition tails that replace them with freshly built
198: * recipes while keeping everything else (entries, alternatives, augments)
199: * intact.
200: */
201: public function withoutConditionalExpressionHolders(): self
202: {
203: $self = clone $this;
204: $self->newConditionalExpressionHolders = [];
205: $self->conditionalExpressionHolderRecipes = [];
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 = $this->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: * @param list<array{?Type, ?Type}> $terms
423: * @return list<array{?Type, ?Type}>
424: */
425: private static function dedupeTerms(array $terms): array
426: {
427: $deduped = [];
428: foreach ($terms as [$sure, $subtract]) {
429: foreach ($deduped as [$seenSure, $seenSubtract]) {
430: if (($sure === null) !== ($seenSure === null)) {
431: continue;
432: }
433: if (($subtract === null) !== ($seenSubtract === null)) {
434: continue;
435: }
436: if ($sure !== null && $seenSure !== null && !$sure->equals($seenSure)) {
437: continue;
438: }
439: if ($subtract !== null && $seenSubtract !== null && !$subtract->equals($seenSubtract)) {
440: continue;
441: }
442:
443: continue 2;
444: }
445:
446: $deduped[] = [$sure, $subtract];
447: }
448:
449: return $deduped;
450: }
451:
452: /**
453: * A single term covering the union of all of them - the safety net that
454: * stops a chain of conjoined alternative forms from growing its
455: * cross-product without bound. Widening a narrowing only loses precision.
456: *
457: * @param non-empty-list<array{?Type, ?Type}> $terms
458: * @return array{?Type, ?Type}
459: */
460: private static function widenTerms(array $terms): array
461: {
462: $sures = [];
463: $subtracts = [];
464: foreach ($terms as [$sure, $subtract]) {
465: if ($sure === null) {
466: // null reads as the subject's type at the application point,
467: // which every term is narrowed to anyway
468: $sures = null;
469: } elseif ($sures !== null) {
470: $sures[] = $sure;
471: }
472:
473: if ($subtract === null) {
474: $subtracts = null;
475: } elseif ($subtracts !== null) {
476: $subtracts[] = $subtract;
477: }
478: }
479:
480: return [
481: $sures === null ? null : TypeCombinator::union(...$sures),
482: $subtracts === null ? null : TypeCombinator::intersect(...$subtracts),
483: ];
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 = $this->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: $result = new self($sureTypeUnion, $sureNotTypeUnion);
516: $alternativeUnion = $this->alternativeTypes;
517: foreach ($other->alternativeTypes as $exprString => [$exprNode, $otherTerms]) {
518: if (!isset($alternativeUnion[$exprString])) {
519: $alternativeUnion[$exprString] = [$exprNode, $otherTerms];
520: continue;
521: }
522:
523: $alternativeUnion[$exprString] = [
524: $alternativeUnion[$exprString][0],
525: self::conjoinTerms($alternativeUnion[$exprString][1], $otherTerms),
526: ];
527: }
528:
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: private function mergeRootExpr(?Expr $rootExprA, ?Expr $rootExprB): ?Expr
550: {
551: if ($rootExprA === $rootExprB) {
552: return $rootExprA;
553: }
554:
555: if ($rootExprA === null || $rootExprB === null) {
556: return $rootExprA ?? $rootExprB;
557: }
558:
559: return null;
560: }
561:
562: }
563: