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