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