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