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