diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 33e32539ff..ca9aded25d 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -105,6 +105,7 @@ use PHPStan\Type\VoidType; use Throwable; use function abs; +use function array_fill_keys; use function array_filter; use function array_key_exists; use function array_key_first; @@ -3529,6 +3530,8 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } } + $scope = $scope->refreshDependentCallsAndFetches(array_keys($specifiedExpressions)); + /** @var static */ return $scope->scopeFactory->create( $scope->context, @@ -3550,6 +3553,105 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self ); } + /** + * When a receiver expression is narrowed, method calls and property fetches + * on it that were narrowed by earlier control flow must be refined too, so + * their remembered type combines with the one derived from the now-narrower + * receiver. + * + * @param string[] $narrowedExprStrings + */ + private function refreshDependentCallsAndFetches(array $narrowedExprStrings): self + { + if (count($narrowedExprStrings) === 0) { + return $this; + } + + $narrowedExprStringsMap = array_fill_keys($narrowedExprStrings, true); + + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $changed = false; + + foreach ($this->expressionTypes as $exprString => $holder) { + $expr = $holder->getExpr(); + if ( + !$expr instanceof MethodCall + && !$expr instanceof Expr\NullsafeMethodCall + && !$expr instanceof PropertyFetch + && !$expr instanceof Expr\NullsafePropertyFetch + ) { + continue; + } + + if (($expr instanceof MethodCall || $expr instanceof Expr\NullsafeMethodCall) && $expr->isFirstClassCallable()) { + continue; + } + + if (!array_key_exists($this->getNodeKey($expr->var), $narrowedExprStringsMap)) { + continue; + } + + $freshType = $this->resolveExprHandlerType($this, $expr); + $newType = TypeCombinator::intersect($holder->getType(), $freshType); + if (!$newType->equals($holder->getType())) { + $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $newType, $holder->getCertainty()); + $changed = true; + } + + if (!array_key_exists($exprString, $nativeExpressionTypes)) { + continue; + } + + $nativeHolder = $nativeExpressionTypes[$exprString]; + $freshNativeType = $this->resolveExprHandlerType($this->promoteNativeTypes(), $expr); + $newNativeType = TypeCombinator::intersect($nativeHolder->getType(), $freshNativeType); + if ($newNativeType->equals($nativeHolder->getType())) { + continue; + } + + $nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($expr, $newNativeType, $nativeHolder->getCertainty()); + $changed = true; + } + + if (!$changed) { + return $this; + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + private function resolveExprHandlerType(self $scope, Expr $node): Type + { + /** @var ExprHandler $exprHandler */ + foreach ($scope->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { + if (!$exprHandler->supports($node)) { + continue; + } + + return $exprHandler->resolveType($scope, $node); + } + + return new MixedType(); + } + /** * @return array */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-10050.php b/tests/PHPStan/Analyser/nsrt/bug-10050.php new file mode 100644 index 0000000000..54b57cf265 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10050.php @@ -0,0 +1,53 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug10050; + +use function PHPStan\Testing\assertType; + +interface Value +{ + public function get(): int|string|null; +} + +interface StringValue extends Value +{ + public function get(): string; +} + +function testMethod(Value $v): void +{ + assertType('int|string|null', $v->get()); + if ($v->get() === null) { + return; + } + assertType('int|string', $v->get()); + if ($v instanceof StringValue) { + assertType('string', $v->get()); + } + assertType('int|string', $v->get()); +} + +class A +{ + public int|null $p; +} + +class B +{ + public string|null $p; +} + +function testProperty(A|B $x): void +{ + assertType('int|string|null', $x->p); + if ($x->p === null) { + return; + } + assertType('int|string', $x->p); + if ($x instanceof A) { + assertType('int', $x->p); + } + assertType('int|string', $x->p); +}