From 228d2ea427956664fc268aaa5ea2a4fd5c6d076d Mon Sep 17 00:00:00 2001 From: Dan Hemberger Date: Thu, 16 Apr 2026 23:34:11 -0700 Subject: [PATCH 1/4] DoctrineKeyValueStyleRule: support named parameters Since we configure which arguments to check by their position, this can break if the arguments are specified at the call site with named parameters (PHP 8.0+), which allows arguments to be passed out of order. By mapping named parameter arguments to the actual index in the function interface, we can properly handle out of order calls. --- src/Rules/DoctrineKeyValueStyleRule.php | 29 +++++++++++++++++-- tests/rules/DoctrineKeyValueStyleRuleTest.php | 14 +++++++++ ...trine-key-value-style-named-parameters.php | 18 ++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 tests/rules/data/doctrine-key-value-style-named-parameters.php diff --git a/src/Rules/DoctrineKeyValueStyleRule.php b/src/Rules/DoctrineKeyValueStyleRule.php index 0ab613e2..7c4663c2 100644 --- a/src/Rules/DoctrineKeyValueStyleRule.php +++ b/src/Rules/DoctrineKeyValueStyleRule.php @@ -115,7 +115,30 @@ public function processNode(Node $callLike, Scope $scope): array return []; } - $tableExpr = $args[0]->value; + // Map function parameter names to parameter index + $params = $methodReflection->getVariants()[0]->getParameters(); + $paramNameToIndex = []; + foreach ($params as $i => $param) { + $paramNameToIndex[$param->getName()] = $i; + } + + // Map parameter positions to actual call arguments + $paramIndexToArg = []; + foreach ($args as $i => $arg) { + if ($arg->name === null) { + // Positional argument + $paramIndexToArg[$i] = $arg; + } else { + // Named argument (PHP 8.0+) + $name = $arg->name->toString(); + $index = $paramNameToIndex[$name] ?? null; + if ($index !== null) { + $paramIndexToArg[$index] = $arg; + } + } + } + + $tableExpr = $paramIndexToArg[0]->value; $tableType = $scope->getType($tableExpr); $tableNames = $tableType->getConstantStrings(); if (\count($tableNames) === 0) { @@ -143,11 +166,11 @@ public function processNode(Node $callLike, Scope $scope): array foreach ($arrayArgPositions as $arrayArgPosition) { // If the argument doesn't exist, just skip it since we don't want // to error in case it has a default value - if (! \array_key_exists($arrayArgPosition, $args)) { + if (! \array_key_exists($arrayArgPosition, $paramIndexToArg)) { continue; } - $argType = $scope->getType($args[$arrayArgPosition]->value); + $argType = $scope->getType($paramIndexToArg[$arrayArgPosition]->value); $argArrays = $argType->getConstantArrays(); if (\count($argArrays) === 0) { $errors[] = 'Argument #' . $arrayArgPosition . ' is not a constant array, got ' . $argType->describe(VerbosityLevel::precise()); diff --git a/tests/rules/DoctrineKeyValueStyleRuleTest.php b/tests/rules/DoctrineKeyValueStyleRuleTest.php index e87a4f36..42dcf423 100644 --- a/tests/rules/DoctrineKeyValueStyleRuleTest.php +++ b/tests/rules/DoctrineKeyValueStyleRuleTest.php @@ -111,4 +111,18 @@ public function testLaxIntegerRanges(): void { $this->analyse([__DIR__ . '/data/doctrine-key-value-style-integer-ranges.php'], []); } + + public function testNamedParameters(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/doctrine-key-value-style-named-parameters.php'], [ + [ + 'Query error: Column "ada.not_a_column" does not exist', + 16, + ], + ]); + } } diff --git a/tests/rules/data/doctrine-key-value-style-named-parameters.php b/tests/rules/data/doctrine-key-value-style-named-parameters.php new file mode 100644 index 00000000..29c7e982 --- /dev/null +++ b/tests/rules/data/doctrine-key-value-style-named-parameters.php @@ -0,0 +1,18 @@ +assembleOneArray(cols: ['email' => 'foo'], tableName: 'ada'); + } + + public function errorWithNamedParameters(Connection $conn) + { + $conn->assembleOneArray(cols: ['not_a_column' => 'foo'], tableName: 'ada'); + } +} From 61d8fc763888b76ab4a4ba0dcf09468feba8c5f1 Mon Sep 17 00:00:00 2001 From: Dan Hemberger <846186+hemberger@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:38:05 -0700 Subject: [PATCH 2/4] fixup: restrict linting to php 8.0+ Apply suggestion from code review. Co-authored-by: Markus Staab --- tests/rules/data/doctrine-key-value-style-named-parameters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rules/data/doctrine-key-value-style-named-parameters.php b/tests/rules/data/doctrine-key-value-style-named-parameters.php index 29c7e982..2c173725 100644 --- a/tests/rules/data/doctrine-key-value-style-named-parameters.php +++ b/tests/rules/data/doctrine-key-value-style-named-parameters.php @@ -1,4 +1,4 @@ -= 8.0 namespace DoctrineKeyValueStyleRuleTest; From 4c64a77bf52d9fcc01135badf82d3a770ee7141e Mon Sep 17 00:00:00 2001 From: Dan Hemberger Date: Fri, 17 Apr 2026 10:05:09 -0700 Subject: [PATCH 3/4] fixup: use ArgumentsNormalizer instead of manually reordering args --- src/Rules/DoctrineKeyValueStyleRule.php | 47 +++++++++++-------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/Rules/DoctrineKeyValueStyleRule.php b/src/Rules/DoctrineKeyValueStyleRule.php index 7c4663c2..803e1627 100644 --- a/src/Rules/DoctrineKeyValueStyleRule.php +++ b/src/Rules/DoctrineKeyValueStyleRule.php @@ -9,7 +9,9 @@ use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; use PhpParser\Node\Name\FullyQualified; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -109,36 +111,29 @@ public function processNode(Node $callLike, Scope $scope): array return []; } - $args = $callLike->getArgs(); + // Reorder arguments to account for named parameters + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $callLike->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); - if (\count($args) < 1) { - return []; - } + $reorderedMethodCall = ArgumentsNormalizer::reorderMethodArguments( + $parametersAcceptor, + $callLike, + ); - // Map function parameter names to parameter index - $params = $methodReflection->getVariants()[0]->getParameters(); - $paramNameToIndex = []; - foreach ($params as $i => $param) { - $paramNameToIndex[$param->getName()] = $i; + if ($reorderedMethodCall === null) { + return []; } + $reorderedArgs = $reorderedMethodCall->getArgs(); - // Map parameter positions to actual call arguments - $paramIndexToArg = []; - foreach ($args as $i => $arg) { - if ($arg->name === null) { - // Positional argument - $paramIndexToArg[$i] = $arg; - } else { - // Named argument (PHP 8.0+) - $name = $arg->name->toString(); - $index = $paramNameToIndex[$name] ?? null; - if ($index !== null) { - $paramIndexToArg[$index] = $arg; - } - } + if (\count($reorderedArgs) < 1) { + return []; } - $tableExpr = $paramIndexToArg[0]->value; + $tableExpr = $reorderedArgs[0]->value; $tableType = $scope->getType($tableExpr); $tableNames = $tableType->getConstantStrings(); if (\count($tableNames) === 0) { @@ -166,11 +161,11 @@ public function processNode(Node $callLike, Scope $scope): array foreach ($arrayArgPositions as $arrayArgPosition) { // If the argument doesn't exist, just skip it since we don't want // to error in case it has a default value - if (! \array_key_exists($arrayArgPosition, $paramIndexToArg)) { + if (! \array_key_exists($arrayArgPosition, $reorderedArgs)) { continue; } - $argType = $scope->getType($paramIndexToArg[$arrayArgPosition]->value); + $argType = $scope->getType($reorderedArgs[$arrayArgPosition]->value); $argArrays = $argType->getConstantArrays(); if (\count($argArrays) === 0) { $errors[] = 'Argument #' . $arrayArgPosition . ' is not a constant array, got ' . $argType->describe(VerbosityLevel::precise()); From 8e30015102d373cfb63dbb00385836c5a22ae258 Mon Sep 17 00:00:00 2001 From: Dan Hemberger Date: Fri, 17 Apr 2026 10:23:59 -0700 Subject: [PATCH 4/4] fixup: handle both MethodCall and New_ --- src/Rules/DoctrineKeyValueStyleRule.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Rules/DoctrineKeyValueStyleRule.php b/src/Rules/DoctrineKeyValueStyleRule.php index 803e1627..a8e408c6 100644 --- a/src/Rules/DoctrineKeyValueStyleRule.php +++ b/src/Rules/DoctrineKeyValueStyleRule.php @@ -119,15 +119,22 @@ public function processNode(Node $callLike, Scope $scope): array $methodReflection->getNamedArgumentsVariants(), ); - $reorderedMethodCall = ArgumentsNormalizer::reorderMethodArguments( - $parametersAcceptor, - $callLike, - ); + if ($callLike instanceof MethodCall) { + $reorderedCall = ArgumentsNormalizer::reorderMethodArguments( + $parametersAcceptor, + $callLike, + ); + } else { + $reorderedCall = ArgumentsNormalizer::reorderNewArguments( + $parametersAcceptor, + $callLike, + ); + } - if ($reorderedMethodCall === null) { + if ($reorderedCall === null) { return []; } - $reorderedArgs = $reorderedMethodCall->getArgs(); + $reorderedArgs = $reorderedCall->getArgs(); if (\count($reorderedArgs) < 1) { return [];