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