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