From 9a3c88ca8d810d6fe38fc97dc645692380ab3a35 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:02:09 +0000 Subject: [PATCH 1/2] Fix method call error reported on wrong line for chained calls - FunctionCallParametersCheck used $funcCall->getStartLine() which returns the line of the entire expression (e.g. $this), not the method name line - For MethodCall, StaticCall, and FuncCall, now use $funcCall->name->getStartLine() to report errors on the line where the actual method/function name appears - New regression test in tests/PHPStan/Rules/Methods/data/bug-9820.php Closes https://github.com/phpstan/phpstan/issues/9820 --- src/Rules/FunctionCallParametersCheck.php | 22 +++++++++------ .../Rules/Methods/CallMethodsRuleTest.php | 17 ++++++++++++ tests/PHPStan/Rules/Methods/data/bug-9820.php | 27 +++++++++++++++++++ 3 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9820.php diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 74ceec9c00..16c0938b06 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -89,6 +89,12 @@ public function check( string $namedArgumentMessage, ): array { + if ($funcCall instanceof Node\Expr\MethodCall || $funcCall instanceof Node\Expr\StaticCall || $funcCall instanceof Node\Expr\FuncCall) { + $funcCallLine = $funcCall->name->getStartLine(); + } else { + $funcCallLine = $funcCall->getStartLine(); + } + $functionParametersMinCount = 0; $functionParametersMaxCount = 0; foreach ($parametersAcceptor->getParameters() as $parameter) { @@ -225,7 +231,7 @@ public function check( if ($hasNamedArguments && !$scope->getPhpVersion()->supportsNamedArguments()->yes() && !(bool) $funcCall->getAttribute('isAttribute', false)) { $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.') ->identifier('argument.namedNotSupported') - ->line($funcCall->getStartLine()) + ->line($funcCallLine) ->nonIgnorable() ->build(); } @@ -250,7 +256,7 @@ public function check( $functionParametersMinCount, )) ->identifier('arguments.count') - ->line($funcCall->getStartLine()) + ->line($funcCallLine) ->build(); } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -259,7 +265,7 @@ public function check( $functionParametersMinCount, )) ->identifier('arguments.count') - ->line($funcCall->getStartLine()) + ->line($funcCallLine) ->build(); } elseif ($functionParametersMaxCount !== -1) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -269,7 +275,7 @@ public function check( $functionParametersMaxCount, )) ->identifier('arguments.count') - ->line($funcCall->getStartLine()) + ->line($funcCallLine) ->build(); } } @@ -282,11 +288,11 @@ public function check( ) { $errors[] = RuleErrorBuilder::message($voidReturnTypeUsed) ->identifier(sprintf('%s.void', $nodeType)) - ->line($funcCall->getStartLine()) + ->line($funcCallLine) ->build(); } - [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCall->getStartLine(), $isBuiltin, $arguments, $hasNamedArguments, $missingParameterMessage, $unknownParameterMessage); + [$addedErrors, $argumentsWithParameters] = $this->processArguments($parametersAcceptor, $funcCallLine, $isBuiltin, $arguments, $hasNamedArguments, $missingParameterMessage, $unknownParameterMessage); foreach ($addedErrors as $error) { $errors[] = $error; } @@ -528,7 +534,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty $errors[] = RuleErrorBuilder::message(sprintf($unresolvableTemplateTypeMessage, $name)) ->identifier('argument.templateType') - ->line($funcCall->getStartLine()) + ->line($funcCallLine) ->tip('See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type') ->build(); } @@ -540,7 +546,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty ) { $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage) ->identifier(sprintf('%s.unresolvableReturnType', $nodeType)) - ->line($funcCall->getStartLine()) + ->line($funcCallLine) ->build(); } } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 0ab14fd52a..80c6470aba 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3853,4 +3853,21 @@ public function testBug13805(): void $this->analyse([__DIR__ . '/data/bug-13805.php'], []); } + public function testBug9820(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-9820.php'], [ + [ + 'Method Bug9820\HelloWorld::x() invoked with 1 parameter, 0 required.', + 18, + ], + [ + 'Method Bug9820\HelloWorld::x() invoked with 1 parameter, 0 required.', + 25, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9820.php b/tests/PHPStan/Rules/Methods/data/bug-9820.php new file mode 100644 index 0000000000..91ffd74f5a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9820.php @@ -0,0 +1,27 @@ +x(1); + } + + public function test2(): void + { + $this + ->x() + ->x(1); + } +} From 549aec9f6d313b56cce61e36b4c6513840d6108e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:14:18 +0000 Subject: [PATCH 2/2] Add test for nullsafe method call error line reporting Co-authored-by: Markus Staab --- .../Rules/Methods/CallMethodsRuleTest.php | 12 ++++++++++-- tests/PHPStan/Rules/Methods/data/bug-9820.php | 17 ++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 80c6470aba..1c56de9b57 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3861,11 +3861,19 @@ public function testBug9820(): void $this->analyse([__DIR__ . '/data/bug-9820.php'], [ [ 'Method Bug9820\HelloWorld::x() invoked with 1 parameter, 0 required.', - 18, + 20, ], [ 'Method Bug9820\HelloWorld::x() invoked with 1 parameter, 0 required.', - 25, + 27, + ], + [ + 'Method Bug9820\HelloWorld::x() invoked with 1 parameter, 0 required.', + 33, + ], + [ + 'Method Bug9820\HelloWorld::x() invoked with 1 parameter, 0 required.', + 40, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9820.php b/tests/PHPStan/Rules/Methods/data/bug-9820.php index 91ffd74f5a..3c1f819aaa 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-9820.php +++ b/tests/PHPStan/Rules/Methods/data/bug-9820.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace Bug9820; @@ -24,4 +26,17 @@ public function test2(): void ->x() ->x(1); } + + public function test3(?self $selfOrNull): void + { + $selfOrNull + ?->x(1); + } + + public function test4(?self $selfOrNull): void + { + $selfOrNull + ?->x() + ?->x(1); + } }