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