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