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