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