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