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