1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node;
6: use PhpParser\Node\Expr;
7: use PhpParser\Node\Expr\ArrayDimFetch;
8: use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
9: use PhpParser\Node\Expr\BinaryOp\BooleanOr;
10: use PhpParser\Node\Expr\BinaryOp\LogicalAnd;
11: use PhpParser\Node\Expr\BinaryOp\LogicalOr;
12: use PhpParser\Node\Expr\ClassConstFetch;
13: use PhpParser\Node\Expr\ConstFetch;
14: use PhpParser\Node\Expr\FuncCall;
15: use PhpParser\Node\Expr\Instanceof_;
16: use PhpParser\Node\Expr\MethodCall;
17: use PhpParser\Node\Expr\PropertyFetch;
18: use PhpParser\Node\Expr\StaticCall;
19: use PhpParser\Node\Expr\StaticPropertyFetch;
20: use PhpParser\Node\Name;
21: use PHPStan\Node\Printer\ExprPrinter;
22: use PHPStan\Reflection\Assertions;
23: use PHPStan\Reflection\ParametersAcceptor;
24: use PHPStan\Reflection\ParametersAcceptorSelector;
25: use PHPStan\Reflection\ReflectionProvider;
26: use PHPStan\Reflection\ResolvedFunctionVariant;
27: use PHPStan\ShouldNotHappenException;
28: use PHPStan\TrinaryLogic;
29: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
30: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
31: use PHPStan\Type\Accessory\HasOffsetType;
32: use PHPStan\Type\Accessory\HasPropertyType;
33: use PHPStan\Type\Accessory\NonEmptyArrayType;
34: use PHPStan\Type\ArrayType;
35: use PHPStan\Type\BooleanType;
36: use PHPStan\Type\ConditionalTypeForParameter;
37: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
38: use PHPStan\Type\Constant\ConstantBooleanType;
39: use PHPStan\Type\Constant\ConstantIntegerType;
40: use PHPStan\Type\Constant\ConstantStringType;
41: use PHPStan\Type\ConstantScalarType;
42: use PHPStan\Type\ConstantType;
43: use PHPStan\Type\Enum\EnumCaseObjectType;
44: use PHPStan\Type\FloatType;
45: use PHPStan\Type\FunctionTypeSpecifyingExtension;
46: use PHPStan\Type\Generic\GenericClassStringType;
47: use PHPStan\Type\Generic\TemplateType;
48: use PHPStan\Type\Generic\TemplateTypeHelper;
49: use PHPStan\Type\IntegerRangeType;
50: use PHPStan\Type\IntegerType;
51: use PHPStan\Type\IntersectionType;
52: use PHPStan\Type\MethodTypeSpecifyingExtension;
53: use PHPStan\Type\MixedType;
54: use PHPStan\Type\NeverType;
55: use PHPStan\Type\NonexistentParentClassType;
56: use PHPStan\Type\NullType;
57: use PHPStan\Type\ObjectType;
58: use PHPStan\Type\ObjectWithoutClassType;
59: use PHPStan\Type\ResourceType;
60: use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
61: use PHPStan\Type\StaticType;
62: use PHPStan\Type\StaticTypeFactory;
63: use PHPStan\Type\StringType;
64: use PHPStan\Type\Type;
65: use PHPStan\Type\TypeCombinator;
66: use PHPStan\Type\TypeTraverser;
67: use PHPStan\Type\TypeUtils;
68: use PHPStan\Type\TypeWithClassName;
69: use PHPStan\Type\UnionType;
70: use function array_filter;
71: use function array_key_exists;
72: use function array_map;
73: use function array_merge;
74: use function array_reduce;
75: use function array_reverse;
76: use function count;
77: use function in_array;
78: use function is_string;
79: use function strtolower;
80: use function substr;
81:
82: class TypeSpecifier
83: {
84:
85: /** @var MethodTypeSpecifyingExtension[][]|null */
86: private ?array $methodTypeSpecifyingExtensionsByClass = null;
87:
88: /** @var StaticMethodTypeSpecifyingExtension[][]|null */
89: private ?array $staticMethodTypeSpecifyingExtensionsByClass = null;
90:
91: /**
92: * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions
93: * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions
94: * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions
95: */
96: public function __construct(
97: private ExprPrinter $exprPrinter,
98: private ReflectionProvider $reflectionProvider,
99: private array $functionTypeSpecifyingExtensions,
100: private array $methodTypeSpecifyingExtensions,
101: private array $staticMethodTypeSpecifyingExtensions,
102: private bool $rememberPossiblyImpureFunctionValues,
103: )
104: {
105: foreach (array_merge($functionTypeSpecifyingExtensions, $methodTypeSpecifyingExtensions, $staticMethodTypeSpecifyingExtensions) as $extension) {
106: if (!($extension instanceof TypeSpecifierAwareExtension)) {
107: continue;
108: }
109:
110: $extension->setTypeSpecifier($this);
111: }
112: }
113:
114: /** @api */
115: public function specifyTypesInCondition(
116: Scope $scope,
117: Expr $expr,
118: TypeSpecifierContext $context,
119: ?Expr $rootExpr = null,
120: ): SpecifiedTypes
121: {
122: $rootExpr ??= $expr;
123:
124: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
125: return new SpecifiedTypes([], [], false, [], $rootExpr);
126: }
127:
128: if ($expr instanceof Instanceof_) {
129: $exprNode = $expr->expr;
130: if ($expr->class instanceof Name) {
131: $className = (string) $expr->class;
132: $lowercasedClassName = strtolower($className);
133: if ($lowercasedClassName === 'self' && $scope->isInClass()) {
134: $type = new ObjectType($scope->getClassReflection()->getName());
135: } elseif ($lowercasedClassName === 'static' && $scope->isInClass()) {
136: $type = new StaticType($scope->getClassReflection());
137: } elseif ($lowercasedClassName === 'parent') {
138: if (
139: $scope->isInClass()
140: && $scope->getClassReflection()->getParentClass() !== null
141: ) {
142: $type = new ObjectType($scope->getClassReflection()->getParentClass()->getName());
143: } else {
144: $type = new NonexistentParentClassType();
145: }
146: } else {
147: $type = new ObjectType($className);
148: }
149: return $this->create($exprNode, $type, $context, false, $scope, $rootExpr);
150: }
151:
152: $classType = $scope->getType($expr->class);
153: $type = TypeTraverser::map($classType, static function (Type $type, callable $traverse): Type {
154: if ($type instanceof UnionType || $type instanceof IntersectionType) {
155: return $traverse($type);
156: }
157: if ($type instanceof TypeWithClassName) {
158: return $type;
159: }
160: if ($type instanceof GenericClassStringType) {
161: return $type->getGenericType();
162: }
163: if ($type instanceof ConstantStringType) {
164: return new ObjectType($type->getValue());
165: }
166: return new MixedType();
167: });
168:
169: if (!$type->isSuperTypeOf(new MixedType())->yes()) {
170: if ($context->true()) {
171: $type = TypeCombinator::intersect(
172: $type,
173: new ObjectWithoutClassType(),
174: );
175: return $this->create($exprNode, $type, $context, false, $scope, $rootExpr);
176: } elseif ($context->false()) {
177: $exprType = $scope->getType($expr->expr);
178: if (!$type->isSuperTypeOf($exprType)->yes()) {
179: return $this->create($exprNode, $type, $context, false, $scope, $rootExpr);
180: }
181: }
182: }
183: if ($context->true()) {
184: return $this->create($exprNode, new ObjectWithoutClassType(), $context, false, $scope, $rootExpr);
185: }
186: } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) {
187: $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
188: if ($expressions !== null) {
189: /** @var Expr $exprNode */
190: $exprNode = $expressions[0];
191: /** @var ConstantScalarType $constantType */
192: $constantType = $expressions[1];
193:
194: $specifiedType = $this->specifyTypesForConstantBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr);
195: if ($specifiedType !== null) {
196: return $specifiedType;
197: }
198: }
199:
200: $rightType = $scope->getType($expr->right);
201: if (
202: $expr->left instanceof ClassConstFetch &&
203: $expr->left->class instanceof Expr &&
204: $expr->left->name instanceof Node\Identifier &&
205: $expr->right instanceof ClassConstFetch &&
206: $rightType instanceof ConstantStringType &&
207: strtolower($expr->left->name->toString()) === 'class'
208: ) {
209: return $this->specifyTypesInCondition(
210: $scope,
211: new Instanceof_(
212: $expr->left->class,
213: new Name($rightType->getValue()),
214: ),
215: $context,
216: $rootExpr,
217: );
218: }
219: if ($context->false()) {
220: $identicalType = $scope->getType($expr);
221: if ($identicalType instanceof ConstantBooleanType) {
222: $never = new NeverType();
223: $contextForTypes = $identicalType->getValue() ? $context->negate() : $context;
224: $leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope, $rootExpr);
225: $rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope, $rootExpr);
226: return $leftTypes->unionWith($rightTypes);
227: }
228: }
229:
230: $types = null;
231: $exprLeftType = $scope->getType($expr->left);
232: $exprRightType = $scope->getType($expr->right);
233: if (
234: $exprLeftType instanceof ConstantScalarType
235: || $exprLeftType instanceof EnumCaseObjectType
236: || ($exprLeftType instanceof ConstantType && !$exprRightType->equals($exprLeftType) && $exprRightType->isSuperTypeOf($exprLeftType)->yes())
237: ) {
238: $types = $this->create(
239: $expr->right,
240: $exprLeftType,
241: $context,
242: false,
243: $scope,
244: $rootExpr,
245: );
246: }
247: if (
248: $exprRightType instanceof ConstantScalarType
249: || $exprRightType instanceof EnumCaseObjectType
250: || ($exprRightType instanceof ConstantType && !$exprLeftType->equals($exprRightType) && $exprLeftType->isSuperTypeOf($exprRightType)->yes())
251: ) {
252: $leftType = $this->create(
253: $expr->left,
254: $exprRightType,
255: $context,
256: false,
257: $scope,
258: $rootExpr,
259: );
260: if ($types !== null) {
261: $types = $types->unionWith($leftType);
262: } else {
263: $types = $leftType;
264: }
265: }
266:
267: if ($types !== null) {
268: return $types;
269: }
270:
271: $leftExprString = $this->exprPrinter->printExpr($expr->left);
272: $rightExprString = $this->exprPrinter->printExpr($expr->right);
273: if ($leftExprString === $rightExprString) {
274: if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) {
275: return new SpecifiedTypes([], [], false, [], $rootExpr);
276: }
277: }
278:
279: if ($context->true()) {
280: $leftTypes = $this->create($expr->left, $exprRightType, $context, false, $scope, $rootExpr);
281: $rightTypes = $this->create($expr->right, $exprLeftType, $context, false, $scope, $rootExpr);
282: return $leftTypes->unionWith($rightTypes);
283: } elseif ($context->false()) {
284: return $this->create($expr->left, $exprLeftType, $context, false, $scope, $rootExpr)->normalize($scope)
285: ->intersectWith($this->create($expr->right, $exprRightType, $context, false, $scope, $rootExpr)->normalize($scope));
286: }
287:
288: } elseif ($expr instanceof Node\Expr\BinaryOp\NotIdentical) {
289: return $this->specifyTypesInCondition(
290: $scope,
291: new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Identical($expr->left, $expr->right)),
292: $context,
293: $rootExpr,
294: );
295: } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) {
296: $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
297: if ($expressions !== null) {
298: /** @var Expr $exprNode */
299: $exprNode = $expressions[0];
300: /** @var ConstantScalarType $constantType */
301: $constantType = $expressions[1];
302: if (!$context->null() && ($constantType->getValue() === false || $constantType->getValue() === null)) {
303: return $this->specifyTypesInCondition(
304: $scope,
305: $exprNode,
306: $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(),
307: $rootExpr,
308: );
309: }
310:
311: if (!$context->null() && $constantType->getValue() === true) {
312: return $this->specifyTypesInCondition(
313: $scope,
314: $exprNode,
315: $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(),
316: $rootExpr,
317: );
318: }
319:
320: if (
321: $exprNode instanceof FuncCall
322: && $exprNode->name instanceof Name
323: && strtolower($exprNode->name->toString()) === 'gettype'
324: && isset($exprNode->getArgs()[0])
325: && $constantType instanceof ConstantStringType
326: ) {
327: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr);
328: }
329:
330: if (
331: $exprNode instanceof FuncCall
332: && $exprNode->name instanceof Name
333: && strtolower($exprNode->name->toString()) === 'get_class'
334: && isset($exprNode->getArgs()[0])
335: && $constantType instanceof ConstantStringType
336: ) {
337: return $this->specifyTypesInCondition(
338: $scope,
339: new Instanceof_(
340: $exprNode->getArgs()[0]->value,
341: new Name($constantType->getValue()),
342: ),
343: $context,
344: $rootExpr,
345: );
346: }
347: }
348:
349: $leftType = $scope->getType($expr->left);
350: $rightType = $scope->getType($expr->right);
351:
352: $leftBooleanType = $leftType->toBoolean();
353: if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) {
354: return $this->specifyTypesInCondition(
355: $scope,
356: new Expr\BinaryOp\Identical(
357: new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')),
358: $expr->right,
359: ),
360: $context,
361: $rootExpr,
362: );
363: }
364:
365: $rightBooleanType = $rightType->toBoolean();
366: if ($rightBooleanType instanceof ConstantBooleanType && $leftType->isBoolean()->yes()) {
367: return $this->specifyTypesInCondition(
368: $scope,
369: new Expr\BinaryOp\Identical(
370: $expr->left,
371: new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')),
372: ),
373: $context,
374: $rootExpr,
375: );
376: }
377:
378: if (
379: !$context->null()
380: && $rightType->isArray()->yes()
381: && $leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no()
382: ) {
383: return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr);
384: }
385:
386: if (
387: !$context->null()
388: && $leftType->isArray()->yes()
389: && $rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no()
390: ) {
391: return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), false, $scope, $rootExpr);
392: }
393:
394: $integerType = new IntegerType();
395: $floatType = new FloatType();
396: if (
397: ($leftType->isString()->yes() && $rightType->isString()->yes())
398: || ($integerType->isSuperTypeOf($leftType)->yes() && $integerType->isSuperTypeOf($rightType)->yes())
399: || ($floatType->isSuperTypeOf($leftType)->yes() && $floatType->isSuperTypeOf($rightType)->yes())
400: ) {
401: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr);
402: }
403:
404: $leftExprString = $this->exprPrinter->printExpr($expr->left);
405: $rightExprString = $this->exprPrinter->printExpr($expr->right);
406: if ($leftExprString === $rightExprString) {
407: if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) {
408: return new SpecifiedTypes([], [], false, [], $rootExpr);
409: }
410: }
411:
412: $leftTypes = $this->create($expr->left, $leftType, $context, false, $scope, $rootExpr);
413: $rightTypes = $this->create($expr->right, $rightType, $context, false, $scope, $rootExpr);
414:
415: return $context->true()
416: ? $leftTypes->unionWith($rightTypes)
417: : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope));
418: } elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) {
419: return $this->specifyTypesInCondition(
420: $scope,
421: new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Equal($expr->left, $expr->right)),
422: $context,
423: $rootExpr,
424: );
425:
426: } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) {
427:
428: if (
429: $expr->left instanceof FuncCall
430: && count($expr->left->getArgs()) === 1
431: && $expr->left->name instanceof Name
432: && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen'], true)
433: && (
434: !$expr->right instanceof FuncCall
435: || !$expr->right->name instanceof Name
436: || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen'], true)
437: )
438: ) {
439: $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller
440: ? new Node\Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left)
441: : new Node\Expr\BinaryOp\Smaller($expr->right, $expr->left);
442:
443: return $this->specifyTypesInCondition(
444: $scope,
445: new Node\Expr\BooleanNot($inverseOperator),
446: $context,
447: $rootExpr,
448: );
449: }
450:
451: $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual;
452: $offset = $orEqual ? 0 : 1;
453: $leftType = $scope->getType($expr->left);
454: $result = new SpecifiedTypes([], [], false, [], $rootExpr);
455:
456: if (
457: !$context->null()
458: && $expr->right instanceof FuncCall
459: && count($expr->right->getArgs()) === 1
460: && $expr->right->name instanceof Name
461: && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true)
462: && (new IntegerType())->isSuperTypeOf($leftType)->yes()
463: ) {
464: if (
465: $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes())
466: || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes())
467: ) {
468: $argType = $scope->getType($expr->right->getArgs()[0]->value);
469: if ($argType->isArray()->yes()) {
470: $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope, $rootExpr));
471: }
472: }
473: }
474:
475: if (
476: !$context->null()
477: && $expr->right instanceof FuncCall
478: && count($expr->right->getArgs()) === 1
479: && $expr->right->name instanceof Name
480: && strtolower((string) $expr->right->name) === 'strlen'
481: && (new IntegerType())->isSuperTypeOf($leftType)->yes()
482: ) {
483: if (
484: $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes())
485: || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes())
486: ) {
487: $argType = $scope->getType($expr->right->getArgs()[0]->value);
488: if ($argType->isString()->yes()) {
489: $accessory = new AccessoryNonEmptyStringType();
490: if ($leftType instanceof ConstantIntegerType && $leftType->getValue() >= 2) {
491: $accessory = new AccessoryNonFalsyStringType();
492: }
493:
494: $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, false, $scope, $rootExpr));
495: }
496: }
497: }
498:
499: if ($leftType instanceof ConstantIntegerType) {
500: if ($expr->right instanceof Expr\PostInc) {
501: $result = $result->unionWith($this->createRangeTypes(
502: $rootExpr,
503: $expr->right->var,
504: IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1),
505: $context,
506: ));
507: } elseif ($expr->right instanceof Expr\PostDec) {
508: $result = $result->unionWith($this->createRangeTypes(
509: $rootExpr,
510: $expr->right->var,
511: IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1),
512: $context,
513: ));
514: } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) {
515: $result = $result->unionWith($this->createRangeTypes(
516: $rootExpr,
517: $expr->right->var,
518: IntegerRangeType::fromInterval($leftType->getValue(), null, $offset),
519: $context,
520: ));
521: }
522: }
523:
524: $rightType = $scope->getType($expr->right);
525: if ($rightType instanceof ConstantIntegerType) {
526: if ($expr->left instanceof Expr\PostInc) {
527: $result = $result->unionWith($this->createRangeTypes(
528: $rootExpr,
529: $expr->left->var,
530: IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1),
531: $context,
532: ));
533: } elseif ($expr->left instanceof Expr\PostDec) {
534: $result = $result->unionWith($this->createRangeTypes(
535: $rootExpr,
536: $expr->left->var,
537: IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1),
538: $context,
539: ));
540: } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) {
541: $result = $result->unionWith($this->createRangeTypes(
542: $rootExpr,
543: $expr->left->var,
544: IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset),
545: $context,
546: ));
547: }
548: }
549:
550: if ($context->true()) {
551: if (!$expr->left instanceof Node\Scalar) {
552: $result = $result->unionWith(
553: $this->create(
554: $expr->left,
555: $orEqual ? $rightType->getSmallerOrEqualType() : $rightType->getSmallerType(),
556: TypeSpecifierContext::createTruthy(),
557: false,
558: $scope,
559: $rootExpr,
560: ),
561: );
562: }
563: if (!$expr->right instanceof Node\Scalar) {
564: $result = $result->unionWith(
565: $this->create(
566: $expr->right,
567: $orEqual ? $leftType->getGreaterOrEqualType() : $leftType->getGreaterType(),
568: TypeSpecifierContext::createTruthy(),
569: false,
570: $scope,
571: $rootExpr,
572: ),
573: );
574: }
575: } elseif ($context->false()) {
576: if (!$expr->left instanceof Node\Scalar) {
577: $result = $result->unionWith(
578: $this->create(
579: $expr->left,
580: $orEqual ? $rightType->getGreaterType() : $rightType->getGreaterOrEqualType(),
581: TypeSpecifierContext::createTruthy(),
582: false,
583: $scope,
584: $rootExpr,
585: ),
586: );
587: }
588: if (!$expr->right instanceof Node\Scalar) {
589: $result = $result->unionWith(
590: $this->create(
591: $expr->right,
592: $orEqual ? $leftType->getSmallerType() : $leftType->getSmallerOrEqualType(),
593: TypeSpecifierContext::createTruthy(),
594: false,
595: $scope,
596: $rootExpr,
597: ),
598: );
599: }
600: }
601:
602: return $result;
603:
604: } elseif ($expr instanceof Node\Expr\BinaryOp\Greater) {
605: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Smaller($expr->right, $expr->left), $context, $rootExpr);
606:
607: } elseif ($expr instanceof Node\Expr\BinaryOp\GreaterOrEqual) {
608: return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context, $rootExpr);
609:
610: } elseif ($expr instanceof FuncCall && $expr->name instanceof Name) {
611: if ($this->reflectionProvider->hasFunction($expr->name, $scope)) {
612: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
613: foreach ($this->getFunctionTypeSpecifyingExtensions() as $extension) {
614: if (!$extension->isFunctionSupported($functionReflection, $expr, $context)) {
615: continue;
616: }
617:
618: return $extension->specifyTypes($functionReflection, $expr, $scope, $context);
619: }
620:
621: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants());
622: if (count($expr->getArgs()) > 0) {
623: $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope);
624: if ($specifiedTypes !== null) {
625: return $specifiedTypes;
626: }
627: }
628:
629: $asserts = $functionReflection->getAsserts()->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $parametersAcceptor->getResolvedTemplateTypeMap()));
630: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
631: if ($specifiedTypes !== null) {
632: return $specifiedTypes;
633: }
634: }
635:
636: return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope);
637: } elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) {
638: $methodCalledOnType = $scope->getType($expr->var);
639: $methodReflection = $scope->getMethodReflection($methodCalledOnType, $expr->name->name);
640: if ($methodReflection !== null) {
641: $referencedClasses = TypeUtils::getDirectClassNames($methodCalledOnType);
642: if (
643: count($referencedClasses) === 1
644: && $this->reflectionProvider->hasClass($referencedClasses[0])
645: ) {
646: $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]);
647: foreach ($this->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) {
648: if (!$extension->isMethodSupported($methodReflection, $expr, $context)) {
649: continue;
650: }
651:
652: return $extension->specifyTypes($methodReflection, $expr, $scope, $context);
653: }
654: }
655:
656: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants());
657: if (count($expr->getArgs()) > 0) {
658: $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope);
659: if ($specifiedTypes !== null) {
660: return $specifiedTypes;
661: }
662: }
663:
664: $asserts = $methodReflection->getAsserts()->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $parametersAcceptor->getResolvedTemplateTypeMap()));
665: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
666: if ($specifiedTypes !== null) {
667: return $specifiedTypes;
668: }
669: }
670:
671: return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope);
672: } elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) {
673: if ($expr->class instanceof Name) {
674: $calleeType = $scope->resolveTypeByName($expr->class);
675: } else {
676: $calleeType = $scope->getType($expr->class);
677: }
678:
679: $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name);
680: if ($staticMethodReflection !== null) {
681: $referencedClasses = TypeUtils::getDirectClassNames($calleeType);
682: if (
683: count($referencedClasses) === 1
684: && $this->reflectionProvider->hasClass($referencedClasses[0])
685: ) {
686: $staticMethodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]);
687: foreach ($this->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $extension) {
688: if (!$extension->isStaticMethodSupported($staticMethodReflection, $expr, $context)) {
689: continue;
690: }
691:
692: return $extension->specifyTypes($staticMethodReflection, $expr, $scope, $context);
693: }
694: }
695:
696: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants());
697: if (count($expr->getArgs()) > 0) {
698: $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope);
699: if ($specifiedTypes !== null) {
700: return $specifiedTypes;
701: }
702: }
703:
704: $asserts = $staticMethodReflection->getAsserts()->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $parametersAcceptor->getResolvedTemplateTypeMap()));
705: $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
706: if ($specifiedTypes !== null) {
707: return $specifiedTypes;
708: }
709: }
710:
711: return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope);
712: } elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) {
713: if (!$scope instanceof MutatingScope) {
714: throw new ShouldNotHappenException();
715: }
716: $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context, $rootExpr);
717: $rightScope = $scope->filterByTruthyValue($expr->left);
718: $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context, $rootExpr);
719: $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope));
720: if ($context->false()) {
721: return new SpecifiedTypes(
722: $types->getSureTypes(),
723: $types->getSureNotTypes(),
724: false,
725: array_merge(
726: $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes),
727: $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes),
728: ),
729: $rootExpr,
730: );
731: }
732:
733: return $types;
734: } elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) {
735: if (!$scope instanceof MutatingScope) {
736: throw new ShouldNotHappenException();
737: }
738: $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context, $rootExpr);
739: $rightScope = $scope->filterByFalseyValue($expr->left);
740: $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context, $rootExpr);
741: $types = $context->true() ? $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)) : $leftTypes->unionWith($rightTypes);
742: if ($context->true()) {
743: return new SpecifiedTypes(
744: $types->getSureTypes(),
745: $types->getSureNotTypes(),
746: false,
747: array_merge(
748: $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes),
749: $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes),
750: ),
751: $rootExpr,
752: );
753: }
754:
755: return $types;
756: } elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) {
757: return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate(), $rootExpr);
758: } elseif ($expr instanceof Node\Expr\Assign) {
759: if (!$scope instanceof MutatingScope) {
760: throw new ShouldNotHappenException();
761: }
762: if ($context->null()) {
763: return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context, $rootExpr);
764: }
765:
766: return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context, $rootExpr);
767: } elseif (
768: $expr instanceof Expr\Isset_
769: && count($expr->vars) > 0
770: && !$context->null()
771: ) {
772: if (!$context->true()) {
773: if (!$scope instanceof MutatingScope) {
774: throw new ShouldNotHappenException();
775: }
776:
777: return array_reduce(
778: array_filter(
779: $expr->vars,
780: static fn (Expr $var) => $scope->issetCheck($var, static fn () => true),
781: ),
782: fn (SpecifiedTypes $types, Expr $var) => $types->unionWith($this->specifyTypesInCondition($scope, $var, $context, $rootExpr)),
783: new SpecifiedTypes(),
784: );
785: }
786:
787: $vars = [];
788: foreach ($expr->vars as $var) {
789: $tmpVars = [$var];
790:
791: while (
792: $var instanceof ArrayDimFetch
793: || $var instanceof PropertyFetch
794: || (
795: $var instanceof StaticPropertyFetch
796: && $var->class instanceof Expr
797: )
798: ) {
799: if ($var instanceof StaticPropertyFetch) {
800: /** @var Expr $var */
801: $var = $var->class;
802: } else {
803: $var = $var->var;
804: }
805: $tmpVars[] = $var;
806: }
807:
808: $vars = array_merge($vars, array_reverse($tmpVars));
809: }
810:
811: $types = null;
812: foreach ($vars as $var) {
813: if ($var instanceof Expr\Variable && is_string($var->name)) {
814: if ($scope->hasVariableType($var->name)->no()) {
815: return new SpecifiedTypes([], [], false, [], $rootExpr);
816: }
817: }
818: if (
819: $var instanceof ArrayDimFetch
820: && $var->dim !== null
821: && !$scope->getType($var->var) instanceof MixedType
822: ) {
823: $dimType = $scope->getType($var->dim);
824:
825: if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
826: $type = $this->create(
827: $var->var,
828: new HasOffsetType($dimType),
829: $context,
830: false,
831: $scope,
832: $rootExpr,
833: );
834: } else {
835: $type = new SpecifiedTypes();
836: }
837:
838: $type = $type->unionWith(
839: $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope, $rootExpr),
840: );
841: } else {
842: $type = $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope, $rootExpr);
843: }
844:
845: if (
846: $var instanceof PropertyFetch
847: && $var->name instanceof Node\Identifier
848: ) {
849: $type = $type->unionWith($this->create($var->var, new IntersectionType([
850: new ObjectWithoutClassType(),
851: new HasPropertyType($var->name->toString()),
852: ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr));
853: } elseif (
854: $var instanceof StaticPropertyFetch
855: && $var->class instanceof Expr
856: && $var->name instanceof Node\VarLikeIdentifier
857: ) {
858: $type = $type->unionWith($this->create($var->class, new IntersectionType([
859: new ObjectWithoutClassType(),
860: new HasPropertyType($var->name->toString()),
861: ]), TypeSpecifierContext::createTruthy(), false, $scope, $rootExpr));
862: }
863:
864: if ($types === null) {
865: $types = $type;
866: } else {
867: $types = $types->unionWith($type);
868: }
869: }
870:
871: return $types;
872: } elseif (
873: $expr instanceof Expr\BinaryOp\Coalesce
874: && $context->true()
875: && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right))->yes())
876: ) {
877: return $this->create(
878: $expr->left,
879: new NullType(),
880: TypeSpecifierContext::createFalse(),
881: false,
882: $scope,
883: $rootExpr,
884: );
885: } elseif (
886: $expr instanceof Expr\Empty_
887: ) {
888: return $this->specifyTypesInCondition($scope, new BooleanOr(
889: new Expr\BooleanNot(new Expr\Isset_([$expr->expr])),
890: new Expr\BooleanNot($expr->expr),
891: ), $context, $rootExpr);
892: } elseif ($expr instanceof Expr\ErrorSuppress) {
893: return $this->specifyTypesInCondition($scope, $expr->expr, $context, $rootExpr);
894: } elseif (
895: $expr instanceof Expr\Ternary
896: && !$context->null()
897: && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->else))->yes())
898: ) {
899: $conditionExpr = $expr->cond;
900: if ($expr->if !== null) {
901: $conditionExpr = new BooleanAnd($conditionExpr, $expr->if);
902: }
903:
904: return $this->specifyTypesInCondition($scope, $conditionExpr, $context, $rootExpr);
905:
906: } elseif ($expr instanceof Expr\NullsafePropertyFetch && !$context->null()) {
907: $types = $this->specifyTypesInCondition(
908: $scope,
909: new BooleanAnd(
910: new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
911: new PropertyFetch($expr->var, $expr->name),
912: ),
913: $context,
914: $rootExpr,
915: );
916:
917: $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope);
918: return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope));
919: } elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) {
920: $types = $this->specifyTypesInCondition(
921: $scope,
922: new BooleanAnd(
923: new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
924: new MethodCall($expr->var, $expr->name, $expr->args),
925: ),
926: $context,
927: $rootExpr,
928: );
929:
930: $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope);
931: return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope));
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: return $types->unionWith($this->specifyTypesInCondition(
950: $scope,
951: $exprNode,
952: $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(),
953: $rootExpr,
954: ));
955: }
956:
957: if (!$context->null() && $constantType->getValue() === true) {
958: $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
959: return $types->unionWith($this->specifyTypesInCondition(
960: $scope,
961: $exprNode,
962: $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(),
963: $rootExpr,
964: ));
965: }
966:
967: if ($constantType->getValue() === null) {
968: return $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
969: }
970:
971: if (
972: !$context->null()
973: && $exprNode instanceof FuncCall
974: && count($exprNode->getArgs()) === 1
975: && $exprNode->name instanceof Name
976: && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true)
977: && $constantType instanceof ConstantIntegerType
978: ) {
979: if ($context->truthy() || $constantType->getValue() === 0) {
980: $newContext = $context;
981: if ($constantType->getValue() === 0) {
982: $newContext = $newContext->negate();
983: }
984: $argType = $scope->getType($exprNode->getArgs()[0]->value);
985: if ($argType->isArray()->yes()) {
986: $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
987: if ($argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
988: $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
989: $itemType = $argType->getIterableValueType();
990: for ($i = 0; $i < $constantType->getValue(); $i++) {
991: $valueTypesBuilder->setOffsetValueType(new ConstantIntegerType($i), $itemType);
992: }
993: $valueTypes = $this->create($exprNode->getArgs()[0]->value, $valueTypesBuilder->getArray(), $context, false, $scope, $rootExpr);
994: } else {
995: $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr);
996: }
997: return $funcTypes->unionWith($valueTypes);
998: }
999: }
1000: }
1001:
1002: if (
1003: !$context->null()
1004: && $exprNode instanceof FuncCall
1005: && count($exprNode->getArgs()) === 1
1006: && $exprNode->name instanceof Name
1007: && strtolower((string) $exprNode->name) === 'strlen'
1008: && $constantType instanceof ConstantIntegerType
1009: ) {
1010: if ($context->truthy() || $constantType->getValue() === 0) {
1011: $newContext = $context;
1012: if ($constantType->getValue() === 0) {
1013: $newContext = $newContext->negate();
1014: }
1015: $argType = $scope->getType($exprNode->getArgs()[0]->value);
1016: if ($argType->isString()->yes()) {
1017: $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
1018:
1019: $accessory = new AccessoryNonEmptyStringType();
1020: if ($constantType->getValue() >= 2) {
1021: $accessory = new AccessoryNonFalsyStringType();
1022: }
1023: $valueTypes = $this->create($exprNode->getArgs()[0]->value, $accessory, $newContext, false, $scope, $rootExpr);
1024:
1025: return $funcTypes->unionWith($valueTypes);
1026: }
1027: }
1028:
1029: }
1030:
1031: if ($constantType instanceof ConstantStringType) {
1032: return $this->specifyTypesForConstantStringBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr);
1033: }
1034:
1035: return null;
1036: }
1037:
1038: private function specifyTypesForConstantStringBinaryExpression(
1039: Expr $exprNode,
1040: ConstantStringType $constantType,
1041: TypeSpecifierContext $context,
1042: Scope $scope,
1043: ?Expr $rootExpr,
1044: ): ?SpecifiedTypes
1045: {
1046: if (
1047: $context->truthy()
1048: && $exprNode instanceof FuncCall
1049: && $exprNode->name instanceof Name
1050: && in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords', 'mb_convert_case', 'mb_convert_kana'], true)
1051: && isset($exprNode->getArgs()[0])
1052: && $constantType->getValue() !== ''
1053: ) {
1054: $argType = $scope->getType($exprNode->getArgs()[0]->value);
1055:
1056: if ($argType->isString()->yes()) {
1057: if ($constantType->getValue() !== '0') {
1058: return $this->create(
1059: $exprNode->getArgs()[0]->value,
1060: TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()),
1061: $context,
1062: false,
1063: $scope,
1064: );
1065: }
1066:
1067: return $this->create(
1068: $exprNode->getArgs()[0]->value,
1069: TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()),
1070: $context,
1071: false,
1072: $scope,
1073: );
1074: }
1075: }
1076:
1077: if (
1078: $exprNode instanceof FuncCall
1079: && $exprNode->name instanceof Name
1080: && strtolower($exprNode->name->toString()) === 'gettype'
1081: && isset($exprNode->getArgs()[0])
1082: ) {
1083: $type = null;
1084: if ($constantType->getValue() === 'string') {
1085: $type = new StringType();
1086: }
1087: if ($constantType->getValue() === 'array') {
1088: $type = new ArrayType(new MixedType(), new MixedType());
1089: }
1090: if ($constantType->getValue() === 'boolean') {
1091: $type = new BooleanType();
1092: }
1093: if ($constantType->getValue() === 'resource' || $constantType->getValue() === 'resource (closed)') {
1094: $type = new ResourceType();
1095: }
1096: if ($constantType->getValue() === 'integer') {
1097: $type = new IntegerType();
1098: }
1099: if ($constantType->getValue() === 'double') {
1100: $type = new FloatType();
1101: }
1102: if ($constantType->getValue() === 'NULL') {
1103: $type = new NullType();
1104: }
1105: if ($constantType->getValue() === 'object') {
1106: $type = new ObjectWithoutClassType();
1107: }
1108:
1109: if ($type !== null) {
1110: return $this->create($exprNode->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr);
1111: }
1112: }
1113:
1114: if (
1115: $context->true()
1116: && $exprNode instanceof FuncCall
1117: && $exprNode->name instanceof Name
1118: && strtolower((string) $exprNode->name) === 'get_parent_class'
1119: && isset($exprNode->getArgs()[0])
1120: ) {
1121: $argType = $scope->getType($exprNode->getArgs()[0]->value);
1122: $objectType = new ObjectType($constantType->getValue());
1123: $classStringType = new GenericClassStringType($objectType);
1124:
1125: if ($argType->isString()->yes()) {
1126: return $this->create(
1127: $exprNode->getArgs()[0]->value,
1128: $classStringType,
1129: $context,
1130: false,
1131: $scope,
1132: );
1133: }
1134:
1135: if ((new ObjectWithoutClassType())->isSuperTypeOf($argType)->yes()) {
1136: return $this->create(
1137: $exprNode->getArgs()[0]->value,
1138: $objectType,
1139: $context,
1140: false,
1141: $scope,
1142: );
1143: }
1144:
1145: return $this->create(
1146: $exprNode->getArgs()[0]->value,
1147: TypeCombinator::union($objectType, $classStringType),
1148: $context,
1149: false,
1150: $scope,
1151: );
1152: }
1153:
1154: return null;
1155: }
1156:
1157: private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, ?Expr $rootExpr, Expr $expr, Scope $scope): SpecifiedTypes
1158: {
1159: if ($context->null()) {
1160: return new SpecifiedTypes([], [], false, [], $rootExpr);
1161: }
1162: if (!$context->truthy()) {
1163: $type = StaticTypeFactory::truthy();
1164: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), false, $scope, $rootExpr);
1165: } elseif (!$context->falsey()) {
1166: $type = StaticTypeFactory::falsey();
1167: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), false, $scope, $rootExpr);
1168: }
1169:
1170: return new SpecifiedTypes([], [], false, [], $rootExpr);
1171: }
1172:
1173: private function specifyTypesFromConditionalReturnType(
1174: TypeSpecifierContext $context,
1175: Expr\CallLike $call,
1176: ParametersAcceptor $parametersAcceptor,
1177: Scope $scope,
1178: ): ?SpecifiedTypes
1179: {
1180: if (!$parametersAcceptor instanceof ResolvedFunctionVariant) {
1181: return null;
1182: }
1183:
1184: $returnType = $parametersAcceptor->getOriginalParametersAcceptor()->getReturnType();
1185: if (!$returnType instanceof ConditionalTypeForParameter) {
1186: return null;
1187: }
1188:
1189: if ($context->true()) {
1190: $leftType = new ConstantBooleanType(true);
1191: $rightType = new ConstantBooleanType(false);
1192: } elseif ($context->false()) {
1193: $leftType = new ConstantBooleanType(false);
1194: $rightType = new ConstantBooleanType(true);
1195: } elseif ($context->null()) {
1196: $leftType = new MixedType();
1197: $rightType = new NeverType();
1198: } else {
1199: return null;
1200: }
1201:
1202: $argsMap = [];
1203: $parameters = $parametersAcceptor->getParameters();
1204: foreach ($call->getArgs() as $i => $arg) {
1205: if ($arg->unpack) {
1206: continue;
1207: }
1208:
1209: if ($arg->name !== null) {
1210: $paramName = $arg->name->toString();
1211: } elseif (isset($parameters[$i])) {
1212: $paramName = $parameters[$i]->getName();
1213: } else {
1214: continue;
1215: }
1216:
1217: $argsMap['$' . $paramName] = $arg->value;
1218: }
1219:
1220: return $this->getConditionalSpecifiedTypes($returnType, $leftType, $rightType, $scope, $argsMap);
1221: }
1222:
1223: /**
1224: * @param array<string, Expr> $argsMap
1225: */
1226: public function getConditionalSpecifiedTypes(
1227: ConditionalTypeForParameter $conditionalType,
1228: Type $leftType,
1229: Type $rightType,
1230: Scope $scope,
1231: array $argsMap,
1232: ): ?SpecifiedTypes
1233: {
1234: $parameterName = $conditionalType->getParameterName();
1235: if (!array_key_exists($parameterName, $argsMap)) {
1236: return null;
1237: }
1238:
1239: $targetType = $conditionalType->getTarget();
1240: $ifType = $conditionalType->getIf();
1241: $elseType = $conditionalType->getElse();
1242:
1243: if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) {
1244: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue();
1245: } elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) {
1246: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
1247: } else {
1248: return null;
1249: }
1250:
1251: $specifiedTypes = $this->create(
1252: $argsMap[$parameterName],
1253: $targetType,
1254: $context,
1255: false,
1256: $scope,
1257: );
1258:
1259: if ($targetType instanceof ConstantBooleanType) {
1260: if (!$targetType->getValue()) {
1261: $context = $context->negate();
1262: }
1263:
1264: $specifiedTypes = $specifiedTypes->unionWith($this->specifyTypesInCondition($scope, $argsMap[$parameterName], $context));
1265: }
1266:
1267: return $specifiedTypes;
1268: }
1269:
1270: private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\CallLike $call, Assertions $assertions, ParametersAcceptor $parametersAcceptor, Scope $scope): ?SpecifiedTypes
1271: {
1272: if ($context->null()) {
1273: $asserts = $assertions->getAsserts();
1274: } elseif ($context->true()) {
1275: $asserts = $assertions->getAssertsIfTrue();
1276: } elseif ($context->false()) {
1277: $asserts = $assertions->getAssertsIfFalse();
1278: } else {
1279: throw new ShouldNotHappenException();
1280: }
1281:
1282: if (count($asserts) === 0) {
1283: return null;
1284: }
1285:
1286: $argsMap = [];
1287: $parameters = $parametersAcceptor->getParameters();
1288: foreach ($call->getArgs() as $i => $arg) {
1289: if ($arg->unpack) {
1290: continue;
1291: }
1292:
1293: if ($arg->name !== null) {
1294: $paramName = $arg->name->toString();
1295: } elseif (isset($parameters[$i])) {
1296: $paramName = $parameters[$i]->getName();
1297: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
1298: $lastParameter = $parameters[count($parameters) - 1];
1299: $paramName = $lastParameter->getName();
1300: } else {
1301: continue;
1302: }
1303:
1304: $argsMap[$paramName][] = $arg->value;
1305: }
1306:
1307: if ($call instanceof MethodCall) {
1308: $argsMap['this'] = [$call->var];
1309: }
1310:
1311: /** @var SpecifiedTypes|null $types */
1312: $types = null;
1313:
1314: foreach ($asserts as $assert) {
1315: foreach ($argsMap[substr($assert->getParameter()->getParameterName(), 1)] ?? [] as $parameterExpr) {
1316: $assertedType = TypeTraverser::map($assert->getType(), static function (Type $type, callable $traverse) use ($argsMap, $scope): Type {
1317: if ($type instanceof ConditionalTypeForParameter) {
1318: $parameterName = substr($type->getParameterName(), 1);
1319: if (array_key_exists($parameterName, $argsMap)) {
1320: $argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[$parameterName]));
1321: $type = $type->toConditional($argType);
1322: }
1323: }
1324:
1325: return $traverse($type);
1326: });
1327:
1328: $assertExpr = $assert->getParameter()->getExpr($parameterExpr);
1329:
1330: $templateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap();
1331: $containsUnresolvedTemplate = false;
1332: TypeTraverser::map(
1333: $assert->getOriginalType(),
1334: static function (Type $type, callable $traverse) use ($templateTypeMap, &$containsUnresolvedTemplate) {
1335: if ($type instanceof TemplateType && $type->getScope()->getClassName() !== null) {
1336: $resolvedType = $templateTypeMap->getType($type->getName());
1337: if ($resolvedType === null || $type->getBound()->equals($resolvedType)) {
1338: $containsUnresolvedTemplate = true;
1339: return $type;
1340: }
1341: }
1342:
1343: return $traverse($type);
1344: },
1345: );
1346:
1347: $newTypes = $this->create(
1348: $assertExpr,
1349: $assertedType,
1350: $assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(),
1351: false,
1352: $scope,
1353: $containsUnresolvedTemplate || $assert->isEquality() ? $call : null,
1354: );
1355: $types = $types !== null ? $types->unionWith($newTypes) : $newTypes;
1356:
1357: if (!$context->null() || !$assertedType instanceof ConstantBooleanType) {
1358: continue;
1359: }
1360:
1361: $subContext = $assertedType->getValue() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
1362: if ($assert->isNegated()) {
1363: $subContext = $subContext->negate();
1364: }
1365:
1366: $types = $types->unionWith($this->specifyTypesInCondition(
1367: $scope,
1368: $assertExpr,
1369: $subContext,
1370: ));
1371: }
1372: }
1373:
1374: return $types;
1375: }
1376:
1377: /**
1378: * @return array<string, ConditionalExpressionHolder[]>
1379: */
1380: private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array
1381: {
1382: $conditionExpressionTypes = [];
1383: foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
1384: if (!$expr instanceof Expr\Variable) {
1385: continue;
1386: }
1387: if (!is_string($expr->name)) {
1388: continue;
1389: }
1390:
1391: $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes(
1392: $expr,
1393: TypeCombinator::intersect($scope->getType($expr), $type),
1394: );
1395: }
1396:
1397: if (count($conditionExpressionTypes) > 0) {
1398: $holders = [];
1399: foreach ($rightTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
1400: if (!$expr instanceof Expr\Variable) {
1401: continue;
1402: }
1403: if (!is_string($expr->name)) {
1404: continue;
1405: }
1406:
1407: if (!isset($holders[$exprString])) {
1408: $holders[$exprString] = [];
1409: }
1410:
1411: $holder = new ConditionalExpressionHolder(
1412: $conditionExpressionTypes,
1413: new ExpressionTypeHolder($expr, TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()),
1414: );
1415: $holders[$exprString][$holder->getKey()] = $holder;
1416: }
1417:
1418: return $holders;
1419: }
1420:
1421: return [];
1422: }
1423:
1424: /**
1425: * @return (Expr|ConstantScalarType)[]|null
1426: */
1427: private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array
1428: {
1429: $leftType = $scope->getType($binaryOperation->left);
1430: $rightType = $scope->getType($binaryOperation->right);
1431: if (
1432: $leftType instanceof ConstantScalarType
1433: && !$binaryOperation->right instanceof ConstFetch
1434: && !$binaryOperation->right instanceof ClassConstFetch
1435: ) {
1436: return [$binaryOperation->right, $leftType];
1437: } elseif (
1438: $rightType instanceof ConstantScalarType
1439: && !$binaryOperation->left instanceof ConstFetch
1440: && !$binaryOperation->left instanceof ClassConstFetch
1441: ) {
1442: return [$binaryOperation->left, $rightType];
1443: }
1444:
1445: return null;
1446: }
1447:
1448: /** @api */
1449: public function create(
1450: Expr $expr,
1451: Type $type,
1452: TypeSpecifierContext $context,
1453: bool $overwrite = false,
1454: ?Scope $scope = null,
1455: ?Expr $rootExpr = null,
1456: ): SpecifiedTypes
1457: {
1458: if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) {
1459: return new SpecifiedTypes([], [], false, [], $rootExpr);
1460: }
1461:
1462: $specifiedExprs = [];
1463:
1464: if ($expr instanceof Expr\Assign) {
1465: $specifiedExprs[] = $expr->var;
1466:
1467: while ($expr->expr instanceof Expr\Assign) {
1468: $specifiedExprs[] = $expr->expr->var;
1469: $expr = $expr->expr;
1470: }
1471: } else {
1472: $specifiedExprs[] = $expr;
1473: }
1474:
1475: $types = null;
1476:
1477: foreach ($specifiedExprs as $specifiedExpr) {
1478: $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $overwrite, $scope, $rootExpr);
1479:
1480: if ($types === null) {
1481: $types = $newTypes;
1482: } else {
1483: $types = $types->unionWith($newTypes);
1484: }
1485: }
1486:
1487: return $types;
1488: }
1489:
1490: private function createForExpr(
1491: Expr $expr,
1492: Type $type,
1493: TypeSpecifierContext $context,
1494: bool $overwrite = false,
1495: ?Scope $scope = null,
1496: ?Expr $rootExpr = null,
1497: ): SpecifiedTypes
1498: {
1499: if ($scope !== null) {
1500: if ($context->true()) {
1501: $containsNull = TypeCombinator::containsNull($type) && TypeCombinator::containsNull($scope->getType($expr));
1502: } elseif ($context->false()) {
1503: $containsNull = !TypeCombinator::containsNull($type) && TypeCombinator::containsNull($scope->getType($expr));
1504: }
1505: }
1506:
1507: $originalExpr = $expr;
1508: if (isset($containsNull) && !$containsNull) {
1509: $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr);
1510: }
1511:
1512: if (
1513: $scope !== null
1514: && !$context->null()
1515: && $expr instanceof Expr\BinaryOp\Coalesce
1516: ) {
1517: $rightIsSuperType = $type->isSuperTypeOf($scope->getType($expr->right));
1518: if (($context->true() && $rightIsSuperType->no()) || ($context->false() && $rightIsSuperType->yes())) {
1519: $expr = $expr->left;
1520: }
1521: }
1522:
1523: if (
1524: $expr instanceof FuncCall
1525: && $expr->name instanceof Name
1526: ) {
1527: $has = $this->reflectionProvider->hasFunction($expr->name, $scope);
1528: if (!$has) {
1529: // backwards compatibility with previous behaviour
1530: return new SpecifiedTypes([], [], false, [], $rootExpr);
1531: }
1532:
1533: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
1534: $hasSideEffects = $functionReflection->hasSideEffects();
1535: if ($hasSideEffects->yes()) {
1536: return new SpecifiedTypes([], [], false, [], $rootExpr);
1537: }
1538:
1539: if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
1540: return new SpecifiedTypes([], [], false, [], $rootExpr);
1541: }
1542: }
1543:
1544: if (
1545: $expr instanceof MethodCall
1546: && $expr->name instanceof Node\Identifier
1547: && $scope !== null
1548: ) {
1549: $methodName = $expr->name->toString();
1550: $calledOnType = $scope->getType($expr->var);
1551: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
1552: if (
1553: $methodReflection === null
1554: || $methodReflection->hasSideEffects()->yes()
1555: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
1556: ) {
1557: if (isset($containsNull) && !$containsNull) {
1558: return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type);
1559: }
1560:
1561: return new SpecifiedTypes([], [], false, [], $rootExpr);
1562: }
1563: }
1564:
1565: if (
1566: $expr instanceof StaticCall
1567: && $expr->name instanceof Node\Identifier
1568: && $scope !== null
1569: ) {
1570: $methodName = $expr->name->toString();
1571: if ($expr->class instanceof Name) {
1572: $calledOnType = $scope->resolveTypeByName($expr->class);
1573: } else {
1574: $calledOnType = $scope->getType($expr->class);
1575: }
1576:
1577: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
1578: if (
1579: $methodReflection === null
1580: || $methodReflection->hasSideEffects()->yes()
1581: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
1582: ) {
1583: if (isset($containsNull) && !$containsNull) {
1584: return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type);
1585: }
1586:
1587: return new SpecifiedTypes([], [], false, [], $rootExpr);
1588: }
1589: }
1590:
1591: $sureTypes = [];
1592: $sureNotTypes = [];
1593: $exprString = $this->exprPrinter->printExpr($expr);
1594: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
1595: if ($context->false()) {
1596: $sureNotTypes[$exprString] = [$expr, $type];
1597: if ($exprString !== $originalExprString) {
1598: $sureNotTypes[$originalExprString] = [$originalExpr, $type];
1599: }
1600: } elseif ($context->true()) {
1601: $sureTypes[$exprString] = [$expr, $type];
1602: if ($exprString !== $originalExprString) {
1603: $sureTypes[$originalExprString] = [$originalExpr, $type];
1604: }
1605: }
1606:
1607: $types = new SpecifiedTypes($sureTypes, $sureNotTypes, $overwrite, [], $rootExpr);
1608: if ($scope !== null && isset($containsNull) && !$containsNull) {
1609: return $this->createNullsafeTypes($rootExpr, $originalExpr, $scope, $context, $overwrite, $type)->unionWith($types);
1610: }
1611:
1612: return $types;
1613: }
1614:
1615: private function createNullsafeTypes(?Expr $rootExpr, Expr $expr, Scope $scope, TypeSpecifierContext $context, bool $overwrite, ?Type $type): SpecifiedTypes
1616: {
1617: if ($expr instanceof Expr\NullsafePropertyFetch) {
1618: if ($type !== null) {
1619: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, false, $scope, $rootExpr);
1620: } else {
1621: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), false, $scope, $rootExpr);
1622: }
1623:
1624: return $propertyFetchTypes->unionWith(
1625: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $overwrite, $scope, $rootExpr),
1626: );
1627: }
1628:
1629: if ($expr instanceof Expr\NullsafeMethodCall) {
1630: if ($type !== null) {
1631: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $overwrite, $scope, $rootExpr);
1632: } else {
1633: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $overwrite, $scope, $rootExpr);
1634: }
1635:
1636: return $methodCallTypes->unionWith(
1637: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $overwrite, $scope, $rootExpr),
1638: );
1639: }
1640:
1641: if ($expr instanceof Expr\PropertyFetch) {
1642: return $this->createNullsafeTypes($rootExpr, $expr->var, $scope, $context, $overwrite, null);
1643: }
1644:
1645: if ($expr instanceof Expr\MethodCall) {
1646: return $this->createNullsafeTypes($rootExpr, $expr->var, $scope, $context, $overwrite, null);
1647: }
1648:
1649: if ($expr instanceof Expr\ArrayDimFetch) {
1650: return $this->createNullsafeTypes($rootExpr, $expr->var, $scope, $context, $overwrite, null);
1651: }
1652:
1653: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
1654: return $this->createNullsafeTypes($rootExpr, $expr->class, $scope, $context, $overwrite, null);
1655: }
1656:
1657: if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
1658: return $this->createNullsafeTypes($rootExpr, $expr->class, $scope, $context, $overwrite, null);
1659: }
1660:
1661: return new SpecifiedTypes([], [], $overwrite, [], $rootExpr);
1662: }
1663:
1664: private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
1665: {
1666: $sureNotTypes = [];
1667:
1668: if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) {
1669: $exprString = $this->exprPrinter->printExpr($expr);
1670: if ($context->false()) {
1671: $sureNotTypes[$exprString] = [$expr, $type];
1672: } elseif ($context->true()) {
1673: $inverted = TypeCombinator::remove(new IntegerType(), $type);
1674: $sureNotTypes[$exprString] = [$expr, $inverted];
1675: }
1676: }
1677:
1678: return new SpecifiedTypes([], $sureNotTypes, false, [], $rootExpr);
1679: }
1680:
1681: /**
1682: * @return FunctionTypeSpecifyingExtension[]
1683: */
1684: private function getFunctionTypeSpecifyingExtensions(): array
1685: {
1686: return $this->functionTypeSpecifyingExtensions;
1687: }
1688:
1689: /**
1690: * @return MethodTypeSpecifyingExtension[]
1691: */
1692: private function getMethodTypeSpecifyingExtensionsForClass(string $className): array
1693: {
1694: if ($this->methodTypeSpecifyingExtensionsByClass === null) {
1695: $byClass = [];
1696: foreach ($this->methodTypeSpecifyingExtensions as $extension) {
1697: $byClass[$extension->getClass()][] = $extension;
1698: }
1699:
1700: $this->methodTypeSpecifyingExtensionsByClass = $byClass;
1701: }
1702: return $this->getTypeSpecifyingExtensionsForType($this->methodTypeSpecifyingExtensionsByClass, $className);
1703: }
1704:
1705: /**
1706: * @return StaticMethodTypeSpecifyingExtension[]
1707: */
1708: private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
1709: {
1710: if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
1711: $byClass = [];
1712: foreach ($this->staticMethodTypeSpecifyingExtensions as $extension) {
1713: $byClass[$extension->getClass()][] = $extension;
1714: }
1715:
1716: $this->staticMethodTypeSpecifyingExtensionsByClass = $byClass;
1717: }
1718: return $this->getTypeSpecifyingExtensionsForType($this->staticMethodTypeSpecifyingExtensionsByClass, $className);
1719: }
1720:
1721: /**
1722: * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions
1723: * @return mixed[]
1724: */
1725: private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array
1726: {
1727: $extensionsForClass = [[]];
1728: $class = $this->reflectionProvider->getClass($className);
1729: foreach (array_merge([$className], $class->getParentClassesNames(), $class->getNativeReflection()->getInterfaceNames()) as $extensionClassName) {
1730: if (!isset($extensions[$extensionClassName])) {
1731: continue;
1732: }
1733:
1734: $extensionsForClass[] = $extensions[$extensionClassName];
1735: }
1736:
1737: return array_merge(...$extensionsForClass);
1738: }
1739:
1740: }
1741: