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