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