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