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