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