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