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