1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use Countable;
6: use PhpParser\Node;
7: use PhpParser\Node\Expr;
8: use PhpParser\Node\Expr\ArrayDimFetch;
9: use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
10: use PhpParser\Node\Expr\BinaryOp\BooleanOr;
11: use PhpParser\Node\Expr\BinaryOp\LogicalAnd;
12: use PhpParser\Node\Expr\BinaryOp\LogicalOr;
13: use PhpParser\Node\Expr\ClassConstFetch;
14: use PhpParser\Node\Expr\ConstFetch;
15: use PhpParser\Node\Expr\FuncCall;
16: use PhpParser\Node\Expr\Instanceof_;
17: use PhpParser\Node\Expr\MethodCall;
18: use PhpParser\Node\Expr\PropertyFetch;
19: use PhpParser\Node\Expr\StaticCall;
20: use PhpParser\Node\Expr\StaticPropertyFetch;
21: use PhpParser\Node\Name;
22: use PHPStan\Analyser\ExprHandler\BooleanAndHandler;
23: use PHPStan\DependencyInjection\AutowiredService;
24: use PHPStan\Node\Expr\AlwaysRememberedExpr;
25: use PHPStan\Node\Expr\TypeExpr;
26: use PHPStan\Node\IssetExpr;
27: use PHPStan\Node\Printer\ExprPrinter;
28: use PHPStan\Php\PhpVersion;
29: use PHPStan\Reflection\Assertions;
30: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
31: use PHPStan\Reflection\ExtendedParametersAcceptor;
32: use PHPStan\Reflection\ParametersAcceptor;
33: use PHPStan\Reflection\ParametersAcceptorSelector;
34: use PHPStan\Reflection\ReflectionProvider;
35: use PHPStan\Reflection\ResolvedFunctionVariant;
36: use PHPStan\Rules\Arrays\AllowedArrayKeysTypes;
37: use PHPStan\ShouldNotHappenException;
38: use PHPStan\TrinaryLogic;
39: use PHPStan\Type\Accessory\AccessoryArrayListType;
40: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
41: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
42: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
43: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
44: use PHPStan\Type\Accessory\HasOffsetType;
45: use PHPStan\Type\Accessory\HasOffsetValueType;
46: use PHPStan\Type\Accessory\HasPropertyType;
47: use PHPStan\Type\Accessory\NonEmptyArrayType;
48: use PHPStan\Type\ArrayType;
49: use PHPStan\Type\BooleanType;
50: use PHPStan\Type\ConditionalTypeForParameter;
51: use PHPStan\Type\Constant\ConstantArrayType;
52: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
53: use PHPStan\Type\Constant\ConstantBooleanType;
54: use PHPStan\Type\Constant\ConstantFloatType;
55: use PHPStan\Type\Constant\ConstantIntegerType;
56: use PHPStan\Type\Constant\ConstantStringType;
57: use PHPStan\Type\ConstantScalarType;
58: use PHPStan\Type\FloatType;
59: use PHPStan\Type\FunctionTypeSpecifyingExtension;
60: use PHPStan\Type\Generic\GenericClassStringType;
61: use PHPStan\Type\Generic\TemplateType;
62: use PHPStan\Type\Generic\TemplateTypeHelper;
63: use PHPStan\Type\Generic\TemplateTypeVariance;
64: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
65: use PHPStan\Type\IntegerRangeType;
66: use PHPStan\Type\IntegerType;
67: use PHPStan\Type\IntersectionType;
68: use PHPStan\Type\MethodTypeSpecifyingExtension;
69: use PHPStan\Type\MixedType;
70: use PHPStan\Type\NeverType;
71: use PHPStan\Type\NonexistentParentClassType;
72: use PHPStan\Type\NullType;
73: use PHPStan\Type\ObjectType;
74: use PHPStan\Type\ObjectWithoutClassType;
75: use PHPStan\Type\ResourceType;
76: use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
77: use PHPStan\Type\StaticType;
78: use PHPStan\Type\StaticTypeFactory;
79: use PHPStan\Type\StringType;
80: use PHPStan\Type\Type;
81: use PHPStan\Type\TypeCombinator;
82: use PHPStan\Type\TypeTraverser;
83: use PHPStan\Type\UnionType;
84: use function array_key_exists;
85: use function array_key_first;
86: use function array_keys;
87: use function array_last;
88: use function array_map;
89: use function array_merge;
90: use function array_reverse;
91: use function array_shift;
92: use function count;
93: use function in_array;
94: use function is_string;
95: use function strtolower;
96: use function substr;
97: use const COUNT_NORMAL;
98:
99: #[AutowiredService(name: 'typeSpecifier', factory: '@typeSpecifierFactory::create')]
100: final class TypeSpecifier
101: {
102:
103: private const MAX_ACCESSORIES_LIMIT = 8;
104:
105: private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4;
106:
107: /** @var MethodTypeSpecifyingExtension[][]|null */
108: private ?array $methodTypeSpecifyingExtensionsByClass = null;
109:
110: /** @var StaticMethodTypeSpecifyingExtension[][]|null */
111: private ?array $staticMethodTypeSpecifyingExtensionsByClass = null;
112:
113: /**
114: * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions
115: * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions
116: * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions
117: */
118: public function __construct(
119: private ExprPrinter $exprPrinter,
120: private ReflectionProvider $reflectionProvider,
121: private PhpVersion $phpVersion,
122: private array $functionTypeSpecifyingExtensions,
123: private array $methodTypeSpecifyingExtensions,
124: private array $staticMethodTypeSpecifyingExtensions,
125: private bool $rememberPossiblyImpureFunctionValues,
126: )
127: {
128: }
129:
130: /**
131: * @api
132: */
133: public function specifyTypesInCondition(
134: Scope $scope,
135: Expr $expr,
136: TypeSpecifierContext $context,
137: ): SpecifiedTypes
138: {
139: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
140: return (new SpecifiedTypes([], []))->setRootExpr($expr);
141: }
142:
143: if ($expr instanceof Instanceof_) {
144: $exprNode = $expr->expr;
145: if ($expr->class instanceof Name) {
146: $className = (string) $expr->class;
147: $lowercasedClassName = strtolower($className);
148: if ($lowercasedClassName === 'self' && $scope->isInClass()) {
149: $type = new ObjectType($scope->getClassReflection()->getName());
150: } elseif ($lowercasedClassName === 'static' && $scope->isInClass()) {
151: $type = new StaticType($scope->getClassReflection());
152: } elseif ($lowercasedClassName === 'parent') {
153: if (
154: $scope->isInClass()
155: && $scope->getClassReflection()->getParentClass() !== null
156: ) {
157: $type = new ObjectType($scope->getClassReflection()->getParentClass()->getName());
158: } else {
159: $type = new NonexistentParentClassType();
160: }
161: } else {
162: $type = new ObjectType($className);
163: }
164: return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr);
165: }
166:
167: $result = $scope->getType($expr->class)->toObjectTypeForInstanceofCheck();
168: $type = $result->type;
169: $uncertainty = $result->uncertainty;
170:
171: if (!$type->isSuperTypeOf(new MixedType())->yes()) {
172: if ($context->true()) {
173: $type = TypeCombinator::intersect(
174: $type,
175: new ObjectWithoutClassType(),
176: );
177: return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr);
178: } elseif ($context->false() && !$uncertainty) {
179: $exprType = $scope->getType($expr->expr);
180: if (!$type->isSuperTypeOf($exprType)->yes()) {
181: return $this->create($exprNode, $type, $context, $scope)->setRootExpr($expr);
182: }
183: }
184: }
185: if ($context->true()) {
186: return $this->create($exprNode, new ObjectWithoutClassType(), $context, $scope)->setRootExpr($exprNode);
187: }
188: } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) {
189: return $this->resolveIdentical($expr, $scope, $context);
190:
191: } elseif ($expr instanceof Node\Expr\BinaryOp\NotIdentical) {
192: return $this->specifyTypesInCondition(
193: $scope,
194: new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Identical($expr->left, $expr->right)),
195: $context,
196: )->setRootExpr($expr);
197: } elseif ($expr instanceof Expr\Cast\Bool_) {
198: return $this->specifyTypesInCondition(
199: $scope,
200: new Node\Expr\BinaryOp\Equal($expr->expr, new ConstFetch(new Name\FullyQualified('true'))),
201: $context,
202: )->setRootExpr($expr);
203: } elseif ($expr instanceof Expr\Cast\String_) {
204: return $this->specifyTypesInCondition(
205: $scope,
206: new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\String_('')),
207: $context,
208: )->setRootExpr($expr);
209: } elseif ($expr instanceof Expr\Cast\Int_) {
210: return $this->specifyTypesInCondition(
211: $scope,
212: new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\Int_(0)),
213: $context,
214: )->setRootExpr($expr);
215: } elseif ($expr instanceof Expr\Cast\Double) {
216: return $this->specifyTypesInCondition(
217: $scope,
218: new Node\Expr\BinaryOp\NotEqual($expr->expr, new Node\Scalar\Float_(0.0)),
219: $context,
220: )->setRootExpr($expr);
221: } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) {
222: return $this->resolveEqual($expr, $scope, $context);
223: } elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) {
224: return $this->specifyTypesInCondition(
225: $scope,
226: new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Equal($expr->left, $expr->right)),
227: $context,
228: )->setRootExpr($expr);
229:
230: } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) {
231:
232: if (
233: $expr->left instanceof FuncCall
234: && $expr->left->name instanceof Name
235: && !$expr->left->isFirstClassCallable()
236: && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true)
237: && count($expr->left->getArgs()) >= 1
238: && (
239: !$expr->right instanceof FuncCall
240: || !$expr->right->name instanceof Name
241: || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true)
242: )
243: ) {
244: $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller
245: ? new Node\Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left)
246: : new Node\Expr\BinaryOp\Smaller($expr->right, $expr->left);
247:
248: return $this->specifyTypesInCondition(
249: $scope,
250: new Node\Expr\BooleanNot($inverseOperator),
251: $context,
252: )->setRootExpr($expr);
253: }
254:
255: $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual;
256: $offset = $orEqual ? 0 : 1;
257: $leftType = $scope->getType($expr->left);
258: $result = (new SpecifiedTypes([], []))->setRootExpr($expr);
259:
260: if (
261: !$context->null()
262: && $expr->right instanceof FuncCall
263: && $expr->right->name instanceof Name
264: && !$expr->right->isFirstClassCallable()
265: && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true)
266: && count($expr->right->getArgs()) >= 1
267: && $leftType->isInteger()->yes()
268: ) {
269: $argType = $scope->getType($expr->right->getArgs()[0]->value);
270:
271: $sizeType = null;
272: if ($leftType instanceof ConstantIntegerType) {
273: if ($orEqual) {
274: $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
275: } else {
276: $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
277: }
278: } elseif ($leftType instanceof IntegerRangeType) {
279: if ($context->falsey() && $leftType->getMax() !== null) {
280: if ($orEqual) {
281: $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax());
282: } else {
283: $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax());
284: }
285: } elseif ($context->truthy() && $leftType->getMin() !== null) {
286: if ($orEqual) {
287: $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin());
288: } else {
289: $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin());
290: }
291: }
292: } else {
293: $sizeType = $leftType;
294: }
295:
296: if ($sizeType !== null) {
297: $specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr);
298: if ($specifiedTypes !== null) {
299: $result = $result->unionWith($specifiedTypes);
300: }
301: }
302:
303: if (
304: $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes())
305: || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes())
306: ) {
307: if ($context->truthy() && $argType->isArray()->maybe()) {
308: $countables = [];
309: if ($argType instanceof UnionType) {
310: $countableInterface = new ObjectType(Countable::class);
311: foreach ($argType->getTypes() as $innerType) {
312: if ($innerType->isArray()->yes()) {
313: $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType);
314: $countables[] = $innerType;
315: }
316:
317: if (!$countableInterface->isSuperTypeOf($innerType)->yes()) {
318: continue;
319: }
320:
321: $countables[] = $innerType;
322: }
323: }
324:
325: if (count($countables) > 0) {
326: $countableType = TypeCombinator::union(...$countables);
327:
328: return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr);
329: }
330: }
331:
332: if ($argType->isArray()->yes()) {
333: $newType = new NonEmptyArrayType();
334: if ($context->true() && $argType->isList()->yes()) {
335: $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType());
336: }
337:
338: $result = $result->unionWith(
339: $this->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr),
340: );
341: }
342: }
343:
344: // infer $list[$index] after $index < count($list)
345: if (
346: $context->true()
347: && !$orEqual
348: // constant offsets are handled via HasOffsetType/HasOffsetValueType
349: && !$leftType instanceof ConstantIntegerType
350: && $argType->isList()->yes()
351: && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()
352: ) {
353: $arrayArg = $expr->right->getArgs()[0]->value;
354: $dimFetch = new ArrayDimFetch($arrayArg, $expr->left);
355: $result = $result->unionWith(
356: $this->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr),
357: );
358: }
359: }
360:
361: // infer $list[$index] after $zeroOrMore < count($list) - N
362: // infer $list[$index] after $zeroOrMore <= count($list) - N
363: if (
364: $context->true()
365: && $expr->right instanceof Expr\BinaryOp\Minus
366: && $expr->right->left instanceof FuncCall
367: && $expr->right->left->name instanceof Name
368: && !$expr->right->left->isFirstClassCallable()
369: && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true)
370: && count($expr->right->left->getArgs()) >= 1
371: // constant offsets are handled via HasOffsetType/HasOffsetValueType
372: && !$leftType instanceof ConstantIntegerType
373: && $leftType->isInteger()->yes()
374: && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()
375: ) {
376: $countArgType = $scope->getType($expr->right->left->getArgs()[0]->value);
377: $subtractedType = $scope->getType($expr->right->right);
378: if (
379: $countArgType->isList()->yes()
380: && $this->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes()
381: && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes()
382: ) {
383: $arrayArg = $expr->right->left->getArgs()[0]->value;
384: $dimFetch = new ArrayDimFetch($arrayArg, $expr->left);
385: $result = $result->unionWith(
386: $this->create($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr),
387: );
388: }
389: }
390:
391: if (
392: !$context->null()
393: && $expr->right instanceof FuncCall
394: && $expr->right->name instanceof Name
395: && !$expr->right->isFirstClassCallable()
396: && in_array(strtolower((string) $expr->right->name), ['preg_match'], true)
397: && count($expr->right->getArgs()) >= 3
398: && (
399: IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes()
400: || ($expr instanceof Expr\BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes())
401: )
402: ) {
403: // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match
404: $newExpr = new Expr\BinaryOp\Identical($expr->right, new Node\Scalar\Int_(1));
405:
406: return $this->specifyTypesInCondition($scope, $newExpr, $context)->setRootExpr($expr);
407: }
408:
409: if (
410: !$context->null()
411: && $expr->right instanceof FuncCall
412: && $expr->right->name instanceof Name
413: && !$expr->right->isFirstClassCallable()
414: && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true)
415: && count($expr->right->getArgs()) === 1
416: && $leftType->isInteger()->yes()
417: ) {
418: if (
419: $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes())
420: || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes())
421: ) {
422: $argType = $scope->getType($expr->right->getArgs()[0]->value);
423: if ($argType->isString()->yes()) {
424: $accessory = new AccessoryNonEmptyStringType();
425:
426: if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) {
427: $accessory = new AccessoryNonFalsyStringType();
428: }
429:
430: $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr));
431: }
432: }
433: }
434:
435: if ($leftType instanceof ConstantIntegerType) {
436: if ($expr->right instanceof Expr\PostInc) {
437: $result = $result->unionWith($this->createRangeTypes(
438: $expr,
439: $expr->right->var,
440: IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1),
441: $context,
442: ));
443: } elseif ($expr->right instanceof Expr\PostDec) {
444: $result = $result->unionWith($this->createRangeTypes(
445: $expr,
446: $expr->right->var,
447: IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1),
448: $context,
449: ));
450: } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) {
451: $result = $result->unionWith($this->createRangeTypes(
452: $expr,
453: $expr->right->var,
454: IntegerRangeType::fromInterval($leftType->getValue(), null, $offset),
455: $context,
456: ));
457: }
458: }
459:
460: $rightType = $scope->getType($expr->right);
461: if ($rightType instanceof ConstantIntegerType) {
462: if ($expr->left instanceof Expr\PostInc) {
463: $result = $result->unionWith($this->createRangeTypes(
464: $expr,
465: $expr->left->var,
466: IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1),
467: $context,
468: ));
469: } elseif ($expr->left instanceof Expr\PostDec) {
470: $result = $result->unionWith($this->createRangeTypes(
471: $expr,
472: $expr->left->var,
473: IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1),
474: $context,
475: ));
476: } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) {
477: $result = $result->unionWith($this->createRangeTypes(
478: $expr,
479: $expr->left->var,
480: IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset),
481: $context,
482: ));
483: }
484: }
485:
486: if ($context->true()) {
487: if (!$expr->left instanceof Node\Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Node\Scalar)) {
488: $result = $result->unionWith(
489: $this->create(
490: $expr->left,
491: $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion),
492: TypeSpecifierContext::createTruthy(),
493: $scope,
494: )->setRootExpr($expr),
495: );
496: }
497: if (!$expr->right instanceof Node\Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Node\Scalar)) {
498: $result = $result->unionWith(
499: $this->create(
500: $expr->right,
501: $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion),
502: TypeSpecifierContext::createTruthy(),
503: $scope,
504: )->setRootExpr($expr),
505: );
506: }
507: } elseif ($context->false()) {
508: if (!$expr->left instanceof Node\Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Node\Scalar)) {
509: $result = $result->unionWith(
510: $this->create(
511: $expr->left,
512: $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion),
513: TypeSpecifierContext::createTruthy(),
514: $scope,
515: )->setRootExpr($expr),
516: );
517: }
518: if (!$expr->right instanceof Node\Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Node\Scalar)) {
519: $result = $result->unionWith(
520: $this->create(
521: $expr->right,
522: $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion),
523: TypeSpecifierContext::createTruthy(),
524: $scope,
525: )->setRootExpr($expr),
526: );
527: }
528: }
529:
530: return $result;
531:
532: } elseif ($expr instanceof Node\Expr\BinaryOp\Greater) {
533: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr);
534:
535: } elseif ($expr instanceof Node\Expr\BinaryOp\GreaterOrEqual) {
536: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr);
537:
538: } elseif ($expr instanceof FuncCall && $expr->name instanceof Name) {
539: if ($this->reflectionProvider->hasFunction($expr->name, $scope)) {
540: // lazy create parametersAcceptor, as creation can be expensive
541: $parametersAcceptor = null;
542:
543: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
544: $normalizedExpr = $expr;
545: $args = $expr->getArgs();
546: if (count($args) > 0) {
547: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants());
548: $normalizedExpr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
549: }
550:
551: foreach ($this->getFunctionTypeSpecifyingExtensions() as $extension) {
552: if (!$extension->isFunctionSupported($functionReflection, $normalizedExpr, $context)) {
553: continue;
554: }
555:
556: return $extension->specifyTypes($functionReflection, $normalizedExpr, $scope, $context);
557: }
558:
559: if (count($args) > 0) {
560: $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope);
561: if ($specifiedTypes !== null) {
562: return $specifiedTypes;
563: }
564: }
565:
566: $assertions = $functionReflection->getAsserts();
567: if ($assertions->getAll() !== []) {
568: $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants());
569:
570: $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
571: $type,
572: $parametersAcceptor->getResolvedTemplateTypeMap(),
573: $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
574: TemplateTypeVariance::createInvariant(),
575: ));
576: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
577: if ($specifiedTypes !== null) {
578: return $specifiedTypes;
579: }
580: }
581: }
582:
583: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
584: } elseif ($expr instanceof FuncCall) {
585: $specifiedTypes = $this->specifyTypesFromCallableCall($context, $expr, $scope);
586: if ($specifiedTypes !== null) {
587: return $specifiedTypes;
588: }
589:
590: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
591: } elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) {
592: $methodCalledOnType = $scope->getType($expr->var);
593: $methodReflection = $scope->getMethodReflection($methodCalledOnType, $expr->name->name);
594: if ($methodReflection !== null) {
595: // lazy create parametersAcceptor, as creation can be expensive
596: $parametersAcceptor = null;
597:
598: $normalizedExpr = $expr;
599: $args = $expr->getArgs();
600: if (count($args) > 0) {
601: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants());
602: $normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr;
603: }
604:
605: $referencedClasses = $methodCalledOnType->getObjectClassNames();
606: if (
607: count($referencedClasses) === 1
608: && $this->reflectionProvider->hasClass($referencedClasses[0])
609: ) {
610: $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]);
611: foreach ($this->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) {
612: if (!$extension->isMethodSupported($methodReflection, $normalizedExpr, $context)) {
613: continue;
614: }
615:
616: return $extension->specifyTypes($methodReflection, $normalizedExpr, $scope, $context);
617: }
618: }
619:
620: if (count($args) > 0) {
621: $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope);
622: if ($specifiedTypes !== null) {
623: return $specifiedTypes;
624: }
625: }
626:
627: $assertions = $methodReflection->getAsserts();
628: if ($assertions->getAll() !== []) {
629: $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants());
630:
631: $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
632: $type,
633: $parametersAcceptor->getResolvedTemplateTypeMap(),
634: $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
635: TemplateTypeVariance::createInvariant(),
636: ));
637: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
638: if ($specifiedTypes !== null) {
639: return $specifiedTypes;
640: }
641: }
642: }
643:
644: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
645: } elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) {
646: if ($expr->class instanceof Name) {
647: $calleeType = $scope->resolveTypeByName($expr->class);
648: } else {
649: $calleeType = $scope->getType($expr->class);
650: }
651:
652: $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name);
653: if ($staticMethodReflection !== null) {
654: // lazy create parametersAcceptor, as creation can be expensive
655: $parametersAcceptor = null;
656:
657: $normalizedExpr = $expr;
658: $args = $expr->getArgs();
659: if (count($args) > 0) {
660: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants());
661: $normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
662: }
663:
664: $referencedClasses = $calleeType->getObjectClassNames();
665: if (
666: count($referencedClasses) === 1
667: && $this->reflectionProvider->hasClass($referencedClasses[0])
668: ) {
669: $staticMethodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]);
670: foreach ($this->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $extension) {
671: if (!$extension->isStaticMethodSupported($staticMethodReflection, $normalizedExpr, $context)) {
672: continue;
673: }
674:
675: return $extension->specifyTypes($staticMethodReflection, $normalizedExpr, $scope, $context);
676: }
677: }
678:
679: if (count($args) > 0) {
680: $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope);
681: if ($specifiedTypes !== null) {
682: return $specifiedTypes;
683: }
684: }
685:
686: $assertions = $staticMethodReflection->getAsserts();
687: if ($assertions->getAll() !== []) {
688: $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants());
689:
690: $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
691: $type,
692: $parametersAcceptor->getResolvedTemplateTypeMap(),
693: $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
694: TemplateTypeVariance::createInvariant(),
695: ));
696: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
697: if ($specifiedTypes !== null) {
698: return $specifiedTypes;
699: }
700: }
701: }
702:
703: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
704: } elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) {
705: if (!$scope instanceof MutatingScope) {
706: throw new ShouldNotHappenException();
707: }
708:
709: // For deep BooleanAnd chains in truthy context, flatten and
710: // process all arms at once to avoid O(N²) recursive
711: // filterByTruthyValue calls.
712: if (
713: $context->true()
714: && BooleanAndHandler::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH
715: ) {
716: return $this->specifyTypesForFlattenedBooleanAnd($scope, $expr, $context);
717: }
718:
719: $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
720: $rightScope = $scope->filterByTruthyValue($expr->left);
721: $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
722: if ($context->true()) {
723: $types = $leftTypes->unionWith($rightTypes);
724: } else {
725: $leftNormalized = $leftTypes->normalize($scope);
726: $rightNormalized = $rightTypes->normalize($rightScope);
727: $types = $leftNormalized->intersectWith($rightNormalized);
728: $types = $this->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types);
729: }
730: if ($context->false()) {
731: $leftTypesForHolders = $leftTypes;
732: $rightTypesForHolders = $rightTypes;
733: if ($context->truthy()) {
734: if ($leftTypesForHolders->getSureTypes() === [] && $leftTypesForHolders->getSureNotTypes() === []) {
735: $leftTypesForHolders = $this->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createFalsey())->setRootExpr($expr);
736: }
737: if ($rightTypesForHolders->getSureTypes() === [] && $rightTypesForHolders->getSureNotTypes() === []) {
738: $rightTypesForHolders = $this->specifyTypesInCondition($rightScope, $expr->right, TypeSpecifierContext::createFalsey())->setRootExpr($expr);
739: }
740: }
741: $result = new SpecifiedTypes(
742: $types->getSureTypes(),
743: $types->getSureNotTypes(),
744: );
745: if ($types->shouldOverwrite()) {
746: $result = $result->setAlwaysOverwriteTypes();
747: }
748: return $result->setNewConditionalExpressionHolders(array_merge(
749: $this->processBooleanNotSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope),
750: $this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope),
751: $this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope),
752: $this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope),
753: ))->setRootExpr($expr);
754: }
755:
756: return $types;
757: } elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) {
758: if (!$scope instanceof MutatingScope) {
759: throw new ShouldNotHappenException();
760: }
761:
762: // For deep BooleanOr chains, flatten and process all arms at once
763: // to avoid O(n^2) recursive filterByFalseyValue calls
764: if (BooleanAndHandler::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) {
765: return $this->specifyTypesForFlattenedBooleanOr($scope, $expr, $context);
766: }
767:
768: $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
769: $rightScope = $scope->filterByFalseyValue($expr->left);
770: $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
771:
772: if ($context->true()) {
773: if (
774: $scope->getType($expr->left)->toBoolean()->isFalse()->yes()
775: ) {
776: $types = $rightTypes->normalize($rightScope);
777: } elseif (
778: $scope->getType($expr->left)->toBoolean()->isTrue()->yes()
779: || $scope->getType($expr->right)->toBoolean()->isFalse()->yes()
780: ) {
781: $types = $leftTypes->normalize($scope);
782: } else {
783: $leftNormalized = $leftTypes->normalize($scope);
784: $rightNormalized = $rightTypes->normalize($rightScope);
785: $types = $leftNormalized->intersectWith($rightNormalized);
786: $types = $this->augmentBooleanOrTruthyWithConditionalHolders($scope, $rightScope, $expr, $types);
787: $types = $this->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types);
788: }
789: } else {
790: $types = $leftTypes->unionWith($rightTypes);
791: }
792:
793: if ($context->true()) {
794: $result = new SpecifiedTypes(
795: $types->getSureTypes(),
796: $types->getSureNotTypes(),
797: );
798: if ($types->shouldOverwrite()) {
799: $result = $result->setAlwaysOverwriteTypes();
800: }
801: return $result->setNewConditionalExpressionHolders(array_merge(
802: $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes, $rightScope),
803: $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes, $scope),
804: $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes, $rightScope),
805: $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes, $scope),
806: ))->setRootExpr($expr);
807: }
808:
809: return $types;
810: } elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) {
811: return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate())->setRootExpr($expr);
812: } elseif ($expr instanceof Node\Expr\Assign) {
813: if (!$scope instanceof MutatingScope) {
814: throw new ShouldNotHappenException();
815: }
816:
817: if ($context->null()) {
818: $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
819: $specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var));
820: } else {
821: $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);
822: }
823:
824: // infer $arr[$key] after $key = array_key_first/last($arr)
825: if (
826: $expr->expr instanceof FuncCall
827: && $expr->expr->name instanceof Name
828: && !$expr->expr->isFirstClassCallable()
829: && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
830: && count($expr->expr->getArgs()) >= 1
831: ) {
832: $arrayArg = $expr->expr->getArgs()[0]->value;
833: $arrayType = $scope->getType($arrayArg);
834:
835: if ($arrayType->isArray()->yes()) {
836: if ($context->true()) {
837: $specifiedTypes = $specifiedTypes->unionWith(
838: $this->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
839: );
840: $isNonEmpty = true;
841: } else {
842: $isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes();
843: }
844:
845: if ($isNonEmpty) {
846: $dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
847: $specifiedTypes = $specifiedTypes->unionWith(
848: $this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
849: );
850: } elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
851: $keyType = $scope->getType($expr->expr);
852: $nonNullKeyType = TypeCombinator::removeNull($keyType);
853: if (!$nonNullKeyType instanceof NeverType) {
854: $specifiedTypes = $specifiedTypes->unionWith(
855: $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()),
856: );
857: }
858: }
859: }
860: }
861:
862: // infer $arr[$key] after $key = array_search($needle, $arr) or $key = array_find_key($arr, $callback)
863: if (
864: $expr->expr instanceof FuncCall
865: && $expr->expr->name instanceof Name
866: && !$expr->expr->isFirstClassCallable()
867: && count($expr->expr->getArgs()) >= 2
868: ) {
869: $funcName = $expr->expr->name->toLowerString();
870: $arrayArg = null;
871: $sentinelType = null;
872: $isStrictArraySearch = false;
873:
874: if ($funcName === 'array_search') {
875: $arrayArg = $expr->expr->getArgs()[1]->value;
876: $sentinelType = new ConstantBooleanType(false);
877: $isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $scope->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes();
878: } elseif ($funcName === 'array_find_key') {
879: $arrayArg = $expr->expr->getArgs()[0]->value;
880: $sentinelType = new NullType();
881: }
882:
883: if ($arrayArg !== null) {
884: $arrayType = $scope->getType($arrayArg);
885:
886: if ($arrayType->isArray()->yes()) {
887: if ($context->true()) {
888: $specifiedTypes = $specifiedTypes->unionWith(
889: $this->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
890: );
891:
892: $dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
893:
894: if ($isStrictArraySearch) {
895: $needleType = $scope->getType($expr->expr->getArgs()[0]->value);
896: $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType());
897: } else {
898: $dimFetchType = $arrayType->getIterableValueType();
899: }
900:
901: $specifiedTypes = $specifiedTypes->unionWith(
902: $this->create($dimFetch, $dimFetchType, TypeSpecifierContext::createTrue(), $scope),
903: );
904: } elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
905: $keyType = $scope->getType($expr->expr);
906: $narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType);
907: if (!$narrowedKeyType instanceof NeverType) {
908: if ($isStrictArraySearch) {
909: $needleType = $scope->getType($expr->expr->getArgs()[0]->value);
910: $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType());
911: } else {
912: $dimFetchType = $arrayType->getIterableValueType();
913: }
914: $specifiedTypes = $specifiedTypes->unionWith(
915: $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType),
916: );
917: }
918: }
919: }
920: }
921: }
922:
923: if ($context->null()) {
924: // infer $arr[$key] after $key = array_rand($arr)
925: if (
926: $expr->expr instanceof FuncCall
927: && $expr->expr->name instanceof Name
928: && !$expr->expr->isFirstClassCallable()
929: && in_array($expr->expr->name->toLowerString(), ['array_rand'], true)
930: && count($expr->expr->getArgs()) >= 1
931: ) {
932: $numArg = null;
933: $args = $expr->expr->getArgs();
934: $arrayArg = $args[0]->value;
935: if (count($args) > 1) {
936: $numArg = $args[1]->value;
937: }
938: $one = new ConstantIntegerType(1);
939: $arrayType = $scope->getType($arrayArg);
940:
941: if (
942: $arrayType->isArray()->yes()
943: && $arrayType->isIterableAtLeastOnce()->yes()
944: && ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes())
945: ) {
946: $dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
947:
948: return $specifiedTypes->unionWith(
949: $this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
950: );
951: }
952: }
953:
954: // infer $list[$count] after $count = count($list) - 1
955: if (
956: $expr->expr instanceof Expr\BinaryOp\Minus
957: && $expr->expr->left instanceof FuncCall
958: && $expr->expr->left->name instanceof Name
959: && !$expr->expr->left->isFirstClassCallable()
960: && $expr->expr->right instanceof Node\Scalar\Int_
961: && $expr->expr->right->value === 1
962: && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true)
963: && count($expr->expr->left->getArgs()) >= 1
964: ) {
965: $arrayArg = $expr->expr->left->getArgs()[0]->value;
966: $arrayType = $scope->getType($arrayArg);
967: if (
968: $arrayType->isList()->yes()
969: && $arrayType->isIterableAtLeastOnce()->yes()
970: ) {
971: $dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
972:
973: return $specifiedTypes->unionWith(
974: $this->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
975: );
976: }
977: }
978:
979: return $specifiedTypes;
980: }
981:
982: return $specifiedTypes;
983: } elseif (
984: $expr instanceof Expr\Isset_
985: && count($expr->vars) > 0
986: && !$context->null()
987: ) {
988: // rewrite multi param isset() to and-chained single param isset()
989: if (count($expr->vars) > 1) {
990: $issets = [];
991: foreach ($expr->vars as $var) {
992: $issets[] = new Expr\Isset_([$var], $expr->getAttributes());
993: }
994:
995: $first = array_shift($issets);
996: $andChain = null;
997: foreach ($issets as $isset) {
998: if ($andChain === null) {
999: $andChain = new BooleanAnd($first, $isset);
1000: continue;
1001: }
1002:
1003: $andChain = new BooleanAnd($andChain, $isset);
1004: }
1005:
1006: if ($andChain === null) {
1007: throw new ShouldNotHappenException();
1008: }
1009:
1010: return $this->specifyTypesInCondition($scope, $andChain, $context)->setRootExpr($expr);
1011: }
1012:
1013: $issetExpr = $expr->vars[0];
1014:
1015: if (!$context->true()) {
1016: if (!$scope instanceof MutatingScope) {
1017: throw new ShouldNotHappenException();
1018: }
1019:
1020: $isset = $scope->issetCheck($issetExpr, static fn () => true);
1021:
1022: if ($isset === false) {
1023: return new SpecifiedTypes();
1024: }
1025:
1026: $type = $scope->getType($issetExpr);
1027: $isNullable = !$type->isNull()->no();
1028: $exprType = $this->create(
1029: $issetExpr,
1030: new NullType(),
1031: $context->negate(),
1032: $scope,
1033: )->setRootExpr($expr);
1034:
1035: if ($issetExpr instanceof Expr\Variable && is_string($issetExpr->name)) {
1036: if ($isset === true) {
1037: if ($isNullable) {
1038: return $exprType;
1039: }
1040:
1041: // variable cannot exist in !isset()
1042: return $exprType->unionWith($this->create(
1043: new IssetExpr($issetExpr),
1044: new NullType(),
1045: $context,
1046: $scope,
1047: ))->setRootExpr($expr);
1048: }
1049:
1050: if ($isNullable) {
1051: // reduces variable certainty to maybe
1052: return $exprType->unionWith($this->create(
1053: new IssetExpr($issetExpr),
1054: new NullType(),
1055: $context->negate(),
1056: $scope,
1057: ))->setRootExpr($expr);
1058: }
1059:
1060: // variable cannot exist in !isset()
1061: return $this->create(
1062: new IssetExpr($issetExpr),
1063: new NullType(),
1064: $context,
1065: $scope,
1066: )->setRootExpr($expr);
1067: }
1068:
1069: if ($isNullable && $isset === true) {
1070: return $exprType;
1071: }
1072:
1073: if (
1074: $issetExpr instanceof ArrayDimFetch
1075: && $issetExpr->dim !== null
1076: ) {
1077: $varType = $scope->getType($issetExpr->var);
1078: if (!$varType instanceof MixedType) {
1079: $dimType = $scope->getType($issetExpr->dim);
1080:
1081: if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
1082: $constantArrays = $varType->getConstantArrays();
1083: $typesToRemove = [];
1084: foreach ($constantArrays as $constantArray) {
1085: $hasOffset = $constantArray->hasOffsetValueType($dimType);
1086: if (!$hasOffset->yes() || !$constantArray->getOffsetValueType($dimType)->isNull()->no()) {
1087: continue;
1088: }
1089:
1090: $typesToRemove[] = $constantArray;
1091: }
1092:
1093: if ($typesToRemove !== []) {
1094: $typeToRemove = TypeCombinator::union(...$typesToRemove);
1095:
1096: $result = $this->create(
1097: $issetExpr->var,
1098: $typeToRemove,
1099: TypeSpecifierContext::createFalse(),
1100: $scope,
1101: )->setRootExpr($expr);
1102:
1103: if ($scope->hasExpressionType($issetExpr->var)->maybe()) {
1104: $result = $result->unionWith(
1105: $this->create(
1106: new IssetExpr($issetExpr->var),
1107: new NullType(),
1108: TypeSpecifierContext::createTruthy(),
1109: $scope,
1110: )->setRootExpr($expr),
1111: );
1112: }
1113:
1114: return $result;
1115: }
1116: }
1117: }
1118: }
1119:
1120: return new SpecifiedTypes();
1121: }
1122:
1123: $tmpVars = [$issetExpr];
1124: while (
1125: $issetExpr instanceof ArrayDimFetch
1126: || $issetExpr instanceof PropertyFetch
1127: || (
1128: $issetExpr instanceof StaticPropertyFetch
1129: && $issetExpr->class instanceof Expr
1130: )
1131: ) {
1132: if ($issetExpr instanceof StaticPropertyFetch) {
1133: /** @var Expr $issetExpr */
1134: $issetExpr = $issetExpr->class;
1135: } else {
1136: $issetExpr = $issetExpr->var;
1137: }
1138: $tmpVars[] = $issetExpr;
1139: }
1140: $vars = array_reverse($tmpVars);
1141:
1142: $types = new SpecifiedTypes();
1143: foreach ($vars as $var) {
1144:
1145: if ($var instanceof Expr\Variable && is_string($var->name)) {
1146: if ($scope->hasVariableType($var->name)->no()) {
1147: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1148: }
1149: }
1150:
1151: if (
1152: $var instanceof ArrayDimFetch
1153: && $var->dim !== null
1154: && !$scope->getType($var->var) instanceof MixedType
1155: ) {
1156: $dimType = $scope->getType($var->dim);
1157:
1158: if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
1159: $types = $types->unionWith(
1160: $this->create(
1161: $var->var,
1162: new HasOffsetType($dimType),
1163: $context,
1164: $scope,
1165: )->setRootExpr($expr),
1166: );
1167: } else {
1168: $varType = $scope->getType($var->var);
1169:
1170: $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType);
1171: if ($narrowedKey !== null) {
1172: $types = $types->unionWith(
1173: $this->create(
1174: $var->dim,
1175: $narrowedKey,
1176: $context,
1177: $scope,
1178: )->setRootExpr($expr),
1179: );
1180: }
1181:
1182: if ($varType->isArray()->yes()) {
1183: $types = $types->unionWith(
1184: $this->create(
1185: $var->var,
1186: new NonEmptyArrayType(),
1187: $context,
1188: $scope,
1189: )->setRootExpr($expr),
1190: );
1191: }
1192: }
1193: }
1194:
1195: if (
1196: $var instanceof PropertyFetch
1197: && $var->name instanceof Node\Identifier
1198: ) {
1199: $types = $types->unionWith(
1200: $this->create($var->var, new IntersectionType([
1201: new ObjectWithoutClassType(),
1202: new HasPropertyType($var->name->toString()),
1203: ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr),
1204: );
1205: } elseif (
1206: $var instanceof StaticPropertyFetch
1207: && $var->class instanceof Expr
1208: && $var->name instanceof Node\VarLikeIdentifier
1209: ) {
1210: $types = $types->unionWith(
1211: $this->create($var->class, new IntersectionType([
1212: new ObjectWithoutClassType(),
1213: new HasPropertyType($var->name->toString()),
1214: ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr),
1215: );
1216: }
1217:
1218: $types = $types->unionWith(
1219: $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr),
1220: );
1221: }
1222:
1223: return $types;
1224: } elseif (
1225: $expr instanceof Expr\BinaryOp\Coalesce
1226: && !$context->null()
1227: ) {
1228: if (!$context->true()) {
1229: if (!$scope instanceof MutatingScope) {
1230: throw new ShouldNotHappenException();
1231: }
1232:
1233: $isset = $scope->issetCheck($expr->left, static fn () => true);
1234:
1235: if ($isset !== true) {
1236: return new SpecifiedTypes();
1237: }
1238:
1239: return $this->create(
1240: $expr->left,
1241: new NullType(),
1242: $context->negate(),
1243: $scope,
1244: )->setRootExpr($expr);
1245: }
1246:
1247: if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) {
1248: return $this->create(
1249: $expr->left,
1250: new NullType(),
1251: TypeSpecifierContext::createFalse(),
1252: $scope,
1253: )->setRootExpr($expr);
1254: }
1255:
1256: } elseif (
1257: $expr instanceof Expr\Empty_
1258: ) {
1259: if (!$scope instanceof MutatingScope) {
1260: throw new ShouldNotHappenException();
1261: }
1262:
1263: $isset = $scope->issetCheck($expr->expr, static fn () => true);
1264: if ($isset === false) {
1265: return new SpecifiedTypes();
1266: }
1267:
1268: return $this->specifyTypesInCondition($scope, new BooleanOr(
1269: new Expr\BooleanNot(new Expr\Isset_([$expr->expr])),
1270: new Expr\BooleanNot($expr->expr),
1271: ), $context)->setRootExpr($expr);
1272: } elseif ($expr instanceof Expr\ErrorSuppress) {
1273: return $this->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr);
1274: } elseif (
1275: $expr instanceof Expr\Ternary
1276: && !$expr->cond instanceof Expr\Ternary
1277: && !$context->null()
1278: ) {
1279: if ($expr->if !== null) {
1280: $conditionExpr = new BooleanOr(
1281: new BooleanAnd($expr->cond, $expr->if),
1282: new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else),
1283: );
1284: } else {
1285: $conditionExpr = new BooleanOr(
1286: $expr->cond,
1287: new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else),
1288: );
1289: }
1290:
1291: return $this->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr);
1292:
1293: } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) {
1294: $types = $this->specifyTypesInCondition(
1295: $scope,
1296: new BooleanAnd(
1297: new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
1298: new PropertyFetch($expr->var, $expr->name),
1299: ),
1300: $context,
1301: )->setRootExpr($expr);
1302:
1303: $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
1304: return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope));
1305: } elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) {
1306: $types = $this->specifyTypesInCondition(
1307: $scope,
1308: new BooleanAnd(
1309: new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
1310: new MethodCall($expr->var, $expr->name, $expr->args),
1311: ),
1312: $context,
1313: )->setRootExpr($expr);
1314:
1315: $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
1316: return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope));
1317: } elseif (
1318: $expr instanceof Expr\New_
1319: && $expr->class instanceof Name
1320: && $this->reflectionProvider->hasClass($expr->class->toString())
1321: ) {
1322: $classReflection = $this->reflectionProvider->getClass($expr->class->toString());
1323:
1324: if ($classReflection->hasConstructor()) {
1325: $methodReflection = $classReflection->getConstructor();
1326: $asserts = $methodReflection->getAsserts();
1327:
1328: if ($asserts->getAll() !== []) {
1329: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants());
1330:
1331: $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
1332: $type,
1333: $parametersAcceptor->getResolvedTemplateTypeMap(),
1334: $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
1335: TemplateTypeVariance::createInvariant(),
1336: ));
1337:
1338: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
1339:
1340: if ($specifiedTypes !== null) {
1341: return $specifiedTypes;
1342: }
1343: }
1344: }
1345: } elseif (!$context->null()) {
1346: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
1347: }
1348:
1349: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1350: }
1351:
1352: private function isNormalCountCall(FuncCall $countFuncCall, Type $typeToCount, Scope $scope): TrinaryLogic
1353: {
1354: if (count($countFuncCall->getArgs()) === 1) {
1355: return TrinaryLogic::createYes();
1356: }
1357:
1358: $mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1359: return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($typeToCount->getIterableValueType()->isArray()->negate());
1360: }
1361:
1362: private function specifyTypesForCountFuncCall(
1363: FuncCall $countFuncCall,
1364: Type $type,
1365: Type $sizeType,
1366: TypeSpecifierContext $context,
1367: Scope $scope,
1368: Expr $rootExpr,
1369: ): ?SpecifiedTypes
1370: {
1371: $isConstantArray = $type->isConstantArray();
1372: $isList = $type->isList();
1373: $oneOrMore = IntegerRangeType::fromInterval(1, null);
1374: if (
1375: !$this->isNormalCountCall($countFuncCall, $type, $scope)->yes()
1376: || (!$isConstantArray->yes() && !$isList->yes())
1377: || !$oneOrMore->isSuperTypeOf($sizeType)->yes()
1378: || $sizeType->isSuperTypeOf($type->getArraySize())->yes()
1379: ) {
1380: return null;
1381: }
1382:
1383: if ($context->falsey() && $isConstantArray->yes()) {
1384: $remainingSize = TypeCombinator::remove($type->getArraySize(), $sizeType);
1385: if (!$remainingSize instanceof NeverType) {
1386: $negatedContext = $context->false()
1387: ? TypeSpecifierContext::createTrue()
1388: : TypeSpecifierContext::createTruthy();
1389: $result = $this->specifyTypesForCountFuncCall(
1390: $countFuncCall,
1391: $type,
1392: $remainingSize,
1393: $negatedContext,
1394: $scope,
1395: $rootExpr,
1396: );
1397: if ($result !== null) {
1398: return $result;
1399: }
1400: }
1401:
1402: // Fallback: directly filter constant arrays by their exact sizes.
1403: // This avoids using TypeCombinator::remove() with falsey context,
1404: // which can incorrectly remove arrays whose count doesn't match
1405: // but whose shape is a subtype of the matched array.
1406: $keptTypes = [];
1407: foreach ($type->getConstantArrays() as $arrayType) {
1408: if ($sizeType->isSuperTypeOf($arrayType->getArraySize())->yes()) {
1409: continue;
1410: }
1411:
1412: $keptTypes[] = $arrayType;
1413: }
1414: if ($keptTypes !== []) {
1415: return $this->create(
1416: $countFuncCall->getArgs()[0]->value,
1417: TypeCombinator::union(...$keptTypes),
1418: $context->negate(),
1419: $scope,
1420: )->setRootExpr($rootExpr);
1421: }
1422: }
1423:
1424: $resultTypes = [];
1425: foreach ($type->getArrays() as $arrayType) {
1426: $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize());
1427: if ($isSizeSuperTypeOfArraySize->no()) {
1428: continue;
1429: }
1430:
1431: if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) {
1432: continue;
1433: }
1434:
1435: if (
1436: $sizeType instanceof ConstantIntegerType
1437: && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1438: && $isList->yes()
1439: && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes()
1440: ) {
1441: // turn optional offsets non-optional
1442: $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1443: for ($i = 0; $i < $sizeType->getValue(); $i++) {
1444: $offsetType = new ConstantIntegerType($i);
1445: $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType));
1446: }
1447: $resultTypes[] = $valueTypesBuilder->getArray();
1448: continue;
1449: }
1450:
1451: if (
1452: $sizeType instanceof IntegerRangeType
1453: && $sizeType->getMin() !== null
1454: && $sizeType->getMin() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1455: && $isList->yes()
1456: && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($sizeType->getMax() ?? $sizeType->getMin()) - 1))->yes()
1457: ) {
1458: $builderData = [];
1459: // turn optional offsets non-optional
1460: for ($i = 0; $i < $sizeType->getMin(); $i++) {
1461: $offsetType = new ConstantIntegerType($i);
1462: $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), false];
1463: }
1464: if ($sizeType->getMax() !== null) {
1465: if ($sizeType->getMax() - $sizeType->getMin() > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1466: $resultTypes[] = $arrayType;
1467: continue;
1468: }
1469: for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
1470: $offsetType = new ConstantIntegerType($i);
1471: $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), true];
1472: }
1473: } elseif ($arrayType->isConstantArray()->yes()) {
1474: for ($i = $sizeType->getMin();; $i++) {
1475: $offsetType = new ConstantIntegerType($i);
1476: $hasOffset = $arrayType->hasOffsetValueType($offsetType);
1477: if ($hasOffset->no()) {
1478: break;
1479: }
1480: $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()];
1481: }
1482: } else {
1483: $intersection = [];
1484: $intersection[] = $arrayType;
1485: $intersection[] = new NonEmptyArrayType();
1486:
1487: $zero = new ConstantIntegerType(0);
1488: $i = 0;
1489: foreach ($builderData as [$offsetType, $valueType]) {
1490: // non-empty-list already implies the offset 0
1491: if ($zero->isSuperTypeOf($offsetType)->yes()) {
1492: continue;
1493: }
1494:
1495: if ($i > self::MAX_ACCESSORIES_LIMIT) {
1496: break;
1497: }
1498:
1499: $intersection[] = new HasOffsetValueType($offsetType, $valueType);
1500: $i++;
1501: }
1502:
1503: $resultTypes[] = TypeCombinator::intersect(...$intersection);
1504: continue;
1505: }
1506:
1507: if (count($builderData) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1508: $resultTypes[] = $arrayType;
1509: continue;
1510: }
1511:
1512: $builder = ConstantArrayTypeBuilder::createEmpty();
1513: foreach ($builderData as [$offsetType, $valueType, $optional]) {
1514: $builder->setOffsetValueType($offsetType, $valueType, $optional);
1515: }
1516:
1517: $builtArray = $builder->getArray();
1518: if ($isList->yes() && !$builder->isList()) {
1519: $constantArrays = $builtArray->getConstantArrays();
1520: if (count($constantArrays) === 1) {
1521: $builtArray = $constantArrays[0]->makeList();
1522: }
1523: }
1524: $resultTypes[] = $builtArray;
1525: continue;
1526: }
1527:
1528: $resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
1529: }
1530:
1531: if ($context->truthy() && $isConstantArray->yes() && $isList->yes()) {
1532: $hasOptionalKeys = false;
1533: foreach ($type->getConstantArrays() as $arrayType) {
1534: if ($arrayType->getOptionalKeys() !== []) {
1535: $hasOptionalKeys = true;
1536: break;
1537: }
1538: }
1539:
1540: if (!$hasOptionalKeys) {
1541: $argExpr = $countFuncCall->getArgs()[0]->value;
1542: $argExprString = $this->exprPrinter->printExpr($argExpr);
1543:
1544: $sizeMin = null;
1545: $sizeMax = null;
1546: if ($sizeType instanceof ConstantIntegerType) {
1547: $sizeMin = $sizeType->getValue();
1548: $sizeMax = $sizeType->getValue();
1549: } elseif ($sizeType instanceof IntegerRangeType) {
1550: $sizeMin = $sizeType->getMin();
1551: $sizeMax = $sizeType->getMax();
1552: }
1553:
1554: $sureTypes = [];
1555: $sureNotTypes = [];
1556:
1557: if ($sizeMin !== null && $sizeMin >= 1) {
1558: $sureTypes[$argExprString] = [$argExpr, new HasOffsetValueType(new ConstantIntegerType($sizeMin - 1), new MixedType())];
1559: }
1560: if ($sizeMax !== null) {
1561: $sureNotTypes[$argExprString] = [$argExpr, new HasOffsetValueType(new ConstantIntegerType($sizeMax), new MixedType())];
1562: }
1563:
1564: if ($sureTypes !== [] || $sureNotTypes !== []) {
1565: return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($rootExpr);
1566: }
1567: }
1568: }
1569:
1570: return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr);
1571: }
1572:
1573: private function specifyTypesForConstantBinaryExpression(
1574: Expr $exprNode,
1575: Type $constantType,
1576: TypeSpecifierContext $context,
1577: Scope $scope,
1578: Expr $rootExpr,
1579: ): ?SpecifiedTypes
1580: {
1581: if (!$context->null() && $constantType->isFalse()->yes()) {
1582: $types = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr);
1583: if (!$context->true() && ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch)) {
1584: return $types;
1585: }
1586:
1587: return $types->unionWith($this->specifyTypesInCondition(
1588: $scope,
1589: $exprNode,
1590: $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(),
1591: )->setRootExpr($rootExpr));
1592: }
1593:
1594: if (!$context->null() && $constantType->isTrue()->yes()) {
1595: $types = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr);
1596: if (!$context->true() && ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch)) {
1597: return $types;
1598: }
1599:
1600: return $types->unionWith($this->specifyTypesInCondition(
1601: $scope,
1602: $exprNode,
1603: $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(),
1604: )->setRootExpr($rootExpr));
1605: }
1606:
1607: return null;
1608: }
1609:
1610: private function specifyTypesForConstantStringBinaryExpression(
1611: Expr $exprNode,
1612: Type $constantType,
1613: TypeSpecifierContext $context,
1614: Scope $scope,
1615: Expr $rootExpr,
1616: ): ?SpecifiedTypes
1617: {
1618: $scalarValues = $constantType->getConstantScalarValues();
1619: if (count($scalarValues) !== 1 || !is_string($scalarValues[0])) {
1620: return null;
1621: }
1622: $constantStringValue = $scalarValues[0];
1623:
1624: if (
1625: $exprNode instanceof FuncCall
1626: && $exprNode->name instanceof Name
1627: && !$exprNode->isFirstClassCallable()
1628: && strtolower($exprNode->name->toString()) === 'gettype'
1629: && isset($exprNode->getArgs()[0])
1630: ) {
1631: $type = null;
1632: if ($constantStringValue === 'string') {
1633: $type = new StringType();
1634: }
1635: if ($constantStringValue === 'array') {
1636: $type = new ArrayType(new MixedType(), new MixedType());
1637: }
1638: if ($constantStringValue === 'boolean') {
1639: $type = new BooleanType();
1640: }
1641: if (in_array($constantStringValue, ['resource', 'resource (closed)'], true)) {
1642: $type = new ResourceType();
1643: }
1644: if ($constantStringValue === 'integer') {
1645: $type = new IntegerType();
1646: }
1647: if ($constantStringValue === 'double') {
1648: $type = new FloatType();
1649: }
1650: if ($constantStringValue === 'NULL') {
1651: $type = new NullType();
1652: }
1653: if ($constantStringValue === 'object') {
1654: $type = new ObjectWithoutClassType();
1655: }
1656:
1657: if ($type !== null) {
1658: $callType = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr);
1659: $argType = $this->create($exprNode->getArgs()[0]->value, $type, $context, $scope)->setRootExpr($rootExpr);
1660: return $callType->unionWith($argType);
1661: }
1662: }
1663:
1664: if (
1665: $context->true()
1666: && $exprNode instanceof FuncCall
1667: && $exprNode->name instanceof Name
1668: && !$exprNode->isFirstClassCallable()
1669: && strtolower((string) $exprNode->name) === 'get_parent_class'
1670: && isset($exprNode->getArgs()[0])
1671: ) {
1672: $argType = $scope->getType($exprNode->getArgs()[0]->value);
1673: $objectType = new ObjectType($constantStringValue);
1674: $classStringType = new GenericClassStringType($objectType);
1675:
1676: if ($argType->isString()->yes()) {
1677: return $this->create(
1678: $exprNode->getArgs()[0]->value,
1679: $classStringType,
1680: $context,
1681: $scope,
1682: )->setRootExpr($rootExpr);
1683: }
1684:
1685: if ($argType->isObject()->yes()) {
1686: return $this->create(
1687: $exprNode->getArgs()[0]->value,
1688: $objectType,
1689: $context,
1690: $scope,
1691: )->setRootExpr($rootExpr);
1692: }
1693:
1694: return $this->create(
1695: $exprNode->getArgs()[0]->value,
1696: TypeCombinator::union($objectType, $classStringType),
1697: $context,
1698: $scope,
1699: )->setRootExpr($rootExpr);
1700: }
1701:
1702: if (
1703: $context->false()
1704: && $exprNode instanceof FuncCall
1705: && $exprNode->name instanceof Name
1706: && !$exprNode->isFirstClassCallable()
1707: && in_array(strtolower((string) $exprNode->name), [
1708: 'trim', 'ltrim', 'rtrim', 'chop',
1709: 'mb_trim', 'mb_ltrim', 'mb_rtrim',
1710: ], true)
1711: && isset($exprNode->getArgs()[0])
1712: && $constantStringValue === ''
1713: ) {
1714: $argValue = $exprNode->getArgs()[0]->value;
1715: $argType = $scope->getType($argValue);
1716: if ($argType->isString()->yes()) {
1717: return $this->create(
1718: $argValue,
1719: new IntersectionType([
1720: new StringType(),
1721: new AccessoryNonEmptyStringType(),
1722: ]),
1723: $context->negate(),
1724: $scope,
1725: )->setRootExpr($rootExpr);
1726: }
1727: }
1728:
1729: return null;
1730: }
1731:
1732: private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes
1733: {
1734: if ($context->null()) {
1735: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1736: }
1737: if (!$context->truthy()) {
1738: $type = StaticTypeFactory::truthy();
1739: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
1740: } elseif (!$context->falsey()) {
1741: $type = StaticTypeFactory::falsey();
1742: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
1743: }
1744:
1745: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1746: }
1747:
1748: private function specifyTypesFromConditionalReturnType(
1749: TypeSpecifierContext $context,
1750: Expr\CallLike $call,
1751: ParametersAcceptor $parametersAcceptor,
1752: Scope $scope,
1753: ): ?SpecifiedTypes
1754: {
1755: if (!$parametersAcceptor instanceof ResolvedFunctionVariant) {
1756: return null;
1757: }
1758:
1759: $returnType = $parametersAcceptor->getOriginalParametersAcceptor()->getReturnType();
1760: if (!$returnType instanceof ConditionalTypeForParameter) {
1761: return null;
1762: }
1763:
1764: if ($context->true()) {
1765: $leftType = new ConstantBooleanType(true);
1766: $rightType = new ConstantBooleanType(false);
1767: } elseif ($context->false()) {
1768: $leftType = new ConstantBooleanType(false);
1769: $rightType = new ConstantBooleanType(true);
1770: } elseif ($context->null()) {
1771: $leftType = new MixedType();
1772: $rightType = new NeverType();
1773: } else {
1774: return null;
1775: }
1776:
1777: $argumentExpr = null;
1778: $parameters = $parametersAcceptor->getParameters();
1779: foreach ($call->getArgs() as $i => $arg) {
1780: if ($arg->unpack) {
1781: continue;
1782: }
1783:
1784: if ($arg->name !== null) {
1785: $paramName = $arg->name->toString();
1786: } elseif (isset($parameters[$i])) {
1787: $paramName = $parameters[$i]->getName();
1788: } else {
1789: continue;
1790: }
1791:
1792: if ($returnType->getParameterName() !== '$' . $paramName) {
1793: continue;
1794: }
1795:
1796: $argumentExpr = $arg->value;
1797: }
1798:
1799: if ($argumentExpr === null) {
1800: return null;
1801: }
1802:
1803: return $this->getConditionalSpecifiedTypes($returnType, $leftType, $rightType, $scope, $argumentExpr);
1804: }
1805:
1806: private function getConditionalSpecifiedTypes(
1807: ConditionalTypeForParameter $conditionalType,
1808: Type $leftType,
1809: Type $rightType,
1810: Scope $scope,
1811: Expr $argumentExpr,
1812: ): ?SpecifiedTypes
1813: {
1814: $targetType = $conditionalType->getTarget();
1815: $ifType = $conditionalType->getIf();
1816: $elseType = $conditionalType->getElse();
1817:
1818: if (
1819: (
1820: $argumentExpr instanceof Node\Scalar
1821: || ($argumentExpr instanceof ConstFetch && in_array(strtolower($argumentExpr->name->toString()), ['true', 'false', 'null'], true))
1822: ) && ($ifType instanceof NeverType || $elseType instanceof NeverType)
1823: ) {
1824: return null;
1825: }
1826:
1827: if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) {
1828: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue();
1829: } elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) {
1830: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
1831: } else {
1832: return null;
1833: }
1834:
1835: $specifiedTypes = $this->create(
1836: $argumentExpr,
1837: $targetType,
1838: $context,
1839: $scope,
1840: );
1841:
1842: if ($targetType instanceof ConstantBooleanType) {
1843: if (!$targetType->getValue()) {
1844: $context = $context->negate();
1845: }
1846:
1847: $specifiedTypes = $specifiedTypes->unionWith($this->specifyTypesInCondition($scope, $argumentExpr, $context));
1848: }
1849:
1850: return $specifiedTypes;
1851: }
1852:
1853: private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\CallLike $call, Assertions $assertions, ParametersAcceptor $parametersAcceptor, Scope $scope): ?SpecifiedTypes
1854: {
1855: if ($context->null()) {
1856: $asserts = $assertions->getAsserts();
1857: } elseif ($context->true()) {
1858: $asserts = $assertions->getAssertsIfTrue();
1859: } elseif ($context->false()) {
1860: $asserts = $assertions->getAssertsIfFalse();
1861: } else {
1862: throw new ShouldNotHappenException();
1863: }
1864:
1865: if (count($asserts) === 0) {
1866: return null;
1867: }
1868:
1869: $argsMap = [];
1870: $parameters = $parametersAcceptor->getParameters();
1871: foreach ($call->getArgs() as $i => $arg) {
1872: if ($arg->unpack) {
1873: continue;
1874: }
1875:
1876: if ($arg->name !== null) {
1877: $paramName = $arg->name->toString();
1878: } elseif (isset($parameters[$i])) {
1879: $paramName = $parameters[$i]->getName();
1880: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
1881: $lastParameter = array_last($parameters);
1882: $paramName = $lastParameter->getName();
1883: } else {
1884: continue;
1885: }
1886:
1887: $argsMap[$paramName][] = $arg->value;
1888: }
1889: foreach ($parameters as $parameter) {
1890: $name = $parameter->getName();
1891: $defaultValue = $parameter->getDefaultValue();
1892: if (isset($argsMap[$name]) || $defaultValue === null) {
1893: continue;
1894: }
1895: $argsMap[$name][] = new TypeExpr($defaultValue);
1896: }
1897:
1898: if ($call instanceof MethodCall) {
1899: $argsMap['this'] = [$call->var];
1900: }
1901:
1902: /** @var SpecifiedTypes|null $types */
1903: $types = null;
1904:
1905: foreach ($asserts as $assert) {
1906: foreach ($argsMap[substr($assert->getParameter()->getParameterName(), 1)] ?? [] as $parameterExpr) {
1907: $assertedType = TypeTraverser::map($assert->getType(), static function (Type $type, callable $traverse) use ($argsMap, $scope): Type {
1908: if ($type instanceof ConditionalTypeForParameter) {
1909: $parameterName = substr($type->getParameterName(), 1);
1910: if (array_key_exists($parameterName, $argsMap)) {
1911: $type = $traverse($type);
1912: if ($type instanceof ConditionalTypeForParameter) {
1913: $argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[substr($type->getParameterName(), 1)]));
1914: return $type->toConditional($argType);
1915: }
1916: return $type;
1917: }
1918: }
1919:
1920: return $traverse($type);
1921: });
1922:
1923: $assertExpr = $assert->getParameter()->getExpr($parameterExpr);
1924:
1925: $templateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap();
1926: $containsUnresolvedTemplate = false;
1927: TypeTraverser::map(
1928: $assert->getOriginalType(),
1929: static function (Type $type, callable $traverse) use ($templateTypeMap, &$containsUnresolvedTemplate) {
1930: if ($type instanceof TemplateType && $type->getScope()->getClassName() !== null) {
1931: $resolvedType = $templateTypeMap->getType($type->getName());
1932: if ($resolvedType === null || $type->getBound()->equals($resolvedType)) {
1933: $containsUnresolvedTemplate = true;
1934: return $type;
1935: }
1936: }
1937:
1938: return $traverse($type);
1939: },
1940: );
1941:
1942: $newTypes = $this->create(
1943: $assertExpr,
1944: $assertedType,
1945: $assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(),
1946: $scope,
1947: )->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null);
1948: $types = $types !== null ? $types->unionWith($newTypes) : $newTypes;
1949:
1950: if (!$context->null() || !$assertedType instanceof ConstantBooleanType) {
1951: continue;
1952: }
1953:
1954: $subContext = $assertedType->getValue() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
1955: if ($assert->isNegated()) {
1956: $subContext = $subContext->negate();
1957: }
1958:
1959: $types = $types->unionWith($this->specifyTypesInCondition(
1960: $scope,
1961: $assertExpr,
1962: $subContext,
1963: ));
1964: }
1965: }
1966:
1967: return $types;
1968: }
1969:
1970: private function specifyTypesFromCallableCall(TypeSpecifierContext $context, FuncCall $call, Scope $scope): ?SpecifiedTypes
1971: {
1972: if (!$call->name instanceof Expr) {
1973: return null;
1974: }
1975:
1976: $calleeType = $scope->getType($call->name);
1977:
1978: $assertions = null;
1979: $parametersAcceptor = null;
1980: if ($calleeType->isCallable()->yes()) {
1981: $variants = $calleeType->getCallableParametersAcceptors($scope);
1982: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $call->getArgs(), $variants);
1983: if ($parametersAcceptor instanceof CallableParametersAcceptor) {
1984: $assertions = $parametersAcceptor->getAsserts();
1985: }
1986: }
1987:
1988: if ($assertions === null || $assertions->getAll() === []) {
1989: return null;
1990: }
1991:
1992: $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
1993: $type,
1994: $parametersAcceptor->getResolvedTemplateTypeMap(),
1995: $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
1996: TemplateTypeVariance::createInvariant(),
1997: ));
1998:
1999: return $this->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope);
2000: }
2001:
2002: /**
2003: * For `if ($a || $b)` truthy, expressions narrowed by stored conditional
2004: * holders (e.g. `$a = $obj instanceof ClassA;` records "when `$a` is
2005: * truthy, `$obj` is `ClassA`") need to be projected into the OR-truthy
2006: * scope as the union of the per-arm narrowings. specifyTypesInCondition
2007: * for each arm only looks at the boolean variable itself, so the held
2008: * narrowing of `$obj` would otherwise be invisible until a later check
2009: * pins one of the booleans down.
2010: *
2011: * For each conditional-holder target $T:
2012: * - resolve $T's type in the left-truthy and right-truthy filtered scopes
2013: * - if both narrow $T strictly below the original, add `$T : leftT|rightT`
2014: * as a sure type to the OR-truthy result
2015: *
2016: * The asymmetric case (one arm narrows, the other doesn't) is intentionally
2017: * skipped: in the OR-truthy scope the arm that didn't narrow could still be
2018: * the truthy one, so the sound result is the original (unnarrowed) type.
2019: */
2020: private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $scope, MutatingScope $rightScope, BooleanOr|LogicalOr $expr, SpecifiedTypes $types): SpecifiedTypes
2021: {
2022: $leftTruthyScope = $scope->filterByTruthyValue($expr->left);
2023: $rightTruthyScope = $rightScope->filterByTruthyValue($expr->right);
2024:
2025: $seen = [];
2026: foreach ([$scope, $rightScope] as $sourceScope) {
2027: foreach ($sourceScope->getConditionalExpressions() as $exprString => $holders) {
2028: if (isset($seen[$exprString])) {
2029: continue;
2030: }
2031: if ($holders === []) {
2032: continue;
2033: }
2034: $seen[$exprString] = true;
2035: $targetExpr = $holders[array_key_first($holders)]->getTypeHolder()->getExpr();
2036:
2037: // Only project when the target stays Yes-defined in the original
2038: // scope and in both filtered branches. A sure type implicitly
2039: // raises certainty to Yes, which would wrongly upgrade Maybe-defined
2040: // variables — `if (empty($a['bar']))` for instance leaves `$a`
2041: // Maybe-defined because `empty()` tolerates undefined offsets.
2042: if (!$scope->hasExpressionType($targetExpr)->yes()) {
2043: continue;
2044: }
2045: if (!$leftTruthyScope->hasExpressionType($targetExpr)->yes()) {
2046: continue;
2047: }
2048: if (!$rightTruthyScope->hasExpressionType($targetExpr)->yes()) {
2049: continue;
2050: }
2051:
2052: $origType = $scope->getType($targetExpr);
2053: $leftType = $leftTruthyScope->getType($targetExpr);
2054: $rightType = $rightTruthyScope->getType($targetExpr);
2055:
2056: $leftNarrowed = !$leftType->equals($origType) && $origType->isSuperTypeOf($leftType)->yes();
2057: $rightNarrowed = !$rightType->equals($origType) && $origType->isSuperTypeOf($rightType)->yes();
2058:
2059: if (!$leftNarrowed || !$rightNarrowed) {
2060: continue;
2061: }
2062:
2063: $unionType = TypeCombinator::union($leftType, $rightType);
2064: if ($unionType->equals($origType)) {
2065: continue;
2066: }
2067:
2068: $types = $types->unionWith(
2069: $this->create($targetExpr, $unionType, TypeSpecifierContext::createTrue(), $scope),
2070: );
2071: }
2072: }
2073:
2074: return $types;
2075: }
2076:
2077: private function augmentDisjunctionTypes(
2078: MutatingScope $scope,
2079: MutatingScope $rightScope,
2080: SpecifiedTypes $leftNormalized,
2081: SpecifiedTypes $rightNormalized,
2082: Expr $leftExpr,
2083: Expr $rightExpr,
2084: bool $truthy,
2085: SpecifiedTypes $types,
2086: ): SpecifiedTypes
2087: {
2088: $candidateExprs = [];
2089: foreach ($leftNormalized->getSureTypes() as $exprString => [$exprNode, $type]) {
2090: $candidateExprs[$exprString] = $exprNode;
2091: }
2092: foreach ($rightNormalized->getSureTypes() as $exprString => [$exprNode, $type]) {
2093: $candidateExprs[$exprString] = $exprNode;
2094: }
2095:
2096: $existingSureTypes = $types->getSureTypes();
2097:
2098: $viableCandidates = [];
2099: foreach ($candidateExprs as $exprString => $targetExpr) {
2100: if (isset($existingSureTypes[$exprString])) {
2101: continue;
2102: }
2103: if (!$scope->hasExpressionType($targetExpr)->yes()) {
2104: continue;
2105: }
2106: $viableCandidates[$exprString] = $targetExpr;
2107: }
2108:
2109: if ($viableCandidates === []) {
2110: return $types;
2111: }
2112:
2113: if ($truthy) {
2114: $leftFilteredScope = $scope->filterByTruthyValue($leftExpr);
2115: $rightFilteredScope = $rightScope->filterByTruthyValue($rightExpr);
2116: } else {
2117: $leftFilteredScope = $scope->filterByFalseyValue($leftExpr);
2118: $rightFilteredScope = $rightScope->filterByFalseyValue($rightExpr);
2119: }
2120:
2121: foreach ($viableCandidates as $targetExpr) {
2122: if (!$leftFilteredScope->hasExpressionType($targetExpr)->yes()) {
2123: continue;
2124: }
2125: if (!$rightFilteredScope->hasExpressionType($targetExpr)->yes()) {
2126: continue;
2127: }
2128:
2129: $originalType = $scope->getType($targetExpr);
2130: $leftType = $leftFilteredScope->getType($targetExpr);
2131: $rightType = $rightFilteredScope->getType($targetExpr);
2132:
2133: if ($leftType->equals($originalType) || !$originalType->isSuperTypeOf($leftType)->yes()) {
2134: continue;
2135: }
2136:
2137: if ($rightType->equals($originalType) || !$originalType->isSuperTypeOf($rightType)->yes()) {
2138: continue;
2139: }
2140:
2141: $unionType = TypeCombinator::union($leftType, $rightType);
2142: if ($unionType->equals($originalType)) {
2143: continue;
2144: }
2145:
2146: $types = $types->unionWith(
2147: $this->create($targetExpr, $unionType, TypeSpecifierContext::createTrue(), $scope),
2148: );
2149: }
2150:
2151: return $types;
2152: }
2153:
2154: /**
2155: * @return array<string, ConditionalExpressionHolder[]>
2156: */
2157: private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes, Scope $rightScope): array
2158: {
2159: $conditionExpressionTypes = [];
2160: foreach ($leftTypes->getSureTypes() as $exprString => [$expr, $type]) {
2161: if (!$this->isTrackableExpression($expr)) {
2162: continue;
2163: }
2164:
2165: $scopeType = $scope->getType($expr);
2166: $conditionType = TypeCombinator::remove($scopeType, $type);
2167: if ($scopeType->equals($conditionType)) {
2168: continue;
2169: }
2170:
2171: $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes(
2172: $expr,
2173: $conditionType,
2174: );
2175: }
2176:
2177: if (count($conditionExpressionTypes) > 0) {
2178: $holders = [];
2179: foreach ($rightTypes->getSureTypes() as $exprString => [$expr, $type]) {
2180: if (!$this->isTrackableExpression($expr)) {
2181: continue;
2182: }
2183:
2184: if (!isset($holders[$exprString])) {
2185: $holders[$exprString] = [];
2186: }
2187:
2188: $conditions = $conditionExpressionTypes;
2189: foreach (array_keys($conditions) as $conditionExprString) {
2190: if ($conditionExprString !== $exprString) {
2191: continue;
2192: }
2193: unset($conditions[$conditionExprString]);
2194: }
2195:
2196: if (count($conditions) === 0) {
2197: continue;
2198: }
2199:
2200: $targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope;
2201: $holder = new ConditionalExpressionHolder(
2202: $conditions,
2203: ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($targetScope->getType($expr), $type)),
2204: );
2205: $holders[$exprString][$holder->getKey()] = $holder;
2206: }
2207:
2208: return $holders;
2209: }
2210:
2211: return [];
2212: }
2213:
2214: private function isTrackableExpression(Expr $expr): bool
2215: {
2216: if ($expr instanceof Expr\Variable) {
2217: return is_string($expr->name);
2218: }
2219:
2220: return $expr instanceof Expr\PropertyFetch
2221: || $expr instanceof Expr\ArrayDimFetch
2222: || $expr instanceof Expr\StaticPropertyFetch;
2223: }
2224:
2225: /**
2226: * Flatten a deep BooleanOr chain into leaf expressions and process them
2227: * without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n)
2228: * for chains with many arms (e.g., 80+ === comparisons in ||).
2229: */
2230: private function specifyTypesForFlattenedBooleanOr(
2231: MutatingScope $scope,
2232: BooleanOr|LogicalOr $expr,
2233: TypeSpecifierContext $context,
2234: ): SpecifiedTypes
2235: {
2236: // Collect all leaf expressions from the chain
2237: $arms = [];
2238: $current = $expr;
2239: while ($current instanceof BooleanOr || $current instanceof LogicalOr) {
2240: $arms[] = $current->right;
2241: $current = $current->left;
2242: }
2243: $arms[] = $current; // leftmost leaf
2244: $arms = array_reverse($arms);
2245:
2246: if ($context->false() || $context->falsey()) {
2247: // Falsey: all arms are false → union all SpecifiedTypes.
2248: // Collect per-expression types first, then build unions once
2249: // to avoid O(N²) from incremental TypeCombinator::union() growth.
2250: /** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
2251: $sureTypesPerExpr = [];
2252: /** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
2253: $sureNotTypesPerExpr = [];
2254:
2255: foreach ($arms as $arm) {
2256: $armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2257: foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) {
2258: $sureTypesPerExpr[$exprString][0] = $exprNode;
2259: $sureTypesPerExpr[$exprString][1][] = $type;
2260: }
2261: foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) {
2262: $sureNotTypesPerExpr[$exprString][0] = $exprNode;
2263: $sureNotTypesPerExpr[$exprString][1][] = $type;
2264: }
2265: }
2266:
2267: $sureTypes = [];
2268: foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) {
2269: $sureTypes[$exprString] = [$exprNode, TypeCombinator::intersect(...$types)];
2270: }
2271: $sureNotTypes = [];
2272: foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) {
2273: $sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
2274: }
2275:
2276: return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr);
2277: }
2278:
2279: // Truthy: at least one arm is true → intersect all normalized SpecifiedTypes
2280: $armSpecifiedTypes = [];
2281: foreach ($arms as $arm) {
2282: $armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2283: $armSpecifiedTypes[] = $armTypes->normalize($scope);
2284: }
2285:
2286: $types = $armSpecifiedTypes[0];
2287: for ($i = 1; $i < count($armSpecifiedTypes); $i++) {
2288: $types = $types->intersectWith($armSpecifiedTypes[$i]);
2289: }
2290:
2291: $result = new SpecifiedTypes(
2292: $types->getSureTypes(),
2293: $types->getSureNotTypes(),
2294: );
2295: if ($types->shouldOverwrite()) {
2296: $result = $result->setAlwaysOverwriteTypes();
2297: }
2298:
2299: return $result->setRootExpr($expr);
2300: }
2301:
2302: /**
2303: * @param BooleanAnd|LogicalAnd $expr
2304: */
2305: private function specifyTypesForFlattenedBooleanAnd(
2306: MutatingScope $scope,
2307: Expr $expr,
2308: TypeSpecifierContext $context,
2309: ): SpecifiedTypes
2310: {
2311: $arms = [];
2312: $current = $expr;
2313: while ($current instanceof BooleanAnd || $current instanceof LogicalAnd) {
2314: $arms[] = $current->right;
2315: $current = $current->left;
2316: }
2317: $arms[] = $current;
2318: $arms = array_reverse($arms);
2319:
2320: // Truthy: all arms are true → union all SpecifiedTypes.
2321: // Collect per-expression types first, then build unions once
2322: // to avoid O(N²) from incremental growth.
2323: /** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
2324: $sureTypesPerExpr = [];
2325: /** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
2326: $sureNotTypesPerExpr = [];
2327:
2328: foreach ($arms as $arm) {
2329: $armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2330: foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) {
2331: $sureTypesPerExpr[$exprString][0] = $exprNode;
2332: $sureTypesPerExpr[$exprString][1][] = $type;
2333: }
2334: foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) {
2335: $sureNotTypesPerExpr[$exprString][0] = $exprNode;
2336: $sureNotTypesPerExpr[$exprString][1][] = $type;
2337: }
2338: }
2339:
2340: $sureTypes = [];
2341: foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) {
2342: $sureTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
2343: }
2344: $sureNotTypes = [];
2345: foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) {
2346: $sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
2347: }
2348:
2349: return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr);
2350: }
2351:
2352: /**
2353: * @return array<string, ConditionalExpressionHolder[]>
2354: */
2355: private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes, Scope $rightScope): array
2356: {
2357: $conditionExpressionTypes = [];
2358: foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
2359: if (!$this->isTrackableExpression($expr)) {
2360: continue;
2361: }
2362:
2363: $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes(
2364: $expr,
2365: TypeCombinator::intersect($scope->getType($expr), $type),
2366: );
2367: }
2368:
2369: if (count($conditionExpressionTypes) > 0) {
2370: $holders = [];
2371: foreach ($rightTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
2372: if (!$this->isTrackableExpression($expr)) {
2373: continue;
2374: }
2375:
2376: if (!isset($holders[$exprString])) {
2377: $holders[$exprString] = [];
2378: }
2379:
2380: $conditions = $conditionExpressionTypes;
2381: foreach (array_keys($conditions) as $conditionExprString) {
2382: if ($conditionExprString !== $exprString) {
2383: continue;
2384: }
2385: unset($conditions[$conditionExprString]);
2386: }
2387:
2388: if (count($conditions) === 0) {
2389: continue;
2390: }
2391:
2392: $targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope;
2393: $holder = new ConditionalExpressionHolder(
2394: $conditions,
2395: ExpressionTypeHolder::createYes($expr, TypeCombinator::remove($targetScope->getType($expr), $type)),
2396: );
2397: $holders[$exprString][$holder->getKey()] = $holder;
2398: }
2399:
2400: return $holders;
2401: }
2402:
2403: return [];
2404: }
2405:
2406: /**
2407: * @return array{Expr, ConstantScalarType, Type}|null
2408: */
2409: private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array
2410: {
2411: $leftType = $scope->getType($binaryOperation->left);
2412: $rightType = $scope->getType($binaryOperation->right);
2413:
2414: $rightExpr = $binaryOperation->right;
2415: if ($rightExpr instanceof AlwaysRememberedExpr) {
2416: $rightExpr = $rightExpr->getExpr();
2417: }
2418:
2419: $leftExpr = $binaryOperation->left;
2420: if ($leftExpr instanceof AlwaysRememberedExpr) {
2421: $leftExpr = $leftExpr->getExpr();
2422: }
2423:
2424: if (
2425: $leftType instanceof ConstantScalarType
2426: && !$rightExpr instanceof ConstFetch
2427: ) {
2428: return [$binaryOperation->right, $leftType, $rightType];
2429: } elseif (
2430: $rightType instanceof ConstantScalarType
2431: && !$leftExpr instanceof ConstFetch
2432: ) {
2433: return [$binaryOperation->left, $rightType, $leftType];
2434: }
2435:
2436: return null;
2437: }
2438:
2439: /**
2440: * @api
2441: */
2442: public function create(
2443: Expr $expr,
2444: Type $type,
2445: TypeSpecifierContext $context,
2446: Scope $scope,
2447: ): SpecifiedTypes
2448: {
2449: if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) {
2450: return (new SpecifiedTypes([], []))->setRootExpr($expr);
2451: }
2452:
2453: $specifiedExprs = [];
2454: if ($expr instanceof AlwaysRememberedExpr) {
2455: $specifiedExprs[] = $expr;
2456: $expr = $expr->expr;
2457: }
2458:
2459: if ($expr instanceof Expr\Assign) {
2460: $specifiedExprs[] = $expr->var;
2461: $specifiedExprs[] = $expr->expr;
2462:
2463: while ($expr->expr instanceof Expr\Assign) {
2464: $specifiedExprs[] = $expr->expr->var;
2465: $expr = $expr->expr;
2466: }
2467: } elseif ($expr instanceof Expr\AssignOp\Coalesce) {
2468: $specifiedExprs[] = $expr->var;
2469: } else {
2470: $specifiedExprs[] = $expr;
2471: }
2472:
2473: $types = null;
2474:
2475: foreach ($specifiedExprs as $specifiedExpr) {
2476: $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $scope);
2477:
2478: if ($types === null) {
2479: $types = $newTypes;
2480: } else {
2481: $types = $types->unionWith($newTypes);
2482: }
2483: }
2484:
2485: return $types;
2486: }
2487:
2488: private function createArrayDimFetchConditionalExpressionHolder(
2489: Expr\Variable $keyVar,
2490: Expr $arrayArg,
2491: Type $narrowedKeyType,
2492: Type $dimFetchType,
2493: ): SpecifiedTypes
2494: {
2495: $dimFetch = new ArrayDimFetch($arrayArg, $keyVar);
2496: $dimFetchString = $this->exprPrinter->printExpr($dimFetch);
2497: $keyExprString = $this->exprPrinter->printExpr($keyVar);
2498:
2499: $holder = new ConditionalExpressionHolder(
2500: [$keyExprString => ExpressionTypeHolder::createYes($keyVar, $narrowedKeyType)],
2501: ExpressionTypeHolder::createYes($dimFetch, $dimFetchType),
2502: );
2503:
2504: return (new SpecifiedTypes([], []))->setNewConditionalExpressionHolders([
2505: $dimFetchString => [$holder->getKey() => $holder],
2506: ]);
2507: }
2508:
2509: private function createForExpr(
2510: Expr $expr,
2511: Type $type,
2512: TypeSpecifierContext $context,
2513: Scope $scope,
2514: ): SpecifiedTypes
2515: {
2516: if ($context->true()) {
2517: $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no();
2518: } elseif ($context->false()) {
2519: $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no();
2520: }
2521:
2522: $originalExpr = $expr;
2523: if (isset($containsNull) && !$containsNull) {
2524: $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr);
2525: }
2526:
2527: if (
2528: !$context->null()
2529: && $expr instanceof Expr\BinaryOp\Coalesce
2530: ) {
2531: if (
2532: ($context->true() && $type->isSuperTypeOf($scope->getType($expr->right))->no())
2533: || ($context->false() && $type->isSuperTypeOf($scope->getType($expr->right))->yes())
2534: ) {
2535: $expr = $expr->left;
2536: }
2537: }
2538:
2539: if (
2540: $expr instanceof FuncCall
2541: && $expr->name instanceof Name
2542: ) {
2543: $has = $this->reflectionProvider->hasFunction($expr->name, $scope);
2544: if (!$has) {
2545: // backwards compatibility with previous behaviour
2546: return new SpecifiedTypes([], []);
2547: }
2548:
2549: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
2550: $hasSideEffects = $functionReflection->hasSideEffects();
2551: if ($hasSideEffects->yes()) {
2552: return new SpecifiedTypes([], []);
2553: }
2554:
2555: if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
2556: return new SpecifiedTypes([], []);
2557: }
2558: }
2559:
2560: if (
2561: $expr instanceof FuncCall
2562: && !$expr->name instanceof Name
2563: ) {
2564: $nameType = $scope->getType($expr->name);
2565: if ($nameType->isCallable()->yes()) {
2566: $isPure = null;
2567: foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
2568: $variantIsPure = $variant->isPure();
2569: $isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
2570: }
2571:
2572: if ($isPure !== null) {
2573: if ($isPure->no()) {
2574: return new SpecifiedTypes([], []);
2575: }
2576:
2577: if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
2578: return new SpecifiedTypes([], []);
2579: }
2580: }
2581: }
2582: }
2583:
2584: if (
2585: $expr instanceof MethodCall
2586: && $expr->name instanceof Node\Identifier
2587: ) {
2588: $methodName = $expr->name->toString();
2589: $calledOnType = $scope->getType($expr->var);
2590: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
2591: if (
2592: $methodReflection === null
2593: || $methodReflection->hasSideEffects()->yes()
2594: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
2595: ) {
2596: if (isset($containsNull) && !$containsNull) {
2597: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
2598: }
2599:
2600: return new SpecifiedTypes([], []);
2601: }
2602: }
2603:
2604: if (
2605: $expr instanceof StaticCall
2606: && $expr->name instanceof Node\Identifier
2607: ) {
2608: $methodName = $expr->name->toString();
2609: if ($expr->class instanceof Name) {
2610: $calledOnType = $scope->resolveTypeByName($expr->class);
2611: } else {
2612: $calledOnType = $scope->getType($expr->class);
2613: }
2614:
2615: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
2616: if (
2617: $methodReflection === null
2618: || $methodReflection->hasSideEffects()->yes()
2619: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
2620: ) {
2621: if (isset($containsNull) && !$containsNull) {
2622: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
2623: }
2624:
2625: return new SpecifiedTypes([], []);
2626: }
2627: }
2628:
2629: $sureTypes = [];
2630: $sureNotTypes = [];
2631: if ($context->false()) {
2632: $exprString = $this->exprPrinter->printExpr($expr);
2633: $sureNotTypes[$exprString] = [$expr, $type];
2634:
2635: if ($expr !== $originalExpr) {
2636: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
2637: $sureNotTypes[$originalExprString] = [$originalExpr, $type];
2638: }
2639: } elseif ($context->true()) {
2640: $exprString = $this->exprPrinter->printExpr($expr);
2641: $sureTypes[$exprString] = [$expr, $type];
2642:
2643: if ($expr !== $originalExpr) {
2644: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
2645: $sureTypes[$originalExprString] = [$originalExpr, $type];
2646: }
2647: }
2648:
2649: $types = new SpecifiedTypes($sureTypes, $sureNotTypes);
2650: if (isset($containsNull) && !$containsNull) {
2651: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types);
2652: }
2653:
2654: return $types;
2655: }
2656:
2657: private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes
2658: {
2659: if ($expr instanceof Expr\NullsafePropertyFetch) {
2660: if ($type !== null) {
2661: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, $scope);
2662: } else {
2663: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), $scope);
2664: }
2665:
2666: return $propertyFetchTypes->unionWith(
2667: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
2668: );
2669: }
2670:
2671: if ($expr instanceof Expr\NullsafeMethodCall) {
2672: if ($type !== null) {
2673: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $scope);
2674: } else {
2675: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $scope);
2676: }
2677:
2678: return $methodCallTypes->unionWith(
2679: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
2680: );
2681: }
2682:
2683: if ($expr instanceof Expr\PropertyFetch) {
2684: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
2685: }
2686:
2687: if ($expr instanceof Expr\MethodCall) {
2688: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
2689: }
2690:
2691: if ($expr instanceof Expr\ArrayDimFetch) {
2692: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
2693: }
2694:
2695: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
2696: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
2697: }
2698:
2699: if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
2700: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
2701: }
2702:
2703: return new SpecifiedTypes([], []);
2704: }
2705:
2706: private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
2707: {
2708: $sureNotTypes = [];
2709:
2710: if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) {
2711: $exprString = $this->exprPrinter->printExpr($expr);
2712: if ($context->false()) {
2713: $sureNotTypes[$exprString] = [$expr, $type];
2714: } elseif ($context->true()) {
2715: $inverted = TypeCombinator::remove(new IntegerType(), $type);
2716: $sureNotTypes[$exprString] = [$expr, $inverted];
2717: }
2718: }
2719:
2720: return (new SpecifiedTypes(sureNotTypes: $sureNotTypes))->setRootExpr($rootExpr);
2721: }
2722:
2723: /**
2724: * @return FunctionTypeSpecifyingExtension[]
2725: */
2726: private function getFunctionTypeSpecifyingExtensions(): array
2727: {
2728: return $this->functionTypeSpecifyingExtensions;
2729: }
2730:
2731: /**
2732: * @return MethodTypeSpecifyingExtension[]
2733: */
2734: private function getMethodTypeSpecifyingExtensionsForClass(string $className): array
2735: {
2736: if ($this->methodTypeSpecifyingExtensionsByClass === null) {
2737: $byClass = [];
2738: foreach ($this->methodTypeSpecifyingExtensions as $extension) {
2739: $byClass[$extension->getClass()][] = $extension;
2740: }
2741:
2742: $this->methodTypeSpecifyingExtensionsByClass = $byClass;
2743: }
2744: return $this->getTypeSpecifyingExtensionsForType($this->methodTypeSpecifyingExtensionsByClass, $className);
2745: }
2746:
2747: /**
2748: * @return StaticMethodTypeSpecifyingExtension[]
2749: */
2750: private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
2751: {
2752: if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
2753: $byClass = [];
2754: foreach ($this->staticMethodTypeSpecifyingExtensions as $extension) {
2755: $byClass[$extension->getClass()][] = $extension;
2756: }
2757:
2758: $this->staticMethodTypeSpecifyingExtensionsByClass = $byClass;
2759: }
2760: return $this->getTypeSpecifyingExtensionsForType($this->staticMethodTypeSpecifyingExtensionsByClass, $className);
2761: }
2762:
2763: /**
2764: * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions
2765: * @return mixed[]
2766: */
2767: private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array
2768: {
2769: $extensionsForClass = [[]];
2770: $class = $this->reflectionProvider->getClass($className);
2771: foreach (array_merge([$className], $class->getParentClassesNames(), $class->getNativeReflection()->getInterfaceNames()) as $extensionClassName) {
2772: if (!isset($extensions[$extensionClassName])) {
2773: continue;
2774: }
2775:
2776: $extensionsForClass[] = $extensions[$extensionClassName];
2777: }
2778:
2779: return array_merge(...$extensionsForClass);
2780: }
2781:
2782: public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
2783: {
2784: $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
2785: if ($expressions !== null) {
2786: $exprNode = $expressions[0];
2787: $constantType = $expressions[1];
2788: $otherType = $expressions[2];
2789:
2790: if (!$context->null() && $constantType->getValue() === null) {
2791: $trueTypes = [
2792: new NullType(),
2793: new ConstantBooleanType(false),
2794: new ConstantIntegerType(0),
2795: new ConstantFloatType(0.0),
2796: new ConstantStringType(''),
2797: new ConstantArrayType([], []),
2798: ];
2799: return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr);
2800: }
2801:
2802: if (!$context->null() && $constantType->getValue() === false) {
2803: return $this->specifyTypesInCondition(
2804: $scope,
2805: $exprNode,
2806: $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(),
2807: )->setRootExpr($expr);
2808: }
2809:
2810: if (!$context->null() && $constantType->getValue() === true) {
2811: return $this->specifyTypesInCondition(
2812: $scope,
2813: $exprNode,
2814: $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(),
2815: )->setRootExpr($expr);
2816: }
2817:
2818: if (!$context->null() && $constantType->getValue() === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) {
2819: /* There is a difference between php 7.x and 8.x on the equality
2820: * behavior between zero and the empty string, so to be conservative
2821: * we leave it untouched regardless of the language version */
2822: if ($context->true()) {
2823: $trueTypes = [
2824: new NullType(),
2825: new ConstantBooleanType(false),
2826: new ConstantIntegerType(0),
2827: new ConstantFloatType(0.0),
2828: new StringType(),
2829: ];
2830: } else {
2831: $trueTypes = [
2832: new NullType(),
2833: new ConstantBooleanType(false),
2834: new ConstantIntegerType(0),
2835: new ConstantFloatType(0.0),
2836: new ConstantStringType('0'),
2837: ];
2838: }
2839: return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr);
2840: }
2841:
2842: if (!$context->null() && $constantType->getValue() === '') {
2843: /* There is a difference between php 7.x and 8.x on the equality
2844: * behavior between zero and the empty string, so to be conservative
2845: * we leave it untouched regardless of the language version */
2846: if ($context->true()) {
2847: $trueTypes = [
2848: new NullType(),
2849: new ConstantBooleanType(false),
2850: new ConstantIntegerType(0),
2851: new ConstantFloatType(0.0),
2852: new ConstantStringType(''),
2853: ];
2854: } else {
2855: $trueTypes = [
2856: new NullType(),
2857: new ConstantBooleanType(false),
2858: new ConstantStringType(''),
2859: ];
2860: }
2861: return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr);
2862: }
2863:
2864: if (
2865: $exprNode instanceof FuncCall
2866: && $exprNode->name instanceof Name
2867: && !$exprNode->isFirstClassCallable()
2868: && in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true)
2869: && isset($exprNode->getArgs()[0])
2870: && $constantType->isString()->yes()
2871: ) {
2872: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2873: }
2874:
2875: if (
2876: $context->true()
2877: && $exprNode instanceof FuncCall
2878: && $exprNode->name instanceof Name
2879: && $exprNode->name->toLowerString() === 'preg_match'
2880: && (new ConstantIntegerType(1))->isSuperTypeOf($constantType)->yes()
2881: ) {
2882: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2883: }
2884:
2885: if (
2886: $context->true()
2887: && $exprNode instanceof ClassConstFetch
2888: && $exprNode->name instanceof Node\Identifier
2889: && strtolower($exprNode->name->toString()) === 'class'
2890: && $constantType->isString()->yes()
2891: ) {
2892: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2893: }
2894: }
2895:
2896: $leftType = $scope->getType($expr->left);
2897: $rightType = $scope->getType($expr->right);
2898:
2899: $leftBooleanType = $leftType->toBoolean();
2900: if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) {
2901: return $this->specifyTypesInCondition(
2902: $scope,
2903: new Expr\BinaryOp\Identical(
2904: new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')),
2905: $expr->right,
2906: ),
2907: $context,
2908: )->setRootExpr($expr);
2909: }
2910:
2911: $rightBooleanType = $rightType->toBoolean();
2912: if ($rightBooleanType instanceof ConstantBooleanType && $leftType->isBoolean()->yes()) {
2913: return $this->specifyTypesInCondition(
2914: $scope,
2915: new Expr\BinaryOp\Identical(
2916: $expr->left,
2917: new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')),
2918: ),
2919: $context,
2920: )->setRootExpr($expr);
2921: }
2922:
2923: if (
2924: !$context->null()
2925: && $rightType->isArray()->yes()
2926: && $leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no()
2927: ) {
2928: return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr);
2929: }
2930:
2931: if (
2932: !$context->null()
2933: && $leftType->isArray()->yes()
2934: && $rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no()
2935: ) {
2936: return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr);
2937: }
2938:
2939: if (
2940: ($leftType->isString()->yes() && $rightType->isString()->yes())
2941: || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes())
2942: || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes())
2943: || ($leftType->isEnum()->yes() && $rightType->isEnum()->yes())
2944: ) {
2945: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2946: }
2947:
2948: $leftExprString = $this->exprPrinter->printExpr($expr->left);
2949: $rightExprString = $this->exprPrinter->printExpr($expr->right);
2950: if ($leftExprString === $rightExprString) {
2951: if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) {
2952: return (new SpecifiedTypes([], []))->setRootExpr($expr);
2953: }
2954: }
2955:
2956: $leftTypes = $this->create($expr->left, $leftType, $context, $scope)->setRootExpr($expr);
2957: $rightTypes = $this->create($expr->right, $rightType, $context, $scope)->setRootExpr($expr);
2958:
2959: return $context->true()
2960: ? $leftTypes->unionWith($rightTypes)
2961: : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope));
2962: }
2963:
2964: public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
2965: {
2966: $leftExpr = $expr->left;
2967: $rightExpr = $expr->right;
2968:
2969: // Normalize to: fn() === expr
2970: if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) {
2971: $specifiedTypes = $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical(
2972: $rightExpr,
2973: $leftExpr,
2974: ), $scope, $context);
2975: } else {
2976: $specifiedTypes = $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical(
2977: $leftExpr,
2978: $rightExpr,
2979: ), $scope, $context);
2980: }
2981:
2982: // merge result of fn1() === fn2() and fn2() === fn1()
2983: if ($rightExpr instanceof FuncCall && $leftExpr instanceof FuncCall) {
2984: return $specifiedTypes->unionWith(
2985: $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical(
2986: $rightExpr,
2987: $leftExpr,
2988: ), $scope, $context),
2989: );
2990: }
2991:
2992: return $specifiedTypes;
2993: }
2994:
2995: private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
2996: {
2997: $leftExpr = $expr->left;
2998: $rightExpr = $expr->right;
2999:
3000: $unwrappedLeftExpr = $leftExpr;
3001: if ($leftExpr instanceof AlwaysRememberedExpr) {
3002: $unwrappedLeftExpr = $leftExpr->getExpr();
3003: }
3004: $unwrappedRightExpr = $rightExpr;
3005: if ($rightExpr instanceof AlwaysRememberedExpr) {
3006: $unwrappedRightExpr = $rightExpr->getExpr();
3007: }
3008:
3009: $rightType = $scope->getType($rightExpr);
3010:
3011: // (count($a) === $expr)
3012: if (
3013: !$context->null()
3014: && $unwrappedLeftExpr instanceof FuncCall
3015: && !$unwrappedLeftExpr->isFirstClassCallable()
3016: && count($unwrappedLeftExpr->getArgs()) >= 1
3017: && $unwrappedLeftExpr->name instanceof Name
3018: && in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
3019: && $rightType->isInteger()->yes()
3020: ) {
3021: // count($a) === count($b)
3022: if (
3023: $context->true()
3024: && $unwrappedRightExpr instanceof FuncCall
3025: && $unwrappedRightExpr->name instanceof Name
3026: && !$unwrappedRightExpr->isFirstClassCallable()
3027: && in_array($unwrappedRightExpr->name->toLowerString(), ['count', 'sizeof'], true)
3028: && count($unwrappedRightExpr->getArgs()) >= 1
3029: ) {
3030: $argType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value);
3031: $sizeType = $scope->getType($leftExpr);
3032:
3033: $specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedRightExpr, $argType, $sizeType, $context, $scope, $expr);
3034: if ($specifiedTypes !== null) {
3035: return $specifiedTypes;
3036: }
3037:
3038: $leftArrayType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
3039: $rightArrayType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value);
3040: if (
3041: $leftArrayType->isArray()->yes()
3042: && $rightArrayType->isArray()->yes()
3043: && !$rightType->isConstantScalarValue()->yes()
3044: && ($leftArrayType->isIterableAtLeastOnce()->yes() || $rightArrayType->isIterableAtLeastOnce()->yes())
3045: ) {
3046: $arrayTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr);
3047: return $arrayTypes->unionWith(
3048: $this->create($unwrappedRightExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr),
3049: );
3050: }
3051: }
3052:
3053: if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
3054: return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr);
3055: }
3056:
3057: $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
3058: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
3059: if ($isZero->yes()) {
3060: $funcTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
3061:
3062: if ($context->truthy() && !$argType->isArray()->yes()) {
3063: $newArgType = new UnionType([
3064: new ObjectType(Countable::class),
3065: new ConstantArrayType([], []),
3066: ]);
3067: } else {
3068: $newArgType = new ConstantArrayType([], []);
3069: }
3070:
3071: return $funcTypes->unionWith(
3072: $this->create($unwrappedLeftExpr->getArgs()[0]->value, $newArgType, $context, $scope)->setRootExpr($expr),
3073: );
3074: }
3075:
3076: $specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
3077: if ($specifiedTypes !== null) {
3078: if ($leftExpr !== $unwrappedLeftExpr) {
3079: $funcTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
3080: return $specifiedTypes->unionWith($funcTypes);
3081: }
3082: return $specifiedTypes;
3083: }
3084:
3085: if ($context->truthy() && $argType->isArray()->yes()) {
3086: $funcTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
3087: if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
3088: return $funcTypes->unionWith(
3089: $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr),
3090: );
3091: }
3092:
3093: return $funcTypes;
3094: }
3095: }
3096:
3097: // strlen($a) === $b
3098: if (
3099: !$context->null()
3100: && $unwrappedLeftExpr instanceof FuncCall
3101: && $unwrappedLeftExpr->name instanceof Name
3102: && !$unwrappedLeftExpr->isFirstClassCallable()
3103: && in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true)
3104: && count($unwrappedLeftExpr->getArgs()) === 1
3105: && $rightType->isInteger()->yes()
3106: ) {
3107: if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
3108: return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr);
3109: }
3110:
3111: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
3112: if ($isZero->yes()) {
3113: $funcTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
3114: return $funcTypes->unionWith(
3115: $this->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), $context, $scope)->setRootExpr($expr),
3116: );
3117: }
3118:
3119: if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
3120: $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
3121: if ($argType->isString()->yes()) {
3122: $funcTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
3123:
3124: $accessory = new AccessoryNonEmptyStringType();
3125: if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($rightType)->yes()) {
3126: $accessory = new AccessoryNonFalsyStringType();
3127: }
3128: $valueTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr);
3129:
3130: return $funcTypes->unionWith($valueTypes);
3131: }
3132: }
3133: }
3134:
3135: // array_key_first($a) !== null
3136: // array_key_last($a) !== null
3137: // array_find_key($a, $cb) !== null
3138: if (
3139: $unwrappedLeftExpr instanceof FuncCall
3140: && $unwrappedLeftExpr->name instanceof Name
3141: && !$unwrappedLeftExpr->isFirstClassCallable()
3142: && in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last', 'array_find_key'], true)
3143: && isset($unwrappedLeftExpr->getArgs()[0])
3144: && $rightType->isNull()->yes()
3145: ) {
3146: $args = $unwrappedLeftExpr->getArgs();
3147: $argType = $scope->getType($args[0]->value);
3148: if ($argType->isArray()->yes()) {
3149: return $this->create($args[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr);
3150: }
3151: }
3152:
3153: // preg_match($a) === $b
3154: if (
3155: $context->true()
3156: && $unwrappedLeftExpr instanceof FuncCall
3157: && $unwrappedLeftExpr->name instanceof Name
3158: && $unwrappedLeftExpr->name->toLowerString() === 'preg_match'
3159: && (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes()
3160: ) {
3161: return $this->specifyTypesInCondition(
3162: $scope,
3163: $leftExpr,
3164: $context,
3165: )->setRootExpr($expr);
3166: }
3167:
3168: // get_class($a) === 'Foo'
3169: if (
3170: $context->true()
3171: && $unwrappedLeftExpr instanceof FuncCall
3172: && $unwrappedLeftExpr->name instanceof Name
3173: && !$unwrappedLeftExpr->isFirstClassCallable()
3174: && in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true)
3175: && isset($unwrappedLeftExpr->getArgs()[0])
3176: ) {
3177: if ($rightType instanceof ConstantStringType && $this->reflectionProvider->hasClass($rightType->getValue())) {
3178: return $this->create(
3179: $unwrappedLeftExpr->getArgs()[0]->value,
3180: new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()),
3181: $context,
3182: $scope,
3183: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
3184: }
3185: if ($rightType->getClassStringObjectType()->isObject()->yes()) {
3186: return $this->create(
3187: $unwrappedLeftExpr->getArgs()[0]->value,
3188: $rightType->getClassStringObjectType(),
3189: $context,
3190: $scope,
3191: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
3192: }
3193: }
3194:
3195: if (
3196: $context->truthy()
3197: && $unwrappedLeftExpr instanceof FuncCall
3198: && $unwrappedLeftExpr->name instanceof Name
3199: && !$unwrappedLeftExpr->isFirstClassCallable()
3200: && in_array(strtolower($unwrappedLeftExpr->name->toString()), [
3201: 'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst',
3202: 'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst',
3203: 'ucwords', 'mb_convert_case', 'mb_convert_kana',
3204: ], true)
3205: && isset($unwrappedLeftExpr->getArgs()[0])
3206: && $rightType->isNonEmptyString()->yes()
3207: ) {
3208: $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
3209:
3210: if ($argType->isString()->yes()) {
3211: $specifiedTypes = new SpecifiedTypes();
3212: if (in_array(strtolower($unwrappedLeftExpr->name->toString()), ['strtolower', 'mb_strtolower'], true)) {
3213: $specifiedTypes = $this->create(
3214: $unwrappedRightExpr,
3215: TypeCombinator::intersect($rightType, new AccessoryLowercaseStringType()),
3216: $context,
3217: $scope,
3218: )->setRootExpr($expr);
3219: }
3220: if (in_array(strtolower($unwrappedLeftExpr->name->toString()), ['strtoupper', 'mb_strtoupper'], true)) {
3221: $specifiedTypes = $this->create(
3222: $unwrappedRightExpr,
3223: TypeCombinator::intersect($rightType, new AccessoryUppercaseStringType()),
3224: $context,
3225: $scope,
3226: )->setRootExpr($expr);
3227: }
3228:
3229: if ($rightType->isNonFalsyString()->yes()) {
3230: return $specifiedTypes->unionWith($this->create(
3231: $unwrappedLeftExpr->getArgs()[0]->value,
3232: TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()),
3233: $context,
3234: $scope,
3235: )->setRootExpr($expr));
3236: }
3237:
3238: return $specifiedTypes->unionWith($this->create(
3239: $unwrappedLeftExpr->getArgs()[0]->value,
3240: TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()),
3241: $context,
3242: $scope,
3243: )->setRootExpr($expr));
3244: }
3245: }
3246:
3247: if ($rightType->isString()->yes()) {
3248: $types = null;
3249: foreach ($rightType->getConstantStrings() as $constantString) {
3250: $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $expr);
3251:
3252: if ($specifiedType === null) {
3253: continue;
3254: }
3255: if ($types === null) {
3256: $types = $specifiedType;
3257: continue;
3258: }
3259:
3260: $types = $types->intersectWith($specifiedType);
3261: }
3262:
3263: if ($types !== null) {
3264: if ($leftExpr !== $unwrappedLeftExpr) {
3265: $types = $types->unionWith($this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr));
3266: }
3267: return $types;
3268: }
3269: }
3270:
3271: $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
3272: if ($expressions !== null) {
3273: $exprNode = $expressions[0];
3274: $constantType = $expressions[1];
3275:
3276: $unwrappedExprNode = $exprNode;
3277: if ($exprNode instanceof AlwaysRememberedExpr) {
3278: $unwrappedExprNode = $exprNode->getExpr();
3279: }
3280:
3281: $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedExprNode, $constantType, $context, $scope, $expr);
3282: if ($specifiedType !== null) {
3283: if ($exprNode !== $unwrappedExprNode) {
3284: $specifiedType = $specifiedType->unionWith(
3285: $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($expr),
3286: );
3287: }
3288: return $specifiedType;
3289: }
3290: }
3291:
3292: // $a::class === 'Foo'
3293: if (
3294: $context->true() &&
3295: $unwrappedLeftExpr instanceof ClassConstFetch &&
3296: $unwrappedLeftExpr->class instanceof Expr &&
3297: $unwrappedLeftExpr->name instanceof Node\Identifier &&
3298: $unwrappedRightExpr instanceof ClassConstFetch &&
3299: $rightType instanceof ConstantStringType &&
3300: $rightType->getValue() !== '' &&
3301: strtolower($unwrappedLeftExpr->name->toString()) === 'class'
3302: ) {
3303: if ($this->reflectionProvider->hasClass($rightType->getValue())) {
3304: return $this->create(
3305: $unwrappedLeftExpr->class,
3306: new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()),
3307: $context,
3308: $scope,
3309: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
3310: }
3311: return $this->specifyTypesInCondition(
3312: $scope,
3313: new Instanceof_(
3314: $unwrappedLeftExpr->class,
3315: new Name($rightType->getValue()),
3316: ),
3317: $context,
3318: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
3319: }
3320:
3321: $leftType = $scope->getType($leftExpr);
3322:
3323: // 'Foo' === $a::class
3324: if (
3325: $context->true() &&
3326: $unwrappedRightExpr instanceof ClassConstFetch &&
3327: $unwrappedRightExpr->class instanceof Expr &&
3328: $unwrappedRightExpr->name instanceof Node\Identifier &&
3329: $unwrappedLeftExpr instanceof ClassConstFetch &&
3330: $leftType instanceof ConstantStringType &&
3331: $leftType->getValue() !== '' &&
3332: strtolower($unwrappedRightExpr->name->toString()) === 'class'
3333: ) {
3334: if ($this->reflectionProvider->hasClass($leftType->getValue())) {
3335: return $this->create(
3336: $unwrappedRightExpr->class,
3337: new ObjectType($leftType->getValue(), classReflection: $this->reflectionProvider->getClass($leftType->getValue())->asFinal()),
3338: $context,
3339: $scope,
3340: )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr));
3341: }
3342:
3343: return $this->specifyTypesInCondition(
3344: $scope,
3345: new Instanceof_(
3346: $unwrappedRightExpr->class,
3347: new Name($leftType->getValue()),
3348: ),
3349: $context,
3350: )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr));
3351: }
3352:
3353: if ($context->false()) {
3354: $identicalType = $scope->getType($expr);
3355: if ($identicalType instanceof ConstantBooleanType) {
3356: $never = new NeverType();
3357: $contextForTypes = $identicalType->getValue() ? $context->negate() : $context;
3358: if ($leftExpr instanceof AlwaysRememberedExpr) {
3359: $leftTypes = $this->create($unwrappedLeftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
3360: } else {
3361: $leftTypes = $this->create($leftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
3362: }
3363: if ($rightExpr instanceof AlwaysRememberedExpr) {
3364: $rightTypes = $this->create($unwrappedRightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
3365: } else {
3366: $rightTypes = $this->create($rightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
3367: }
3368: return $leftTypes->unionWith($rightTypes);
3369: }
3370: }
3371:
3372: $types = null;
3373: if (
3374: count($leftType->getFiniteTypes()) === 1
3375: || (
3376: $context->true()
3377: && $leftType->isConstantValue()->yes()
3378: && !$rightType->equals($leftType)
3379: && $rightType->isSuperTypeOf($leftType)->yes())
3380: ) {
3381: $types = $this->create(
3382: $rightExpr,
3383: $leftType,
3384: $context,
3385: $scope,
3386: )->setRootExpr($expr);
3387: if ($rightExpr instanceof AlwaysRememberedExpr) {
3388: $types = $types->unionWith($this->create(
3389: $unwrappedRightExpr,
3390: $leftType,
3391: $context,
3392: $scope,
3393: ))->setRootExpr($expr);
3394: }
3395: }
3396: if (
3397: count($rightType->getFiniteTypes()) === 1
3398: || (
3399: $context->true()
3400: && $rightType->isConstantValue()->yes()
3401: && !$leftType->equals($rightType)
3402: && $leftType->isSuperTypeOf($rightType)->yes()
3403: )
3404: ) {
3405: $leftTypes = $this->create(
3406: $leftExpr,
3407: $rightType,
3408: $context,
3409: $scope,
3410: )->setRootExpr($expr);
3411: if ($leftExpr instanceof AlwaysRememberedExpr) {
3412: $leftTypes = $leftTypes->unionWith($this->create(
3413: $unwrappedLeftExpr,
3414: $rightType,
3415: $context,
3416: $scope,
3417: ))->setRootExpr($expr);
3418: }
3419: if ($types !== null) {
3420: $types = $types->unionWith($leftTypes);
3421: } else {
3422: $types = $leftTypes;
3423: }
3424: }
3425:
3426: if ($types !== null) {
3427: return $types;
3428: }
3429:
3430: $leftExprString = $this->exprPrinter->printExpr($unwrappedLeftExpr);
3431: $rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr);
3432: if ($leftExprString === $rightExprString) {
3433: if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) {
3434: return (new SpecifiedTypes([], []))->setRootExpr($expr);
3435: }
3436: }
3437:
3438: if ($context->true()) {
3439: $leftTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
3440: $rightTypes = $this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr);
3441: if ($leftExpr instanceof AlwaysRememberedExpr) {
3442: $leftTypes = $leftTypes->unionWith(
3443: $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr),
3444: );
3445: }
3446: if ($rightExpr instanceof AlwaysRememberedExpr) {
3447: $rightTypes = $rightTypes->unionWith(
3448: $this->create($unwrappedRightExpr, $leftType, $context, $scope)->setRootExpr($expr),
3449: );
3450: }
3451: return $leftTypes->unionWith($rightTypes);
3452: } elseif ($context->false()) {
3453: return $this->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope)
3454: ->intersectWith($this->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope));
3455: }
3456:
3457: return (new SpecifiedTypes([], []))->setRootExpr($expr);
3458: }
3459:
3460: }
3461: