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