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