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_key_first/last($arr)
691: if (
692: $expr->expr instanceof FuncCall
693: && $expr->expr->name instanceof Name
694: && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
695: && count($expr->expr->getArgs()) >= 1
696: ) {
697: $arrayArg = $expr->expr->getArgs()[0]->value;
698: $arrayType = $scope->getType($arrayArg);
699: if (
700: $arrayType->isArray()->yes()
701: && $arrayType->isIterableAtLeastOnce()->yes()
702: ) {
703: $dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
704: $iterableValueType = $expr->expr->name->toLowerString() === 'array_key_first'
705: ? $arrayType->getFirstIterableValueType()
706: : $arrayType->getLastIterableValueType();
707:
708: return $specifiedTypes->unionWith(
709: $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope),
710: );
711: }
712: }
713:
714: // infer $list[$count] after $count = count($list) - 1
715: if (
716: $expr->expr instanceof Expr\BinaryOp\Minus
717: && $expr->expr->left instanceof FuncCall
718: && $expr->expr->left->name instanceof Name
719: && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true)
720: && count($expr->expr->left->getArgs()) >= 1
721: && $expr->expr->right instanceof Node\Scalar\Int_
722: && $expr->expr->right->value === 1
723: ) {
724: $arrayArg = $expr->expr->left->getArgs()[0]->value;
725: $arrayType = $scope->getType($arrayArg);
726: if (
727: $arrayType->isList()->yes()
728: && $arrayType->isIterableAtLeastOnce()->yes()
729: ) {
730: $dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
731:
732: return $specifiedTypes->unionWith(
733: $this->create($dimFetch, $arrayType->getLastIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
734: );
735: }
736: }
737:
738: return $specifiedTypes;
739: }
740:
741: $specifiedTypes = $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);
742:
743: if ($context->true()) {
744: // infer $arr[$key] after $key = array_search($needle, $arr)
745: if (
746: $expr->expr instanceof FuncCall
747: && $expr->expr->name instanceof Name
748: && $expr->expr->name->toLowerString() === 'array_search'
749: && count($expr->expr->getArgs()) >= 2
750: ) {
751: $arrayArg = $expr->expr->getArgs()[1]->value;
752: $arrayType = $scope->getType($arrayArg);
753:
754: if ($arrayType->isArray()->yes()) {
755: $dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
756: $iterableValueType = $arrayType->getIterableValueType();
757:
758: return $specifiedTypes->unionWith(
759: $this->create($dimFetch, $iterableValueType, TypeSpecifierContext::createTrue(), $scope),
760: );
761: }
762: }
763: }
764: return $specifiedTypes;
765: } elseif (
766: $expr instanceof Expr\Isset_
767: && count($expr->vars) > 0
768: && !$context->null()
769: ) {
770: // rewrite multi param isset() to and-chained single param isset()
771: if (count($expr->vars) > 1) {
772: $issets = [];
773: foreach ($expr->vars as $var) {
774: $issets[] = new Expr\Isset_([$var], $expr->getAttributes());
775: }
776:
777: $first = array_shift($issets);
778: $andChain = null;
779: foreach ($issets as $isset) {
780: if ($andChain === null) {
781: $andChain = new BooleanAnd($first, $isset);
782: continue;
783: }
784:
785: $andChain = new BooleanAnd($andChain, $isset);
786: }
787:
788: if ($andChain === null) {
789: throw new ShouldNotHappenException();
790: }
791:
792: return $this->specifyTypesInCondition($scope, $andChain, $context)->setRootExpr($expr);
793: }
794:
795: $issetExpr = $expr->vars[0];
796:
797: if (!$context->true()) {
798: if (!$scope instanceof MutatingScope) {
799: throw new ShouldNotHappenException();
800: }
801:
802: $isset = $scope->issetCheck($issetExpr, static fn () => true);
803:
804: if ($isset === false) {
805: return new SpecifiedTypes();
806: }
807:
808: $type = $scope->getType($issetExpr);
809: $isNullable = !$type->isNull()->no();
810: $exprType = $this->create(
811: $issetExpr,
812: new NullType(),
813: $context->negate(),
814: $scope,
815: )->setRootExpr($expr);
816:
817: if ($issetExpr instanceof Expr\Variable && is_string($issetExpr->name)) {
818: if ($isset === true) {
819: if ($isNullable) {
820: return $exprType;
821: }
822:
823: // variable cannot exist in !isset()
824: return $exprType->unionWith($this->create(
825: new IssetExpr($issetExpr),
826: new NullType(),
827: $context,
828: $scope,
829: ))->setRootExpr($expr);
830: }
831:
832: if ($isNullable) {
833: // reduces variable certainty to maybe
834: return $exprType->unionWith($this->create(
835: new IssetExpr($issetExpr),
836: new NullType(),
837: $context->negate(),
838: $scope,
839: ))->setRootExpr($expr);
840: }
841:
842: // variable cannot exist in !isset()
843: return $this->create(
844: new IssetExpr($issetExpr),
845: new NullType(),
846: $context,
847: $scope,
848: )->setRootExpr($expr);
849: }
850:
851: if ($isNullable && $isset === true) {
852: return $exprType;
853: }
854:
855: return new SpecifiedTypes();
856: }
857:
858: $tmpVars = [$issetExpr];
859: while (
860: $issetExpr instanceof ArrayDimFetch
861: || $issetExpr instanceof PropertyFetch
862: || (
863: $issetExpr instanceof StaticPropertyFetch
864: && $issetExpr->class instanceof Expr
865: )
866: ) {
867: if ($issetExpr instanceof StaticPropertyFetch) {
868: /** @var Expr $issetExpr */
869: $issetExpr = $issetExpr->class;
870: } else {
871: $issetExpr = $issetExpr->var;
872: }
873: $tmpVars[] = $issetExpr;
874: }
875: $vars = array_reverse($tmpVars);
876:
877: $types = new SpecifiedTypes();
878: foreach ($vars as $var) {
879:
880: if ($var instanceof Expr\Variable && is_string($var->name)) {
881: if ($scope->hasVariableType($var->name)->no()) {
882: return (new SpecifiedTypes([], []))->setRootExpr($expr);
883: }
884: }
885:
886: if (
887: $var instanceof ArrayDimFetch
888: && $var->dim !== null
889: && !$scope->getType($var->var) instanceof MixedType
890: ) {
891: $dimType = $scope->getType($var->dim);
892:
893: if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
894: $types = $types->unionWith(
895: $this->create(
896: $var->var,
897: new HasOffsetType($dimType),
898: $context,
899: $scope,
900: )->setRootExpr($expr),
901: );
902: } else {
903: $varType = $scope->getType($var->var);
904: $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType);
905: if ($narrowedKey !== null) {
906: $types = $types->unionWith(
907: $this->create(
908: $var->dim,
909: $narrowedKey,
910: $context,
911: $scope,
912: )->setRootExpr($expr),
913: );
914: }
915: }
916: }
917:
918: if (
919: $var instanceof PropertyFetch
920: && $var->name instanceof Node\Identifier
921: ) {
922: $types = $types->unionWith(
923: $this->create($var->var, new IntersectionType([
924: new ObjectWithoutClassType(),
925: new HasPropertyType($var->name->toString()),
926: ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr),
927: );
928: } elseif (
929: $var instanceof StaticPropertyFetch
930: && $var->class instanceof Expr
931: && $var->name instanceof Node\VarLikeIdentifier
932: ) {
933: $types = $types->unionWith(
934: $this->create($var->class, new IntersectionType([
935: new ObjectWithoutClassType(),
936: new HasPropertyType($var->name->toString()),
937: ]), TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($expr),
938: );
939: }
940:
941: $types = $types->unionWith(
942: $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr),
943: );
944: }
945:
946: return $types;
947: } elseif (
948: $expr instanceof Expr\BinaryOp\Coalesce
949: && !$context->null()
950: ) {
951: if (!$context->true()) {
952: if (!$scope instanceof MutatingScope) {
953: throw new ShouldNotHappenException();
954: }
955:
956: $isset = $scope->issetCheck($expr->left, static fn () => true);
957:
958: if ($isset !== true) {
959: return new SpecifiedTypes();
960: }
961:
962: return $this->create(
963: $expr->left,
964: new NullType(),
965: $context->negate(),
966: $scope,
967: )->setRootExpr($expr);
968: }
969:
970: if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) {
971: return $this->create(
972: $expr->left,
973: new NullType(),
974: TypeSpecifierContext::createFalse(),
975: $scope,
976: )->setRootExpr($expr);
977: }
978:
979: } elseif (
980: $expr instanceof Expr\Empty_
981: ) {
982: if (!$scope instanceof MutatingScope) {
983: throw new ShouldNotHappenException();
984: }
985:
986: $isset = $scope->issetCheck($expr->expr, static fn () => true);
987: if ($isset === false) {
988: return new SpecifiedTypes();
989: }
990:
991: return $this->specifyTypesInCondition($scope, new BooleanOr(
992: new Expr\BooleanNot(new Expr\Isset_([$expr->expr])),
993: new Expr\BooleanNot($expr->expr),
994: ), $context)->setRootExpr($expr);
995: } elseif ($expr instanceof Expr\ErrorSuppress) {
996: return $this->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr);
997: } elseif (
998: $expr instanceof Expr\Ternary
999: && !$context->null()
1000: && $scope->getType($expr->else)->isFalse()->yes()
1001: ) {
1002: $conditionExpr = $expr->cond;
1003: if ($expr->if !== null) {
1004: $conditionExpr = new BooleanAnd($conditionExpr, $expr->if);
1005: }
1006:
1007: return $this->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr);
1008:
1009: } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) {
1010: $types = $this->specifyTypesInCondition(
1011: $scope,
1012: new BooleanAnd(
1013: new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
1014: new PropertyFetch($expr->var, $expr->name),
1015: ),
1016: $context,
1017: )->setRootExpr($expr);
1018:
1019: $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
1020: return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope));
1021: } elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) {
1022: $types = $this->specifyTypesInCondition(
1023: $scope,
1024: new BooleanAnd(
1025: new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
1026: new MethodCall($expr->var, $expr->name, $expr->args),
1027: ),
1028: $context,
1029: )->setRootExpr($expr);
1030:
1031: $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
1032: return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope));
1033: } elseif (
1034: $expr instanceof Expr\New_
1035: && $expr->class instanceof Name
1036: && $this->reflectionProvider->hasClass($expr->class->toString())
1037: ) {
1038: $classReflection = $this->reflectionProvider->getClass($expr->class->toString());
1039:
1040: if ($classReflection->hasConstructor()) {
1041: $methodReflection = $classReflection->getConstructor();
1042: $asserts = $methodReflection->getAsserts();
1043:
1044: if ($asserts->getAll() !== []) {
1045: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants());
1046:
1047: $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes(
1048: $type,
1049: $parametersAcceptor->getResolvedTemplateTypeMap(),
1050: $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
1051: TemplateTypeVariance::createInvariant(),
1052: ));
1053:
1054: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
1055:
1056: if ($specifiedTypes !== null) {
1057: return $specifiedTypes;
1058: }
1059: }
1060: }
1061: } elseif (!$context->null()) {
1062: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
1063: }
1064:
1065: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1066: }
1067:
1068: private function specifyTypesForCountFuncCall(
1069: FuncCall $countFuncCall,
1070: Type $type,
1071: Type $sizeType,
1072: TypeSpecifierContext $context,
1073: Scope $scope,
1074: Expr $rootExpr,
1075: ): ?SpecifiedTypes
1076: {
1077: if (count($countFuncCall->getArgs()) === 1) {
1078: $isNormalCount = TrinaryLogic::createYes();
1079: } else {
1080: $mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1081: $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate());
1082: }
1083:
1084: $isConstantArray = $type->isConstantArray();
1085: $isList = $type->isList();
1086: $oneOrMore = IntegerRangeType::fromInterval(1, null);
1087: if (
1088: !$isNormalCount->yes()
1089: || (!$isConstantArray->yes() && !$isList->yes())
1090: || !$oneOrMore->isSuperTypeOf($sizeType)->yes()
1091: || $sizeType->isSuperTypeOf($type->getArraySize())->yes()
1092: ) {
1093: return null;
1094: }
1095:
1096: $resultTypes = [];
1097: foreach ($type->getArrays() as $arrayType) {
1098: $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize());
1099: if ($isSizeSuperTypeOfArraySize->no()) {
1100: continue;
1101: }
1102:
1103: if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) {
1104: continue;
1105: }
1106:
1107: if (
1108: $sizeType instanceof ConstantIntegerType
1109: && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1110: && $isList->yes()
1111: && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes()
1112: ) {
1113: // turn optional offsets non-optional
1114: $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1115: for ($i = 0; $i < $sizeType->getValue(); $i++) {
1116: $offsetType = new ConstantIntegerType($i);
1117: $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType));
1118: }
1119: $resultTypes[] = $valueTypesBuilder->getArray();
1120: continue;
1121: }
1122:
1123: if (
1124: $sizeType instanceof IntegerRangeType
1125: && $sizeType->getMin() !== null
1126: && $sizeType->getMin() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1127: && $isList->yes()
1128: && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($sizeType->getMax() ?? $sizeType->getMin()) - 1))->yes()
1129: ) {
1130: $builderData = [];
1131: // turn optional offsets non-optional
1132: for ($i = 0; $i < $sizeType->getMin(); $i++) {
1133: $offsetType = new ConstantIntegerType($i);
1134: $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), false];
1135: }
1136: if ($sizeType->getMax() !== null) {
1137: if ($sizeType->getMax() - $sizeType->getMin() > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1138: $resultTypes[] = $arrayType;
1139: continue;
1140: }
1141: for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
1142: $offsetType = new ConstantIntegerType($i);
1143: $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), true];
1144: }
1145: } elseif ($arrayType->isConstantArray()->yes()) {
1146: for ($i = $sizeType->getMin();; $i++) {
1147: $offsetType = new ConstantIntegerType($i);
1148: $hasOffset = $arrayType->hasOffsetValueType($offsetType);
1149: if ($hasOffset->no()) {
1150: break;
1151: }
1152: $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()];
1153: }
1154: } else {
1155: $resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
1156: continue;
1157: }
1158:
1159: if (count($builderData) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1160: $resultTypes[] = $arrayType;
1161: continue;
1162: }
1163:
1164: $builder = ConstantArrayTypeBuilder::createEmpty();
1165: foreach ($builderData as [$offsetType, $valueType, $optional]) {
1166: $builder->setOffsetValueType($offsetType, $valueType, $optional);
1167: }
1168:
1169: $resultTypes[] = $builder->getArray();
1170: continue;
1171: }
1172:
1173: $resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
1174: }
1175:
1176: return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr);
1177: }
1178:
1179: private function specifyTypesForConstantBinaryExpression(
1180: Expr $exprNode,
1181: Type $constantType,
1182: TypeSpecifierContext $context,
1183: Scope $scope,
1184: Expr $rootExpr,
1185: ): ?SpecifiedTypes
1186: {
1187: if (!$context->null() && $constantType->isFalse()->yes()) {
1188: $types = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr);
1189: if (!$context->true() && ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch)) {
1190: return $types;
1191: }
1192:
1193: return $types->unionWith($this->specifyTypesInCondition(
1194: $scope,
1195: $exprNode,
1196: $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(),
1197: )->setRootExpr($rootExpr));
1198: }
1199:
1200: if (!$context->null() && $constantType->isTrue()->yes()) {
1201: $types = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr);
1202: if (!$context->true() && ($exprNode instanceof Expr\NullsafeMethodCall || $exprNode instanceof Expr\NullsafePropertyFetch)) {
1203: return $types;
1204: }
1205:
1206: return $types->unionWith($this->specifyTypesInCondition(
1207: $scope,
1208: $exprNode,
1209: $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(),
1210: )->setRootExpr($rootExpr));
1211: }
1212:
1213: return null;
1214: }
1215:
1216: private function specifyTypesForConstantStringBinaryExpression(
1217: Expr $exprNode,
1218: Type $constantType,
1219: TypeSpecifierContext $context,
1220: Scope $scope,
1221: Expr $rootExpr,
1222: ): ?SpecifiedTypes
1223: {
1224: $scalarValues = $constantType->getConstantScalarValues();
1225: if (count($scalarValues) !== 1 || !is_string($scalarValues[0])) {
1226: return null;
1227: }
1228: $constantStringValue = $scalarValues[0];
1229:
1230: if (
1231: $exprNode instanceof FuncCall
1232: && $exprNode->name instanceof Name
1233: && strtolower($exprNode->name->toString()) === 'gettype'
1234: && isset($exprNode->getArgs()[0])
1235: ) {
1236: $type = null;
1237: if ($constantStringValue === 'string') {
1238: $type = new StringType();
1239: }
1240: if ($constantStringValue === 'array') {
1241: $type = new ArrayType(new MixedType(), new MixedType());
1242: }
1243: if ($constantStringValue === 'boolean') {
1244: $type = new BooleanType();
1245: }
1246: if (in_array($constantStringValue, ['resource', 'resource (closed)'], true)) {
1247: $type = new ResourceType();
1248: }
1249: if ($constantStringValue === 'integer') {
1250: $type = new IntegerType();
1251: }
1252: if ($constantStringValue === 'double') {
1253: $type = new FloatType();
1254: }
1255: if ($constantStringValue === 'NULL') {
1256: $type = new NullType();
1257: }
1258: if ($constantStringValue === 'object') {
1259: $type = new ObjectWithoutClassType();
1260: }
1261:
1262: if ($type !== null) {
1263: $callType = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr);
1264: $argType = $this->create($exprNode->getArgs()[0]->value, $type, $context, $scope)->setRootExpr($rootExpr);
1265: return $callType->unionWith($argType);
1266: }
1267: }
1268:
1269: if (
1270: $context->true()
1271: && $exprNode instanceof FuncCall
1272: && $exprNode->name instanceof Name
1273: && strtolower((string) $exprNode->name) === 'get_parent_class'
1274: && isset($exprNode->getArgs()[0])
1275: ) {
1276: $argType = $scope->getType($exprNode->getArgs()[0]->value);
1277: $objectType = new ObjectType($constantStringValue);
1278: $classStringType = new GenericClassStringType($objectType);
1279:
1280: if ($argType->isString()->yes()) {
1281: return $this->create(
1282: $exprNode->getArgs()[0]->value,
1283: $classStringType,
1284: $context,
1285: $scope,
1286: )->setRootExpr($rootExpr);
1287: }
1288:
1289: if ($argType->isObject()->yes()) {
1290: return $this->create(
1291: $exprNode->getArgs()[0]->value,
1292: $objectType,
1293: $context,
1294: $scope,
1295: )->setRootExpr($rootExpr);
1296: }
1297:
1298: return $this->create(
1299: $exprNode->getArgs()[0]->value,
1300: TypeCombinator::union($objectType, $classStringType),
1301: $context,
1302: $scope,
1303: )->setRootExpr($rootExpr);
1304: }
1305:
1306: return null;
1307: }
1308:
1309: private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes
1310: {
1311: if ($context->null()) {
1312: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1313: }
1314: if (!$context->truthy()) {
1315: $type = StaticTypeFactory::truthy();
1316: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
1317: } elseif (!$context->falsey()) {
1318: $type = StaticTypeFactory::falsey();
1319: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
1320: }
1321:
1322: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1323: }
1324:
1325: private function specifyTypesFromConditionalReturnType(
1326: TypeSpecifierContext $context,
1327: Expr\CallLike $call,
1328: ParametersAcceptor $parametersAcceptor,
1329: Scope $scope,
1330: ): ?SpecifiedTypes
1331: {
1332: if (!$parametersAcceptor instanceof ResolvedFunctionVariant) {
1333: return null;
1334: }
1335:
1336: $returnType = $parametersAcceptor->getOriginalParametersAcceptor()->getReturnType();
1337: if (!$returnType instanceof ConditionalTypeForParameter) {
1338: return null;
1339: }
1340:
1341: if ($context->true()) {
1342: $leftType = new ConstantBooleanType(true);
1343: $rightType = new ConstantBooleanType(false);
1344: } elseif ($context->false()) {
1345: $leftType = new ConstantBooleanType(false);
1346: $rightType = new ConstantBooleanType(true);
1347: } elseif ($context->null()) {
1348: $leftType = new MixedType();
1349: $rightType = new NeverType();
1350: } else {
1351: return null;
1352: }
1353:
1354: $argsMap = [];
1355: $parameters = $parametersAcceptor->getParameters();
1356: foreach ($call->getArgs() as $i => $arg) {
1357: if ($arg->unpack) {
1358: continue;
1359: }
1360:
1361: if ($arg->name !== null) {
1362: $paramName = $arg->name->toString();
1363: } elseif (isset($parameters[$i])) {
1364: $paramName = $parameters[$i]->getName();
1365: } else {
1366: continue;
1367: }
1368:
1369: $argsMap['$' . $paramName] = $arg->value;
1370: }
1371:
1372: return $this->getConditionalSpecifiedTypes($returnType, $leftType, $rightType, $scope, $argsMap);
1373: }
1374:
1375: /**
1376: * @param array<string, Expr> $argsMap
1377: */
1378: public function getConditionalSpecifiedTypes(
1379: ConditionalTypeForParameter $conditionalType,
1380: Type $leftType,
1381: Type $rightType,
1382: Scope $scope,
1383: array $argsMap,
1384: ): ?SpecifiedTypes
1385: {
1386: $parameterName = $conditionalType->getParameterName();
1387: if (!array_key_exists($parameterName, $argsMap)) {
1388: return null;
1389: }
1390:
1391: $targetType = $conditionalType->getTarget();
1392: $ifType = $conditionalType->getIf();
1393: $elseType = $conditionalType->getElse();
1394:
1395: if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) {
1396: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue();
1397: } elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) {
1398: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
1399: } else {
1400: return null;
1401: }
1402:
1403: $specifiedTypes = $this->create(
1404: $argsMap[$parameterName],
1405: $targetType,
1406: $context,
1407: $scope,
1408: );
1409:
1410: if ($targetType instanceof ConstantBooleanType) {
1411: if (!$targetType->getValue()) {
1412: $context = $context->negate();
1413: }
1414:
1415: $specifiedTypes = $specifiedTypes->unionWith($this->specifyTypesInCondition($scope, $argsMap[$parameterName], $context));
1416: }
1417:
1418: return $specifiedTypes;
1419: }
1420:
1421: private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\CallLike $call, Assertions $assertions, ParametersAcceptor $parametersAcceptor, Scope $scope): ?SpecifiedTypes
1422: {
1423: if ($context->null()) {
1424: $asserts = $assertions->getAsserts();
1425: } elseif ($context->true()) {
1426: $asserts = $assertions->getAssertsIfTrue();
1427: } elseif ($context->false()) {
1428: $asserts = $assertions->getAssertsIfFalse();
1429: } else {
1430: throw new ShouldNotHappenException();
1431: }
1432:
1433: if (count($asserts) === 0) {
1434: return null;
1435: }
1436:
1437: $argsMap = [];
1438: $parameters = $parametersAcceptor->getParameters();
1439: foreach ($call->getArgs() as $i => $arg) {
1440: if ($arg->unpack) {
1441: continue;
1442: }
1443:
1444: if ($arg->name !== null) {
1445: $paramName = $arg->name->toString();
1446: } elseif (isset($parameters[$i])) {
1447: $paramName = $parameters[$i]->getName();
1448: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
1449: $lastParameter = $parameters[count($parameters) - 1];
1450: $paramName = $lastParameter->getName();
1451: } else {
1452: continue;
1453: }
1454:
1455: $argsMap[$paramName][] = $arg->value;
1456: }
1457: foreach ($parameters as $parameter) {
1458: $name = $parameter->getName();
1459: $defaultValue = $parameter->getDefaultValue();
1460: if (isset($argsMap[$name]) || $defaultValue === null) {
1461: continue;
1462: }
1463: $argsMap[$name][] = new TypeExpr($defaultValue);
1464: }
1465:
1466: if ($call instanceof MethodCall) {
1467: $argsMap['this'] = [$call->var];
1468: }
1469:
1470: /** @var SpecifiedTypes|null $types */
1471: $types = null;
1472:
1473: foreach ($asserts as $assert) {
1474: foreach ($argsMap[substr($assert->getParameter()->getParameterName(), 1)] ?? [] as $parameterExpr) {
1475: $assertedType = TypeTraverser::map($assert->getType(), static function (Type $type, callable $traverse) use ($argsMap, $scope): Type {
1476: if ($type instanceof ConditionalTypeForParameter) {
1477: $parameterName = substr($type->getParameterName(), 1);
1478: if (array_key_exists($parameterName, $argsMap)) {
1479: $argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[$parameterName]));
1480: $type = $type->toConditional($argType);
1481: }
1482: }
1483:
1484: return $traverse($type);
1485: });
1486:
1487: $assertExpr = $assert->getParameter()->getExpr($parameterExpr);
1488:
1489: $templateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap();
1490: $containsUnresolvedTemplate = false;
1491: TypeTraverser::map(
1492: $assert->getOriginalType(),
1493: static function (Type $type, callable $traverse) use ($templateTypeMap, &$containsUnresolvedTemplate) {
1494: if ($type instanceof TemplateType && $type->getScope()->getClassName() !== null) {
1495: $resolvedType = $templateTypeMap->getType($type->getName());
1496: if ($resolvedType === null || $type->getBound()->equals($resolvedType)) {
1497: $containsUnresolvedTemplate = true;
1498: return $type;
1499: }
1500: }
1501:
1502: return $traverse($type);
1503: },
1504: );
1505:
1506: $newTypes = $this->create(
1507: $assertExpr,
1508: $assertedType,
1509: $assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(),
1510: $scope,
1511: )->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null);
1512: $types = $types !== null ? $types->unionWith($newTypes) : $newTypes;
1513:
1514: if (!$context->null() || !$assertedType instanceof ConstantBooleanType) {
1515: continue;
1516: }
1517:
1518: $subContext = $assertedType->getValue() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
1519: if ($assert->isNegated()) {
1520: $subContext = $subContext->negate();
1521: }
1522:
1523: $types = $types->unionWith($this->specifyTypesInCondition(
1524: $scope,
1525: $assertExpr,
1526: $subContext,
1527: ));
1528: }
1529: }
1530:
1531: return $types;
1532: }
1533:
1534: /**
1535: * @return array<string, ConditionalExpressionHolder[]>
1536: */
1537: private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array
1538: {
1539: $conditionExpressionTypes = [];
1540: foreach ($leftTypes->getSureTypes() as $exprString => [$expr, $type]) {
1541: if (!$expr instanceof Expr\Variable) {
1542: continue;
1543: }
1544: if (!is_string($expr->name)) {
1545: continue;
1546: }
1547:
1548: $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes(
1549: $expr,
1550: TypeCombinator::remove($scope->getType($expr), $type),
1551: );
1552: }
1553:
1554: if (count($conditionExpressionTypes) > 0) {
1555: $holders = [];
1556: foreach ($rightTypes->getSureTypes() as $exprString => [$expr, $type]) {
1557: if (!$expr instanceof Expr\Variable) {
1558: continue;
1559: }
1560: if (!is_string($expr->name)) {
1561: continue;
1562: }
1563:
1564: if (!isset($holders[$exprString])) {
1565: $holders[$exprString] = [];
1566: }
1567:
1568: $conditions = $conditionExpressionTypes;
1569: foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) {
1570: $conditionExpr = $conditionExprTypeHolder->getExpr();
1571: if (!$conditionExpr instanceof Expr\Variable) {
1572: continue;
1573: }
1574: if (!is_string($conditionExpr->name)) {
1575: continue;
1576: }
1577: if ($conditionExpr->name !== $expr->name) {
1578: continue;
1579: }
1580:
1581: unset($conditions[$conditionExprString]);
1582: }
1583:
1584: if (count($conditions) === 0) {
1585: continue;
1586: }
1587:
1588: $holder = new ConditionalExpressionHolder(
1589: $conditions,
1590: new ExpressionTypeHolder($expr, TypeCombinator::intersect($scope->getType($expr), $type), TrinaryLogic::createYes()),
1591: );
1592: $holders[$exprString][$holder->getKey()] = $holder;
1593: }
1594:
1595: return $holders;
1596: }
1597:
1598: return [];
1599: }
1600:
1601: /**
1602: * @return array<string, ConditionalExpressionHolder[]>
1603: */
1604: private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array
1605: {
1606: $conditionExpressionTypes = [];
1607: foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
1608: if (!$expr instanceof Expr\Variable) {
1609: continue;
1610: }
1611: if (!is_string($expr->name)) {
1612: continue;
1613: }
1614:
1615: $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes(
1616: $expr,
1617: TypeCombinator::intersect($scope->getType($expr), $type),
1618: );
1619: }
1620:
1621: if (count($conditionExpressionTypes) > 0) {
1622: $holders = [];
1623: foreach ($rightTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
1624: if (!$expr instanceof Expr\Variable) {
1625: continue;
1626: }
1627: if (!is_string($expr->name)) {
1628: continue;
1629: }
1630:
1631: if (!isset($holders[$exprString])) {
1632: $holders[$exprString] = [];
1633: }
1634:
1635: $conditions = $conditionExpressionTypes;
1636: foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) {
1637: $conditionExpr = $conditionExprTypeHolder->getExpr();
1638: if (!$conditionExpr instanceof Expr\Variable) {
1639: continue;
1640: }
1641: if (!is_string($conditionExpr->name)) {
1642: continue;
1643: }
1644: if ($conditionExpr->name !== $expr->name) {
1645: continue;
1646: }
1647:
1648: unset($conditions[$conditionExprString]);
1649: }
1650:
1651: if (count($conditions) === 0) {
1652: continue;
1653: }
1654:
1655: $holder = new ConditionalExpressionHolder(
1656: $conditions,
1657: new ExpressionTypeHolder($expr, TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()),
1658: );
1659: $holders[$exprString][$holder->getKey()] = $holder;
1660: }
1661:
1662: return $holders;
1663: }
1664:
1665: return [];
1666: }
1667:
1668: /**
1669: * @return array{Expr, ConstantScalarType, Type}|null
1670: */
1671: private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array
1672: {
1673: $leftType = $scope->getType($binaryOperation->left);
1674: $rightType = $scope->getType($binaryOperation->right);
1675:
1676: $rightExpr = $binaryOperation->right;
1677: if ($rightExpr instanceof AlwaysRememberedExpr) {
1678: $rightExpr = $rightExpr->getExpr();
1679: }
1680:
1681: $leftExpr = $binaryOperation->left;
1682: if ($leftExpr instanceof AlwaysRememberedExpr) {
1683: $leftExpr = $leftExpr->getExpr();
1684: }
1685:
1686: if (
1687: $leftType instanceof ConstantScalarType
1688: && !$rightExpr instanceof ConstFetch
1689: ) {
1690: return [$binaryOperation->right, $leftType, $rightType];
1691: } elseif (
1692: $rightType instanceof ConstantScalarType
1693: && !$leftExpr instanceof ConstFetch
1694: ) {
1695: return [$binaryOperation->left, $rightType, $leftType];
1696: }
1697:
1698: return null;
1699: }
1700:
1701: /** @api */
1702: public function create(
1703: Expr $expr,
1704: Type $type,
1705: TypeSpecifierContext $context,
1706: Scope $scope,
1707: ): SpecifiedTypes
1708: {
1709: if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) {
1710: return (new SpecifiedTypes([], []))->setRootExpr($expr);
1711: }
1712:
1713: $specifiedExprs = [];
1714: if ($expr instanceof AlwaysRememberedExpr) {
1715: $specifiedExprs[] = $expr;
1716: $expr = $expr->expr;
1717: }
1718:
1719: if ($expr instanceof Expr\Assign) {
1720: $specifiedExprs[] = $expr->var;
1721: $specifiedExprs[] = $expr->expr;
1722:
1723: while ($expr->expr instanceof Expr\Assign) {
1724: $specifiedExprs[] = $expr->expr->var;
1725: $expr = $expr->expr;
1726: }
1727: } elseif ($expr instanceof Expr\AssignOp\Coalesce) {
1728: $specifiedExprs[] = $expr->var;
1729: } else {
1730: $specifiedExprs[] = $expr;
1731: }
1732:
1733: $types = null;
1734:
1735: foreach ($specifiedExprs as $specifiedExpr) {
1736: $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $scope);
1737:
1738: if ($types === null) {
1739: $types = $newTypes;
1740: } else {
1741: $types = $types->unionWith($newTypes);
1742: }
1743: }
1744:
1745: return $types;
1746: }
1747:
1748: private function createForExpr(
1749: Expr $expr,
1750: Type $type,
1751: TypeSpecifierContext $context,
1752: Scope $scope,
1753: ): SpecifiedTypes
1754: {
1755: if ($context->true()) {
1756: $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no();
1757: } elseif ($context->false()) {
1758: $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no();
1759: }
1760:
1761: $originalExpr = $expr;
1762: if (isset($containsNull) && !$containsNull) {
1763: $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr);
1764: }
1765:
1766: if (
1767: !$context->null()
1768: && $expr instanceof Expr\BinaryOp\Coalesce
1769: ) {
1770: $rightIsSuperType = $type->isSuperTypeOf($scope->getType($expr->right));
1771: if (($context->true() && $rightIsSuperType->no()) || ($context->false() && $rightIsSuperType->yes())) {
1772: $expr = $expr->left;
1773: }
1774: }
1775:
1776: if (
1777: $expr instanceof FuncCall
1778: && $expr->name instanceof Name
1779: ) {
1780: $has = $this->reflectionProvider->hasFunction($expr->name, $scope);
1781: if (!$has) {
1782: // backwards compatibility with previous behaviour
1783: return new SpecifiedTypes([], []);
1784: }
1785:
1786: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
1787: $hasSideEffects = $functionReflection->hasSideEffects();
1788: if ($hasSideEffects->yes()) {
1789: return new SpecifiedTypes([], []);
1790: }
1791:
1792: if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
1793: return new SpecifiedTypes([], []);
1794: }
1795: }
1796:
1797: if (
1798: $expr instanceof MethodCall
1799: && $expr->name instanceof Node\Identifier
1800: ) {
1801: $methodName = $expr->name->toString();
1802: $calledOnType = $scope->getType($expr->var);
1803: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
1804: if (
1805: $methodReflection === null
1806: || $methodReflection->hasSideEffects()->yes()
1807: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
1808: ) {
1809: if (isset($containsNull) && !$containsNull) {
1810: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
1811: }
1812:
1813: return new SpecifiedTypes([], []);
1814: }
1815: }
1816:
1817: if (
1818: $expr instanceof StaticCall
1819: && $expr->name instanceof Node\Identifier
1820: ) {
1821: $methodName = $expr->name->toString();
1822: if ($expr->class instanceof Name) {
1823: $calledOnType = $scope->resolveTypeByName($expr->class);
1824: } else {
1825: $calledOnType = $scope->getType($expr->class);
1826: }
1827:
1828: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
1829: if (
1830: $methodReflection === null
1831: || $methodReflection->hasSideEffects()->yes()
1832: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
1833: ) {
1834: if (isset($containsNull) && !$containsNull) {
1835: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
1836: }
1837:
1838: return new SpecifiedTypes([], []);
1839: }
1840: }
1841:
1842: $sureTypes = [];
1843: $sureNotTypes = [];
1844: $exprString = $this->exprPrinter->printExpr($expr);
1845: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
1846: if ($context->false()) {
1847: $sureNotTypes[$exprString] = [$expr, $type];
1848: if ($exprString !== $originalExprString) {
1849: $sureNotTypes[$originalExprString] = [$originalExpr, $type];
1850: }
1851: } elseif ($context->true()) {
1852: $sureTypes[$exprString] = [$expr, $type];
1853: if ($exprString !== $originalExprString) {
1854: $sureTypes[$originalExprString] = [$originalExpr, $type];
1855: }
1856: }
1857:
1858: $types = new SpecifiedTypes($sureTypes, $sureNotTypes);
1859: if (isset($containsNull) && !$containsNull) {
1860: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types);
1861: }
1862:
1863: return $types;
1864: }
1865:
1866: private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes
1867: {
1868: if ($expr instanceof Expr\NullsafePropertyFetch) {
1869: if ($type !== null) {
1870: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, $scope);
1871: } else {
1872: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), $scope);
1873: }
1874:
1875: return $propertyFetchTypes->unionWith(
1876: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
1877: );
1878: }
1879:
1880: if ($expr instanceof Expr\NullsafeMethodCall) {
1881: if ($type !== null) {
1882: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $scope);
1883: } else {
1884: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $scope);
1885: }
1886:
1887: return $methodCallTypes->unionWith(
1888: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
1889: );
1890: }
1891:
1892: if ($expr instanceof Expr\PropertyFetch) {
1893: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
1894: }
1895:
1896: if ($expr instanceof Expr\MethodCall) {
1897: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
1898: }
1899:
1900: if ($expr instanceof Expr\ArrayDimFetch) {
1901: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
1902: }
1903:
1904: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
1905: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
1906: }
1907:
1908: if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
1909: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
1910: }
1911:
1912: return new SpecifiedTypes([], []);
1913: }
1914:
1915: private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
1916: {
1917: $sureNotTypes = [];
1918:
1919: if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) {
1920: $exprString = $this->exprPrinter->printExpr($expr);
1921: if ($context->false()) {
1922: $sureNotTypes[$exprString] = [$expr, $type];
1923: } elseif ($context->true()) {
1924: $inverted = TypeCombinator::remove(new IntegerType(), $type);
1925: $sureNotTypes[$exprString] = [$expr, $inverted];
1926: }
1927: }
1928:
1929: return (new SpecifiedTypes(sureNotTypes: $sureNotTypes))->setRootExpr($rootExpr);
1930: }
1931:
1932: /**
1933: * @return FunctionTypeSpecifyingExtension[]
1934: */
1935: private function getFunctionTypeSpecifyingExtensions(): array
1936: {
1937: return $this->functionTypeSpecifyingExtensions;
1938: }
1939:
1940: /**
1941: * @return MethodTypeSpecifyingExtension[]
1942: */
1943: private function getMethodTypeSpecifyingExtensionsForClass(string $className): array
1944: {
1945: if ($this->methodTypeSpecifyingExtensionsByClass === null) {
1946: $byClass = [];
1947: foreach ($this->methodTypeSpecifyingExtensions as $extension) {
1948: $byClass[$extension->getClass()][] = $extension;
1949: }
1950:
1951: $this->methodTypeSpecifyingExtensionsByClass = $byClass;
1952: }
1953: return $this->getTypeSpecifyingExtensionsForType($this->methodTypeSpecifyingExtensionsByClass, $className);
1954: }
1955:
1956: /**
1957: * @return StaticMethodTypeSpecifyingExtension[]
1958: */
1959: private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
1960: {
1961: if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
1962: $byClass = [];
1963: foreach ($this->staticMethodTypeSpecifyingExtensions as $extension) {
1964: $byClass[$extension->getClass()][] = $extension;
1965: }
1966:
1967: $this->staticMethodTypeSpecifyingExtensionsByClass = $byClass;
1968: }
1969: return $this->getTypeSpecifyingExtensionsForType($this->staticMethodTypeSpecifyingExtensionsByClass, $className);
1970: }
1971:
1972: /**
1973: * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions
1974: * @return mixed[]
1975: */
1976: private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array
1977: {
1978: $extensionsForClass = [[]];
1979: $class = $this->reflectionProvider->getClass($className);
1980: foreach (array_merge([$className], $class->getParentClassesNames(), $class->getNativeReflection()->getInterfaceNames()) as $extensionClassName) {
1981: if (!isset($extensions[$extensionClassName])) {
1982: continue;
1983: }
1984:
1985: $extensionsForClass[] = $extensions[$extensionClassName];
1986: }
1987:
1988: return array_merge(...$extensionsForClass);
1989: }
1990:
1991: public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
1992: {
1993: $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
1994: if ($expressions !== null) {
1995: $exprNode = $expressions[0];
1996: $constantType = $expressions[1];
1997: $otherType = $expressions[2];
1998:
1999: if (!$context->null() && $constantType->getValue() === null) {
2000: $trueTypes = [
2001: new NullType(),
2002: new ConstantBooleanType(false),
2003: new ConstantIntegerType(0),
2004: new ConstantFloatType(0.0),
2005: new ConstantStringType(''),
2006: new ConstantArrayType([], []),
2007: ];
2008: return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr);
2009: }
2010:
2011: if (!$context->null() && $constantType->getValue() === false) {
2012: return $this->specifyTypesInCondition(
2013: $scope,
2014: $exprNode,
2015: $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(),
2016: )->setRootExpr($expr);
2017: }
2018:
2019: if (!$context->null() && $constantType->getValue() === true) {
2020: return $this->specifyTypesInCondition(
2021: $scope,
2022: $exprNode,
2023: $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(),
2024: )->setRootExpr($expr);
2025: }
2026:
2027: if (!$context->null() && $constantType->getValue() === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) {
2028: /* There is a difference between php 7.x and 8.x on the equality
2029: * behavior between zero and the empty string, so to be conservative
2030: * we leave it untouched regardless of the language version */
2031: if ($context->true()) {
2032: $trueTypes = [
2033: new NullType(),
2034: new ConstantBooleanType(false),
2035: new ConstantIntegerType(0),
2036: new ConstantFloatType(0.0),
2037: new StringType(),
2038: ];
2039: } else {
2040: $trueTypes = [
2041: new NullType(),
2042: new ConstantBooleanType(false),
2043: new ConstantIntegerType(0),
2044: new ConstantFloatType(0.0),
2045: new ConstantStringType('0'),
2046: ];
2047: }
2048: return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr);
2049: }
2050:
2051: if (!$context->null() && $constantType->getValue() === '') {
2052: /* There is a difference between php 7.x and 8.x on the equality
2053: * behavior between zero and the empty string, so to be conservative
2054: * we leave it untouched regardless of the language version */
2055: if ($context->true()) {
2056: $trueTypes = [
2057: new NullType(),
2058: new ConstantBooleanType(false),
2059: new ConstantIntegerType(0),
2060: new ConstantFloatType(0.0),
2061: new ConstantStringType(''),
2062: ];
2063: } else {
2064: $trueTypes = [
2065: new NullType(),
2066: new ConstantBooleanType(false),
2067: new ConstantStringType(''),
2068: ];
2069: }
2070: return $this->create($exprNode, new UnionType($trueTypes), $context, $scope)->setRootExpr($expr);
2071: }
2072:
2073: if (
2074: $exprNode instanceof FuncCall
2075: && $exprNode->name instanceof Name
2076: && in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true)
2077: && isset($exprNode->getArgs()[0])
2078: && $constantType->isString()->yes()
2079: ) {
2080: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2081: }
2082:
2083: if (
2084: $context->true()
2085: && $exprNode instanceof FuncCall
2086: && $exprNode->name instanceof Name
2087: && $exprNode->name->toLowerString() === 'preg_match'
2088: && (new ConstantIntegerType(1))->isSuperTypeOf($constantType)->yes()
2089: ) {
2090: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2091: }
2092:
2093: if (
2094: $context->true()
2095: && $exprNode instanceof ClassConstFetch
2096: && $exprNode->name instanceof Node\Identifier
2097: && strtolower($exprNode->name->toString()) === 'class'
2098: && $constantType->isString()->yes()
2099: ) {
2100: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2101: }
2102: }
2103:
2104: $leftType = $scope->getType($expr->left);
2105: $rightType = $scope->getType($expr->right);
2106:
2107: $leftBooleanType = $leftType->toBoolean();
2108: if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) {
2109: return $this->specifyTypesInCondition(
2110: $scope,
2111: new Expr\BinaryOp\Identical(
2112: new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')),
2113: $expr->right,
2114: ),
2115: $context,
2116: )->setRootExpr($expr);
2117: }
2118:
2119: $rightBooleanType = $rightType->toBoolean();
2120: if ($rightBooleanType instanceof ConstantBooleanType && $leftType->isBoolean()->yes()) {
2121: return $this->specifyTypesInCondition(
2122: $scope,
2123: new Expr\BinaryOp\Identical(
2124: $expr->left,
2125: new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')),
2126: ),
2127: $context,
2128: )->setRootExpr($expr);
2129: }
2130:
2131: if (
2132: !$context->null()
2133: && $rightType->isArray()->yes()
2134: && $leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no()
2135: ) {
2136: return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr);
2137: }
2138:
2139: if (
2140: !$context->null()
2141: && $leftType->isArray()->yes()
2142: && $rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no()
2143: ) {
2144: return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr);
2145: }
2146:
2147: if (
2148: ($leftType->isString()->yes() && $rightType->isString()->yes())
2149: || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes())
2150: || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes())
2151: || ($leftType->isEnum()->yes() && $rightType->isEnum()->yes())
2152: ) {
2153: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
2154: }
2155:
2156: $leftExprString = $this->exprPrinter->printExpr($expr->left);
2157: $rightExprString = $this->exprPrinter->printExpr($expr->right);
2158: if ($leftExprString === $rightExprString) {
2159: if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) {
2160: return (new SpecifiedTypes([], []))->setRootExpr($expr);
2161: }
2162: }
2163:
2164: $leftTypes = $this->create($expr->left, $leftType, $context, $scope)->setRootExpr($expr);
2165: $rightTypes = $this->create($expr->right, $rightType, $context, $scope)->setRootExpr($expr);
2166:
2167: return $context->true()
2168: ? $leftTypes->unionWith($rightTypes)
2169: : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope));
2170: }
2171:
2172: public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
2173: {
2174: // Normalize to: fn() === expr
2175: $leftExpr = $expr->left;
2176: $rightExpr = $expr->right;
2177: if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) {
2178: [$leftExpr, $rightExpr] = [$rightExpr, $leftExpr];
2179: }
2180:
2181: $unwrappedLeftExpr = $leftExpr;
2182: if ($leftExpr instanceof AlwaysRememberedExpr) {
2183: $unwrappedLeftExpr = $leftExpr->getExpr();
2184: }
2185: $unwrappedRightExpr = $rightExpr;
2186: if ($rightExpr instanceof AlwaysRememberedExpr) {
2187: $unwrappedRightExpr = $rightExpr->getExpr();
2188: }
2189:
2190: $rightType = $scope->getType($rightExpr);
2191:
2192: // (count($a) === $b)
2193: if (
2194: !$context->null()
2195: && $unwrappedLeftExpr instanceof FuncCall
2196: && count($unwrappedLeftExpr->getArgs()) >= 1
2197: && $unwrappedLeftExpr->name instanceof Name
2198: && in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
2199: && $rightType->isInteger()->yes()
2200: ) {
2201: if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
2202: return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr);
2203: }
2204:
2205: $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
2206: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
2207: if ($isZero->yes()) {
2208: $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2209:
2210: if ($context->truthy() && !$argType->isArray()->yes()) {
2211: $newArgType = new UnionType([
2212: new ObjectType(Countable::class),
2213: new ConstantArrayType([], []),
2214: ]);
2215: } else {
2216: $newArgType = new ConstantArrayType([], []);
2217: }
2218:
2219: return $funcTypes->unionWith(
2220: $this->create($unwrappedLeftExpr->getArgs()[0]->value, $newArgType, $context, $scope)->setRootExpr($expr),
2221: );
2222: }
2223:
2224: $specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
2225: if ($specifiedTypes !== null) {
2226: return $specifiedTypes;
2227: }
2228:
2229: if ($context->truthy() && $argType->isArray()->yes()) {
2230: $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2231: if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
2232: return $funcTypes->unionWith(
2233: $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr),
2234: );
2235: }
2236:
2237: return $funcTypes;
2238: }
2239: }
2240:
2241: // strlen($a) === $b
2242: if (
2243: !$context->null()
2244: && $unwrappedLeftExpr instanceof FuncCall
2245: && count($unwrappedLeftExpr->getArgs()) === 1
2246: && $unwrappedLeftExpr->name instanceof Name
2247: && in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true)
2248: && $rightType->isInteger()->yes()
2249: ) {
2250: if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
2251: return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr);
2252: }
2253:
2254: $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
2255: if ($isZero->yes()) {
2256: $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2257: return $funcTypes->unionWith(
2258: $this->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), $context, $scope)->setRootExpr($expr),
2259: );
2260: }
2261:
2262: if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
2263: $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
2264: if ($argType->isString()->yes()) {
2265: $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2266:
2267: $accessory = new AccessoryNonEmptyStringType();
2268: if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($rightType)->yes()) {
2269: $accessory = new AccessoryNonFalsyStringType();
2270: }
2271: $valueTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr);
2272:
2273: return $funcTypes->unionWith($valueTypes);
2274: }
2275: }
2276: }
2277:
2278: // preg_match($a) === $b
2279: if (
2280: $context->true()
2281: && $unwrappedLeftExpr instanceof FuncCall
2282: && $unwrappedLeftExpr->name instanceof Name
2283: && $unwrappedLeftExpr->name->toLowerString() === 'preg_match'
2284: && (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes()
2285: ) {
2286: return $this->specifyTypesInCondition(
2287: $scope,
2288: $leftExpr,
2289: $context,
2290: )->setRootExpr($expr);
2291: }
2292:
2293: // get_class($a) === 'Foo'
2294: if (
2295: $context->true()
2296: && $unwrappedLeftExpr instanceof FuncCall
2297: && $unwrappedLeftExpr->name instanceof Name
2298: && in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true)
2299: && isset($unwrappedLeftExpr->getArgs()[0])
2300: ) {
2301: if ($rightType instanceof ConstantStringType && $this->reflectionProvider->hasClass($rightType->getValue())) {
2302: return $this->create(
2303: $unwrappedLeftExpr->getArgs()[0]->value,
2304: new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()),
2305: $context,
2306: $scope,
2307: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
2308: }
2309: if ($rightType->getClassStringObjectType()->isObject()->yes()) {
2310: return $this->create(
2311: $unwrappedLeftExpr->getArgs()[0]->value,
2312: $rightType->getClassStringObjectType(),
2313: $context,
2314: $scope,
2315: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
2316: }
2317: }
2318:
2319: if (
2320: $context->truthy()
2321: && $unwrappedLeftExpr instanceof FuncCall
2322: && $unwrappedLeftExpr->name instanceof Name
2323: && in_array(strtolower($unwrappedLeftExpr->name->toString()), [
2324: 'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst',
2325: 'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst',
2326: 'ucwords', 'mb_convert_case', 'mb_convert_kana',
2327: ], true)
2328: && isset($unwrappedLeftExpr->getArgs()[0])
2329: && $rightType->isNonEmptyString()->yes()
2330: ) {
2331: $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
2332:
2333: if ($argType->isString()->yes()) {
2334: if ($rightType->isNonFalsyString()->yes()) {
2335: return $this->create(
2336: $unwrappedLeftExpr->getArgs()[0]->value,
2337: TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()),
2338: $context,
2339: $scope,
2340: )->setRootExpr($expr);
2341: }
2342:
2343: return $this->create(
2344: $unwrappedLeftExpr->getArgs()[0]->value,
2345: TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()),
2346: $context,
2347: $scope,
2348: )->setRootExpr($expr);
2349: }
2350: }
2351:
2352: if ($rightType->isString()->yes()) {
2353: $types = null;
2354: foreach ($rightType->getConstantStrings() as $constantString) {
2355: $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $expr);
2356:
2357: if ($specifiedType === null) {
2358: continue;
2359: }
2360: if ($types === null) {
2361: $types = $specifiedType;
2362: continue;
2363: }
2364:
2365: $types = $types->intersectWith($specifiedType);
2366: }
2367:
2368: if ($types !== null) {
2369: if ($leftExpr !== $unwrappedLeftExpr) {
2370: $types = $types->unionWith($this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr));
2371: }
2372: return $types;
2373: }
2374: }
2375:
2376: $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
2377: if ($expressions !== null) {
2378: $exprNode = $expressions[0];
2379: $constantType = $expressions[1];
2380:
2381: $unwrappedExprNode = $exprNode;
2382: if ($exprNode instanceof AlwaysRememberedExpr) {
2383: $unwrappedExprNode = $exprNode->getExpr();
2384: }
2385:
2386: $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedExprNode, $constantType, $context, $scope, $expr);
2387: if ($specifiedType !== null) {
2388: if ($exprNode !== $unwrappedExprNode) {
2389: $specifiedType = $specifiedType->unionWith(
2390: $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($expr),
2391: );
2392: }
2393: return $specifiedType;
2394: }
2395: }
2396:
2397: // $a::class === 'Foo'
2398: if (
2399: $context->true() &&
2400: $unwrappedLeftExpr instanceof ClassConstFetch &&
2401: $unwrappedLeftExpr->class instanceof Expr &&
2402: $unwrappedLeftExpr->name instanceof Node\Identifier &&
2403: $unwrappedRightExpr instanceof ClassConstFetch &&
2404: $rightType instanceof ConstantStringType &&
2405: $rightType->getValue() !== '' &&
2406: strtolower($unwrappedLeftExpr->name->toString()) === 'class'
2407: ) {
2408: if ($this->reflectionProvider->hasClass($rightType->getValue())) {
2409: return $this->create(
2410: $unwrappedLeftExpr->class,
2411: new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()),
2412: $context,
2413: $scope,
2414: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
2415: }
2416: return $this->specifyTypesInCondition(
2417: $scope,
2418: new Instanceof_(
2419: $unwrappedLeftExpr->class,
2420: new Name($rightType->getValue()),
2421: ),
2422: $context,
2423: )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr);
2424: }
2425:
2426: $leftType = $scope->getType($leftExpr);
2427:
2428: // 'Foo' === $a::class
2429: if (
2430: $context->true() &&
2431: $unwrappedRightExpr instanceof ClassConstFetch &&
2432: $unwrappedRightExpr->class instanceof Expr &&
2433: $unwrappedRightExpr->name instanceof Node\Identifier &&
2434: $unwrappedLeftExpr instanceof ClassConstFetch &&
2435: $leftType instanceof ConstantStringType &&
2436: $leftType->getValue() !== '' &&
2437: strtolower($unwrappedRightExpr->name->toString()) === 'class'
2438: ) {
2439: if ($this->reflectionProvider->hasClass($leftType->getValue())) {
2440: return $this->create(
2441: $unwrappedRightExpr->class,
2442: new ObjectType($leftType->getValue(), classReflection: $this->reflectionProvider->getClass($leftType->getValue())->asFinal()),
2443: $context,
2444: $scope,
2445: )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr));
2446: }
2447:
2448: return $this->specifyTypesInCondition(
2449: $scope,
2450: new Instanceof_(
2451: $unwrappedRightExpr->class,
2452: new Name($leftType->getValue()),
2453: ),
2454: $context,
2455: )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr));
2456: }
2457:
2458: if ($context->false()) {
2459: $identicalType = $scope->getType($expr);
2460: if ($identicalType instanceof ConstantBooleanType) {
2461: $never = new NeverType();
2462: $contextForTypes = $identicalType->getValue() ? $context->negate() : $context;
2463: $leftTypes = $this->create($leftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
2464: $rightTypes = $this->create($rightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
2465: if ($leftExpr instanceof AlwaysRememberedExpr) {
2466: $leftTypes = $leftTypes->unionWith(
2467: $this->create($unwrappedLeftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr),
2468: );
2469: }
2470: if ($rightExpr instanceof AlwaysRememberedExpr) {
2471: $rightTypes = $rightTypes->unionWith(
2472: $this->create($unwrappedRightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr),
2473: );
2474: }
2475: return $leftTypes->unionWith($rightTypes);
2476: }
2477: }
2478:
2479: $types = null;
2480: if (
2481: count($leftType->getFiniteTypes()) === 1
2482: || (
2483: $context->true()
2484: && $leftType->isConstantValue()->yes()
2485: && !$rightType->equals($leftType)
2486: && $rightType->isSuperTypeOf($leftType)->yes())
2487: ) {
2488: $types = $this->create(
2489: $rightExpr,
2490: $leftType,
2491: $context,
2492: $scope,
2493: )->setRootExpr($expr);
2494: if ($rightExpr instanceof AlwaysRememberedExpr) {
2495: $types = $types->unionWith($this->create(
2496: $unwrappedRightExpr,
2497: $leftType,
2498: $context,
2499: $scope,
2500: ))->setRootExpr($expr);
2501: }
2502: }
2503: if (
2504: count($rightType->getFiniteTypes()) === 1
2505: || (
2506: $context->true()
2507: && $rightType->isConstantValue()->yes()
2508: && !$leftType->equals($rightType)
2509: && $leftType->isSuperTypeOf($rightType)->yes()
2510: )
2511: ) {
2512: $leftTypes = $this->create(
2513: $leftExpr,
2514: $rightType,
2515: $context,
2516: $scope,
2517: )->setRootExpr($expr);
2518: if ($leftExpr instanceof AlwaysRememberedExpr) {
2519: $leftTypes = $leftTypes->unionWith($this->create(
2520: $unwrappedLeftExpr,
2521: $rightType,
2522: $context,
2523: $scope,
2524: ))->setRootExpr($expr);
2525: }
2526: if ($types !== null) {
2527: $types = $types->unionWith($leftTypes);
2528: } else {
2529: $types = $leftTypes;
2530: }
2531: }
2532:
2533: if ($types !== null) {
2534: return $types;
2535: }
2536:
2537: $leftExprString = $this->exprPrinter->printExpr($unwrappedLeftExpr);
2538: $rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr);
2539: if ($leftExprString === $rightExprString) {
2540: if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) {
2541: return (new SpecifiedTypes([], []))->setRootExpr($expr);
2542: }
2543: }
2544:
2545: if ($context->true()) {
2546: $leftTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2547: $rightTypes = $this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr);
2548: if ($leftExpr instanceof AlwaysRememberedExpr) {
2549: $leftTypes = $leftTypes->unionWith(
2550: $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr),
2551: );
2552: }
2553: if ($rightExpr instanceof AlwaysRememberedExpr) {
2554: $rightTypes = $rightTypes->unionWith(
2555: $this->create($unwrappedRightExpr, $leftType, $context, $scope)->setRootExpr($expr),
2556: );
2557: }
2558: return $leftTypes->unionWith($rightTypes);
2559: } elseif ($context->false()) {
2560: return $this->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope)
2561: ->intersectWith($this->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope));
2562: }
2563:
2564: return (new SpecifiedTypes([], []))->setRootExpr($expr);
2565: }
2566:
2567: }
2568: