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