diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 229dccb72d..96bf233209 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -38,4 +38,9 @@ public function supportsNamedArguments(): TrinaryLogic return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; } + public function supportsNamedArgumentAfterUnpackedArgument(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80100, null)->isSuperTypeOf($this->phpVersions)->result; + } + } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 50371ab23c..bd637913bc 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -101,6 +101,12 @@ public function check( $hasUnpackedArgument = false; $errors = []; foreach ($args as $arg) { + $argumentName = null; + if ($arg->name !== null) { + $hasNamedArguments = true; + $argumentName = $arg->name->toString(); + } + if ($hasNamedArguments && $arg->unpack) { $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.') ->identifier('argument.unpackAfterNamed') @@ -109,20 +115,17 @@ public function check( ->build(); } if ($hasUnpackedArgument && !$arg->unpack) { - $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') - ->identifier('argument.nonUnpackAfterUnpacked') - ->line($arg->getStartLine()) - ->nonIgnorable() - ->build(); + if ($argumentName === null || !$scope->getPhpVersion()->supportsNamedArgumentAfterUnpackedArgument()->yes()) { + $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') + ->identifier('argument.nonUnpackAfterUnpacked') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); + } } if ($arg->unpack) { $hasUnpackedArgument = true; } - $argumentName = null; - if ($arg->name !== null) { - $hasNamedArguments = true; - $argumentName = $arg->name->toString(); - } if ($arg->unpack) { $type = $scope->getType($arg->value); $arrays = $type->getConstantArrays(); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a6513f03cb..2e8afa35a4 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -499,6 +499,20 @@ public function testNamedArguments(): void $this->analyse([__DIR__ . '/data/named-arguments.php'], $errors); } + public function testNamedArgumentsAfterUnpacking(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/named-arguments-after-unpacking.php'], [ + [ + 'Named parameter cannot overwrite already unpacked argument $b.', + 14, + ], + ]); + } + public function testBug4514(): void { $this->analyse([__DIR__ . '/data/bug-4514.php'], []); @@ -1936,4 +1950,22 @@ public function testBug12051(): void $this->analyse([__DIR__ . '/data/bug-12051.php'], []); } + public function testBug8046(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8046.php'], []); + } + + public function testBug11418(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11418.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11418.php b/tests/PHPStan/Rules/Functions/data/bug-11418.php new file mode 100755 index 0000000000..8172892d95 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11418.php @@ -0,0 +1,9 @@ + 7]; + +var_dump(add(...$args, b: 8)); diff --git a/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php new file mode 100755 index 0000000000..29d9ac8b4e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php @@ -0,0 +1,14 @@ + 2, 'a' => 1], d: 40)); // 46 + +var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument