diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6be5088a39..5b6f2a7b4e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2653,7 +2653,7 @@ public function enterAnonymousFunction( $scope->getNamespace(), $scope->expressionTypes, $scope->nativeExpressionTypes, - [], + $scope->conditionalExpressions, $scope->inClosureBindScopeClasses, $anonymousFunctionReflection, true, @@ -2703,12 +2703,14 @@ private function enterAnonymousFunctionWithoutReflection( } $nonRefVariableNames = []; + $useVariableNames = []; foreach ($closure->uses as $use) { if (!is_string($use->var->name)) { throw new ShouldNotHappenException(); } $variableName = $use->var->name; $paramExprString = '$' . $use->var->name; + $useVariableNames[$paramExprString] = true; if ($use->byRef) { $holder = ExpressionTypeHolder::createYes($use->var, new MixedType()); $expressionTypes[$paramExprString] = $holder; @@ -2774,6 +2776,25 @@ private function enterAnonymousFunctionWithoutReflection( } } + $filteredConditionalExpressions = []; + foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { + if (!array_key_exists($conditionalExprString, $useVariableNames)) { + continue; + } + $filteredHolders = []; + foreach ($holders as $holder) { + foreach ($holder->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $useVariableNames)) { + continue 2; + } + } + $filteredHolders[] = $holder; + } + if ($filteredHolders !== []) { + $filteredConditionalExpressions[$conditionalExprString] = $filteredHolders; + } + } + return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), @@ -2781,7 +2802,7 @@ private function enterAnonymousFunctionWithoutReflection( $this->getNamespace(), array_merge($this->getConstantTypes(), $expressionTypes), array_merge($this->getNativeConstantTypes(), $nativeTypes), - [], + $filteredConditionalExpressions, $this->inClosureBindScopeClasses, new ClosureType(), true, diff --git a/tests/PHPStan/Analyser/nsrt/bug-12875.php b/tests/PHPStan/Analyser/nsrt/bug-12875.php new file mode 100644 index 0000000000..f2c8fa71d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12875.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug12875; + +use function PHPStan\Testing\assertType; + +interface HasFoo +{ + public function foo(): int; +} + +interface HasBar +{ + public function bar(): int; +} + +class HelloWorld +{ + /** + * @param "foo"|"bar" $method + * @param ($method is "foo" ? HasFoo : HasBar) $a + * @param ($method is "foo" ? HasFoo : HasBar) $b + */ + public function add(string $method, HasFoo|HasBar $a, HasFoo|HasBar $b): void + { + assertType('int', $a->{$method}()); + assertType('int', $b->{$method}()); + + $addInArrow = fn () => assertType('int', $a->{$method}()); + + $addInAnonymous = function () use ($a, $b, $method): void { + assertType('int', $a->{$method}()); + assertType('int', $b->{$method}()); + }; + } +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 32b0b054aa..edca41a237 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3827,4 +3827,13 @@ public function testDiscussion14038(): void $this->analyse([__DIR__ . '/data/discussion-14038.php'], []); } + public function testBug12875(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12875.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-12875.php b/tests/PHPStan/Rules/Methods/data/bug-12875.php new file mode 100644 index 0000000000..c262814452 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12875.php @@ -0,0 +1,30 @@ += 8.0 + +namespace Bug12875; + +interface HasFoo +{ + public function foo(): int; +} + +interface HasBar +{ + public function bar(): int; +} + +class HelloWorld +{ + /** + * @param "foo"|"bar" $method + * @param ($method is "foo" ? HasFoo : HasBar) $a + * @param ($method is "foo" ? HasFoo : HasBar) $b + */ + public function add(string $method, HasFoo|HasBar $a, HasFoo|HasBar $b): void + { + $add = $a->{$method}() + $b->{$method}(); + $addInArrow = fn () => $a->{$method}() + $b->{$method}(); + $addInAnonymous = function () use ($a, $b, $method): int { + return $a->{$method}() + $b->{$method}(); + }; + } +}