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\ConstFetch;
8: use PhpParser\Node\Expr\FuncCall;
9: use PhpParser\Node\Expr\Instanceof_;
10: use PhpParser\Node\Expr\MethodCall;
11: use PhpParser\Node\Expr\PropertyFetch;
12: use PhpParser\Node\Expr\StaticCall;
13: use PhpParser\Node\Name;
14: use PHPStan\DependencyInjection\AutowiredService;
15: use PHPStan\DependencyInjection\Container;
16: use PHPStan\Node\Expr\AlwaysRememberedExpr;
17: use PHPStan\Node\Expr\TypeExpr;
18: use PHPStan\Node\Printer\ExprPrinter;
19: use PHPStan\Reflection\Assertions;
20: use PHPStan\Reflection\ParametersAcceptor;
21: use PHPStan\Reflection\ReflectionProvider;
22: use PHPStan\Reflection\ResolvedFunctionVariant;
23: use PHPStan\ShouldNotHappenException;
24: use PHPStan\TrinaryLogic;
25: use PHPStan\Type\Accessory\HasOffsetValueType;
26: use PHPStan\Type\Accessory\NonEmptyArrayType;
27: use PHPStan\Type\ConditionalTypeForParameter;
28: use PHPStan\Type\Constant\ConstantBooleanType;
29: use PHPStan\Type\Constant\ConstantIntegerType;
30: use PHPStan\Type\FunctionTypeSpecifyingExtension;
31: use PHPStan\Type\Generic\TemplateType;
32: use PHPStan\Type\IntegerRangeType;
33: use PHPStan\Type\MethodTypeSpecifyingExtension;
34: use PHPStan\Type\MixedType;
35: use PHPStan\Type\NeverType;
36: use PHPStan\Type\NullType;
37: use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
38: use PHPStan\Type\StaticTypeFactory;
39: use PHPStan\Type\Type;
40: use PHPStan\Type\TypeCombinator;
41: use PHPStan\Type\TypeTraverser;
42: use function array_key_exists;
43: use function array_last;
44: use function array_map;
45: use function array_merge;
46: use function count;
47: use function in_array;
48: use function strtolower;
49: use function substr;
50: use const COUNT_NORMAL;
51:
52: #[AutowiredService(name: 'typeSpecifier', factory: '@typeSpecifierFactory::create')]
53: final class TypeSpecifier
54: {
55:
56: /** @var MethodTypeSpecifyingExtension[][]|null */
57: private ?array $methodTypeSpecifyingExtensionsByClass = null;
58:
59: /** @var StaticMethodTypeSpecifyingExtension[][]|null */
60: private ?array $staticMethodTypeSpecifyingExtensionsByClass = null;
61:
62: /**
63: * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions
64: * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions
65: * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions
66: */
67: public function __construct(
68: private ExprPrinter $exprPrinter,
69: private ReflectionProvider $reflectionProvider,
70: private array $functionTypeSpecifyingExtensions,
71: private array $methodTypeSpecifyingExtensions,
72: private array $staticMethodTypeSpecifyingExtensions,
73: private bool $rememberPossiblyImpureFunctionValues,
74: private Container $container,
75: )
76: {
77: }
78:
79: /**
80: * @api
81: */
82: public function specifyTypesInCondition(
83: Scope $scope,
84: Expr $expr,
85: TypeSpecifierContext $context,
86: ): SpecifiedTypes
87: {
88: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
89: return (new SpecifiedTypes([], []))->setRootExpr($expr);
90: }
91:
92: /** @var ExprHandler<Expr> $exprHandler */
93: foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) {
94: if (!$exprHandler->supports($expr)) {
95: continue;
96: }
97:
98: return $exprHandler->specifyTypes($this, $scope, $expr, $context);
99: }
100:
101: return $this->specifyDefaultTypes($scope, $expr, $context);
102: }
103:
104: /** @internal */
105: public function isNormalCountCall(FuncCall $countFuncCall, Type $typeToCount, Scope $scope): TrinaryLogic
106: {
107: if (count($countFuncCall->getArgs()) === 1) {
108: return TrinaryLogic::createYes();
109: }
110:
111: $mode = $scope->getType($countFuncCall->getArgs()[1]->value);
112: return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($typeToCount->getIterableValueType()->isArray()->negate());
113: }
114:
115: /** @internal */
116: public function specifyTypesForCountFuncCall(
117: FuncCall $countFuncCall,
118: Type $type,
119: Type $sizeType,
120: TypeSpecifierContext $context,
121: Scope $scope,
122: Expr $rootExpr,
123: ): ?SpecifiedTypes
124: {
125: $isConstantArray = $type->isConstantArray();
126: $isList = $type->isList();
127: $oneOrMore = IntegerRangeType::fromInterval(1, null);
128: if (
129: !$this->isNormalCountCall($countFuncCall, $type, $scope)->yes()
130: || (!$isConstantArray->yes() && !$isList->yes())
131: || !$oneOrMore->isSuperTypeOf($sizeType)->yes()
132: || $sizeType->isSuperTypeOf($type->getArraySize())->yes()
133: ) {
134: return null;
135: }
136:
137: if ($context->falsey() && $isConstantArray->yes()) {
138: $remainingSize = TypeCombinator::remove($type->getArraySize(), $sizeType);
139: if (!$remainingSize instanceof NeverType) {
140: $negatedContext = $context->false()
141: ? TypeSpecifierContext::createTrue()
142: : TypeSpecifierContext::createTruthy();
143: $result = $this->specifyTypesForCountFuncCall(
144: $countFuncCall,
145: $type,
146: $remainingSize,
147: $negatedContext,
148: $scope,
149: $rootExpr,
150: );
151: if ($result !== null) {
152: return $result;
153: }
154: }
155:
156: // Fallback: directly filter constant arrays by their exact sizes.
157: // This avoids using TypeCombinator::remove() with falsey context,
158: // which can incorrectly remove arrays whose count doesn't match
159: // but whose shape is a subtype of the matched array.
160: $keptTypes = [];
161: foreach ($type->getConstantArrays() as $arrayType) {
162: if ($sizeType->isSuperTypeOf($arrayType->getArraySize())->yes()) {
163: continue;
164: }
165:
166: $keptTypes[] = $arrayType;
167: }
168: if ($keptTypes !== []) {
169: return $this->create(
170: $countFuncCall->getArgs()[0]->value,
171: TypeCombinator::union(...$keptTypes),
172: $context->negate(),
173: $scope,
174: )->setRootExpr($rootExpr);
175: }
176: }
177:
178: $resultTypes = [];
179: foreach ($type->getArrays() as $arrayType) {
180: $isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize());
181: if ($isSizeSuperTypeOfArraySize->no()) {
182: continue;
183: }
184:
185: if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) {
186: continue;
187: }
188:
189: $resultTypes[] = $isList->yes()
190: ? $arrayType->truncateListToSize($sizeType)
191: : TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
192: }
193:
194: if ($context->truthy() && $isConstantArray->yes() && $isList->yes()) {
195: $hasOptionalKeysOrUnsealed = false;
196: foreach ($type->getConstantArrays() as $arrayType) {
197: if ($arrayType->getOptionalKeys() !== [] || $arrayType->isUnsealed()->yes()) {
198: // Unsealed CATs can't be narrowed via the
199: // `HasOffsetValueType`-only shortcut below — the
200: // intersection of an unsealed shape with a single-slot
201: // constraint produces `NeverType`. Fall through to
202: // the full builder-based narrowing, which carries the
203: // unsealed slot via the loop above.
204: $hasOptionalKeysOrUnsealed = true;
205: break;
206: }
207: }
208:
209: if (!$hasOptionalKeysOrUnsealed) {
210: $argExpr = $countFuncCall->getArgs()[0]->value;
211: $argExprString = $this->exprPrinter->printExpr($argExpr);
212:
213: $sizeMin = null;
214: $sizeMax = null;
215: if ($sizeType instanceof ConstantIntegerType) {
216: $sizeMin = $sizeType->getValue();
217: $sizeMax = $sizeType->getValue();
218: } elseif ($sizeType instanceof IntegerRangeType) {
219: $sizeMin = $sizeType->getMin();
220: $sizeMax = $sizeType->getMax();
221: }
222:
223: $sureTypes = [];
224: $sureNotTypes = [];
225:
226: if ($sizeMin !== null && $sizeMin >= 1) {
227: $sureTypes[$argExprString] = [$argExpr, new HasOffsetValueType(new ConstantIntegerType($sizeMin - 1), new MixedType())];
228: }
229: if ($sizeMax !== null) {
230: $sureNotTypes[$argExprString] = [$argExpr, new HasOffsetValueType(new ConstantIntegerType($sizeMax), new MixedType())];
231: }
232:
233: if ($sureTypes !== [] || $sureNotTypes !== []) {
234: return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($rootExpr);
235: }
236: }
237: }
238:
239: return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr);
240: }
241:
242: /**
243: * Fallback used by ExprHandler::specifyTypes implementations that have no
244: * Expr-specific narrowing: applies the default truthy/falsey narrowing, or
245: * returns empty SpecifiedTypes in a null context.
246: *
247: * @internal
248: */
249: public function specifyDefaultTypes(Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
250: {
251: if (!$context->null()) {
252: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
253: }
254:
255: return (new SpecifiedTypes([], []))->setRootExpr($expr);
256: }
257:
258: /** @internal */
259: public function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes
260: {
261: if ($context->null()) {
262: return (new SpecifiedTypes([], []))->setRootExpr($expr);
263: }
264: if (!$context->truthy()) {
265: $type = StaticTypeFactory::truthy();
266: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
267: } elseif (!$context->falsey()) {
268: $type = StaticTypeFactory::falsey();
269: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
270: }
271:
272: return (new SpecifiedTypes([], []))->setRootExpr($expr);
273: }
274:
275: /** @internal */
276: public function specifyTypesFromConditionalReturnType(
277: TypeSpecifierContext $context,
278: Expr\CallLike $call,
279: ParametersAcceptor $parametersAcceptor,
280: Scope $scope,
281: ): ?SpecifiedTypes
282: {
283: if (!$parametersAcceptor instanceof ResolvedFunctionVariant) {
284: return null;
285: }
286:
287: $returnType = $parametersAcceptor->getOriginalParametersAcceptor()->getReturnType();
288: if (!$returnType instanceof ConditionalTypeForParameter) {
289: return null;
290: }
291:
292: if ($context->true()) {
293: $leftType = new ConstantBooleanType(true);
294: $rightType = new ConstantBooleanType(false);
295: } elseif ($context->false()) {
296: $leftType = new ConstantBooleanType(false);
297: $rightType = new ConstantBooleanType(true);
298: } elseif ($context->null()) {
299: $leftType = new MixedType();
300: $rightType = new NeverType();
301: } else {
302: return null;
303: }
304:
305: $argumentExpr = null;
306: $parameters = $parametersAcceptor->getParameters();
307: foreach ($call->getArgs() as $i => $arg) {
308: if ($arg->unpack) {
309: continue;
310: }
311:
312: if ($arg->name !== null) {
313: $paramName = $arg->name->toString();
314: } elseif (isset($parameters[$i])) {
315: $paramName = $parameters[$i]->getName();
316: } else {
317: continue;
318: }
319:
320: if ($returnType->getParameterName() !== '$' . $paramName) {
321: continue;
322: }
323:
324: $argumentExpr = $arg->value;
325: }
326:
327: if ($argumentExpr === null) {
328: return null;
329: }
330:
331: return $this->getConditionalSpecifiedTypes($returnType, $leftType, $rightType, $scope, $argumentExpr);
332: }
333:
334: private function getConditionalSpecifiedTypes(
335: ConditionalTypeForParameter $conditionalType,
336: Type $leftType,
337: Type $rightType,
338: Scope $scope,
339: Expr $argumentExpr,
340: ): ?SpecifiedTypes
341: {
342: $targetType = $conditionalType->getTarget();
343: $ifType = $conditionalType->getIf();
344: $elseType = $conditionalType->getElse();
345:
346: if (
347: (
348: $argumentExpr instanceof Node\Scalar
349: || ($argumentExpr instanceof ConstFetch && in_array(strtolower($argumentExpr->name->toString()), ['true', 'false', 'null'], true))
350: ) && ($ifType instanceof NeverType || $elseType instanceof NeverType)
351: ) {
352: return null;
353: }
354:
355: if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) {
356: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue();
357: } elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) {
358: $context = $conditionalType->isNegated() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
359: } else {
360: return null;
361: }
362:
363: $specifiedTypes = $this->create(
364: $argumentExpr,
365: $targetType,
366: $context,
367: $scope,
368: );
369:
370: if ($targetType instanceof ConstantBooleanType) {
371: if (!$targetType->getValue()) {
372: $context = $context->negate();
373: }
374:
375: $specifiedTypes = $specifiedTypes->unionWith($this->specifyTypesInCondition($scope, $argumentExpr, $context));
376: }
377:
378: return $specifiedTypes;
379: }
380:
381: /** @internal */
382: public function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\CallLike $call, Assertions $assertions, ParametersAcceptor $parametersAcceptor, Scope $scope): ?SpecifiedTypes
383: {
384: if ($context->null()) {
385: $asserts = $assertions->getAsserts();
386: } elseif ($context->true()) {
387: $asserts = $assertions->getAssertsIfTrue();
388: } elseif ($context->false()) {
389: $asserts = $assertions->getAssertsIfFalse();
390: } else {
391: throw new ShouldNotHappenException();
392: }
393:
394: if (count($asserts) === 0) {
395: return null;
396: }
397:
398: $argsMap = [];
399: $parameters = $parametersAcceptor->getParameters();
400: foreach ($call->getArgs() as $i => $arg) {
401: if ($arg->unpack) {
402: continue;
403: }
404:
405: if ($arg->name !== null) {
406: $paramName = $arg->name->toString();
407: } elseif (isset($parameters[$i])) {
408: $paramName = $parameters[$i]->getName();
409: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
410: $lastParameter = array_last($parameters);
411: $paramName = $lastParameter->getName();
412: } else {
413: continue;
414: }
415:
416: $argsMap[$paramName][] = $arg->value;
417: }
418: foreach ($parameters as $parameter) {
419: $name = $parameter->getName();
420: $defaultValue = $parameter->getDefaultValue();
421: if (isset($argsMap[$name]) || $defaultValue === null) {
422: continue;
423: }
424: $argsMap[$name][] = new TypeExpr($defaultValue);
425: }
426:
427: if ($call instanceof MethodCall) {
428: $argsMap['this'] = [$call->var];
429: }
430:
431: /** @var SpecifiedTypes|null $types */
432: $types = null;
433:
434: foreach ($asserts as $assert) {
435: foreach ($argsMap[substr($assert->getParameter()->getParameterName(), 1)] ?? [] as $parameterExpr) {
436: $assertedType = TypeTraverser::map($assert->getType(), static function (Type $type, callable $traverse) use ($argsMap, $scope): Type {
437: if ($type instanceof ConditionalTypeForParameter) {
438: $parameterName = substr($type->getParameterName(), 1);
439: if (array_key_exists($parameterName, $argsMap)) {
440: $type = $traverse($type);
441: if ($type instanceof ConditionalTypeForParameter) {
442: $argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[substr($type->getParameterName(), 1)]));
443: return $type->toConditional($argType);
444: }
445: return $type;
446: }
447: }
448:
449: return $traverse($type);
450: });
451:
452: $assertExpr = $assert->getParameter()->getExpr($parameterExpr);
453:
454: $templateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap();
455: $containsUnresolvedTemplate = false;
456: TypeTraverser::map(
457: $assert->getOriginalType(),
458: static function (Type $type, callable $traverse) use ($templateTypeMap, &$containsUnresolvedTemplate) {
459: if ($type instanceof TemplateType && $type->getScope()->getClassName() !== null) {
460: $resolvedType = $templateTypeMap->getType($type->getName());
461: if ($resolvedType === null || $type->getBound()->equals($resolvedType)) {
462: $containsUnresolvedTemplate = true;
463: return $type;
464: }
465: }
466:
467: return $traverse($type);
468: },
469: );
470:
471: $newTypes = $this->create(
472: $assertExpr,
473: $assertedType,
474: $assert->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(),
475: $scope,
476: )->setRootExpr($containsUnresolvedTemplate || $assert->isEquality() ? $call : null);
477: $types = $types !== null ? $types->unionWith($newTypes) : $newTypes;
478:
479: if (!$context->null() || !$assertedType instanceof ConstantBooleanType) {
480: continue;
481: }
482:
483: $subContext = $assertedType->getValue() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createFalse();
484: if ($assert->isNegated()) {
485: $subContext = $subContext->negate();
486: }
487:
488: $types = $types->unionWith($this->specifyTypesInCondition(
489: $scope,
490: $assertExpr,
491: $subContext,
492: ));
493: }
494: }
495:
496: return $types;
497: }
498:
499: /**
500: * @api
501: */
502: public function create(
503: Expr $expr,
504: Type $type,
505: TypeSpecifierContext $context,
506: Scope $scope,
507: ): SpecifiedTypes
508: {
509: if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) {
510: return (new SpecifiedTypes([], []))->setRootExpr($expr);
511: }
512:
513: $specifiedExprs = [];
514: if ($expr instanceof AlwaysRememberedExpr) {
515: $specifiedExprs[] = $expr;
516: $expr = $expr->expr;
517: }
518:
519: if ($expr instanceof Expr\Assign) {
520: $specifiedExprs[] = $expr->var;
521: $specifiedExprs[] = $expr->expr;
522:
523: while ($expr->expr instanceof Expr\Assign) {
524: $specifiedExprs[] = $expr->expr->var;
525: $expr = $expr->expr;
526: }
527: } elseif ($expr instanceof Expr\AssignOp\Coalesce) {
528: $specifiedExprs[] = $expr->var;
529: } else {
530: $specifiedExprs[] = $expr;
531: }
532:
533: $types = null;
534:
535: foreach ($specifiedExprs as $specifiedExpr) {
536: $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $scope);
537:
538: if ($types === null) {
539: $types = $newTypes;
540: } else {
541: $types = $types->unionWith($newTypes);
542: }
543: }
544:
545: return $types;
546: }
547:
548: private function createForExpr(
549: Expr $expr,
550: Type $type,
551: TypeSpecifierContext $context,
552: Scope $scope,
553: ): SpecifiedTypes
554: {
555: if ($context->true()) {
556: $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no();
557: } elseif ($context->false()) {
558: $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no();
559: }
560:
561: $originalExpr = $expr;
562: if (isset($containsNull) && !$containsNull) {
563: $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr);
564: }
565:
566: if (
567: !$context->null()
568: && $expr instanceof Expr\BinaryOp\Coalesce
569: ) {
570: if (
571: ($context->true() && $type->isSuperTypeOf($scope->getType($expr->right))->no())
572: || ($context->false() && $type->isSuperTypeOf($scope->getType($expr->right))->yes())
573: ) {
574: $expr = $expr->left;
575: }
576: }
577:
578: if (
579: $expr instanceof FuncCall
580: && $expr->name instanceof Name
581: ) {
582: $has = $this->reflectionProvider->hasFunction($expr->name, $scope);
583: if (!$has) {
584: // backwards compatibility with previous behaviour
585: return new SpecifiedTypes([], []);
586: }
587:
588: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
589: $hasSideEffects = $functionReflection->hasSideEffects();
590: if ($hasSideEffects->yes()) {
591: return new SpecifiedTypes([], []);
592: }
593:
594: if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
595: return new SpecifiedTypes([], []);
596: }
597: }
598:
599: if (
600: $expr instanceof FuncCall
601: && !$expr->name instanceof Name
602: ) {
603: $nameType = $scope->getType($expr->name);
604: if ($nameType->isCallable()->yes()) {
605: $isPure = null;
606: foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
607: $variantIsPure = $variant->isPure();
608: $isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
609: }
610:
611: if ($isPure !== null) {
612: if ($isPure->no()) {
613: return new SpecifiedTypes([], []);
614: }
615:
616: if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
617: return new SpecifiedTypes([], []);
618: }
619: }
620: }
621: }
622:
623: if (
624: $expr instanceof MethodCall
625: && $expr->name instanceof Node\Identifier
626: ) {
627: $methodName = $expr->name->toString();
628: $calledOnType = $scope->getType($expr->var);
629: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
630: if (
631: $methodReflection === null
632: || $methodReflection->hasSideEffects()->yes()
633: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
634: ) {
635: if (isset($containsNull) && !$containsNull) {
636: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
637: }
638:
639: return new SpecifiedTypes([], []);
640: }
641: }
642:
643: if (
644: $expr instanceof StaticCall
645: && $expr->name instanceof Node\Identifier
646: ) {
647: $methodName = $expr->name->toString();
648: if ($expr->class instanceof Name) {
649: $calledOnType = $scope->resolveTypeByName($expr->class);
650: } else {
651: $calledOnType = $scope->getType($expr->class);
652: }
653:
654: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
655: if (
656: $methodReflection === null
657: || $methodReflection->hasSideEffects()->yes()
658: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
659: ) {
660: if (isset($containsNull) && !$containsNull) {
661: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
662: }
663:
664: return new SpecifiedTypes([], []);
665: }
666: }
667:
668: $sureTypes = [];
669: $sureNotTypes = [];
670: if ($context->false()) {
671: $exprString = $this->exprPrinter->printExpr($expr);
672: $sureNotTypes[$exprString] = [$expr, $type];
673:
674: if ($expr !== $originalExpr) {
675: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
676: $sureNotTypes[$originalExprString] = [$originalExpr, $type];
677: }
678: } elseif ($context->true()) {
679: $exprString = $this->exprPrinter->printExpr($expr);
680: $sureTypes[$exprString] = [$expr, $type];
681:
682: if ($expr !== $originalExpr) {
683: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
684: $sureTypes[$originalExprString] = [$originalExpr, $type];
685: }
686: }
687:
688: $types = new SpecifiedTypes($sureTypes, $sureNotTypes);
689: if (isset($containsNull) && !$containsNull) {
690: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types);
691: }
692:
693: return $types;
694: }
695:
696: private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes
697: {
698: if ($expr instanceof Expr\NullsafePropertyFetch) {
699: if ($type !== null) {
700: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, $scope);
701: } else {
702: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), $scope);
703: }
704:
705: return $propertyFetchTypes->unionWith(
706: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
707: );
708: }
709:
710: if ($expr instanceof Expr\NullsafeMethodCall) {
711: if ($type !== null) {
712: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $scope);
713: } else {
714: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $scope);
715: }
716:
717: return $methodCallTypes->unionWith(
718: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
719: );
720: }
721:
722: if ($expr instanceof Expr\PropertyFetch) {
723: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
724: }
725:
726: if ($expr instanceof Expr\MethodCall) {
727: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
728: }
729:
730: if ($expr instanceof Expr\ArrayDimFetch) {
731: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
732: }
733:
734: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
735: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
736: }
737:
738: if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
739: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
740: }
741:
742: return new SpecifiedTypes([], []);
743: }
744:
745: /**
746: * @return FunctionTypeSpecifyingExtension[]
747: *
748: * @internal
749: */
750: public function getFunctionTypeSpecifyingExtensions(): array
751: {
752: return $this->functionTypeSpecifyingExtensions;
753: }
754:
755: /**
756: * @return MethodTypeSpecifyingExtension[]
757: *
758: * @internal
759: */
760: public function getMethodTypeSpecifyingExtensionsForClass(string $className): array
761: {
762: if ($this->methodTypeSpecifyingExtensionsByClass === null) {
763: $byClass = [];
764: foreach ($this->methodTypeSpecifyingExtensions as $extension) {
765: $byClass[$extension->getClass()][] = $extension;
766: }
767:
768: $this->methodTypeSpecifyingExtensionsByClass = $byClass;
769: }
770: return $this->getTypeSpecifyingExtensionsForType($this->methodTypeSpecifyingExtensionsByClass, $className);
771: }
772:
773: /**
774: * @return StaticMethodTypeSpecifyingExtension[]
775: *
776: * @internal
777: */
778: public function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
779: {
780: if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
781: $byClass = [];
782: foreach ($this->staticMethodTypeSpecifyingExtensions as $extension) {
783: $byClass[$extension->getClass()][] = $extension;
784: }
785:
786: $this->staticMethodTypeSpecifyingExtensionsByClass = $byClass;
787: }
788: return $this->getTypeSpecifyingExtensionsForType($this->staticMethodTypeSpecifyingExtensionsByClass, $className);
789: }
790:
791: /**
792: * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions
793: * @return mixed[]
794: */
795: private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array
796: {
797: $extensionsForClass = [[]];
798: $class = $this->reflectionProvider->getClass($className);
799: foreach (array_merge([$className], $class->getParentClassesNames(), $class->getNativeReflection()->getInterfaceNames()) as $extensionClassName) {
800: if (!isset($extensions[$extensionClassName])) {
801: continue;
802: }
803:
804: $extensionsForClass[] = $extensions[$extensionClassName];
805: }
806:
807: return array_merge(...$extensionsForClass);
808: }
809:
810: }
811: