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