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