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