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