Skip to content

Commit b102d98

Browse files
authored
Support named arguments after unpacking
1 parent 13f6406 commit b102d98

File tree

6 files changed

+84
-10
lines changed

6 files changed

+84
-10
lines changed

src/Php/PhpVersions.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,9 @@ public function supportsNamedArguments(): TrinaryLogic
3838
return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result;
3939
}
4040

41+
public function supportsNamedArgumentAfterUnpackedArgument(): TrinaryLogic
42+
{
43+
return IntegerRangeType::fromInterval(80100, null)->isSuperTypeOf($this->phpVersions)->result;
44+
}
45+
4146
}

src/Rules/FunctionCallParametersCheck.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ public function check(
101101
$hasUnpackedArgument = false;
102102
$errors = [];
103103
foreach ($args as $arg) {
104+
$argumentName = null;
105+
if ($arg->name !== null) {
106+
$hasNamedArguments = true;
107+
$argumentName = $arg->name->toString();
108+
}
109+
104110
if ($hasNamedArguments && $arg->unpack) {
105111
$errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.')
106112
->identifier('argument.unpackAfterNamed')
@@ -109,20 +115,17 @@ public function check(
109115
->build();
110116
}
111117
if ($hasUnpackedArgument && !$arg->unpack) {
112-
$errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.')
113-
->identifier('argument.nonUnpackAfterUnpacked')
114-
->line($arg->getStartLine())
115-
->nonIgnorable()
116-
->build();
118+
if ($argumentName === null || !$scope->getPhpVersion()->supportsNamedArgumentAfterUnpackedArgument()->yes()) {
119+
$errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.')
120+
->identifier('argument.nonUnpackAfterUnpacked')
121+
->line($arg->getStartLine())
122+
->nonIgnorable()
123+
->build();
124+
}
117125
}
118126
if ($arg->unpack) {
119127
$hasUnpackedArgument = true;
120128
}
121-
$argumentName = null;
122-
if ($arg->name !== null) {
123-
$hasNamedArguments = true;
124-
$argumentName = $arg->name->toString();
125-
}
126129
if ($arg->unpack) {
127130
$type = $scope->getType($arg->value);
128131
$arrays = $type->getConstantArrays();

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,20 @@ public function testNamedArguments(): void
499499
$this->analyse([__DIR__ . '/data/named-arguments.php'], $errors);
500500
}
501501

502+
public function testNamedArgumentsAfterUnpacking(): void
503+
{
504+
if (PHP_VERSION_ID < 80100) {
505+
$this->markTestSkipped('Test requires PHP 8.1.');
506+
}
507+
508+
$this->analyse([__DIR__ . '/data/named-arguments-after-unpacking.php'], [
509+
[
510+
'Named parameter cannot overwrite already unpacked argument $b.',
511+
14,
512+
],
513+
]);
514+
}
515+
502516
public function testBug4514(): void
503517
{
504518
$this->analyse([__DIR__ . '/data/bug-4514.php'], []);
@@ -1936,4 +1950,22 @@ public function testBug12051(): void
19361950
$this->analyse([__DIR__ . '/data/bug-12051.php'], []);
19371951
}
19381952

1953+
public function testBug8046(): void
1954+
{
1955+
if (PHP_VERSION_ID < 80100) {
1956+
$this->markTestSkipped('Test requires PHP 8.1.');
1957+
}
1958+
1959+
$this->analyse([__DIR__ . '/data/bug-8046.php'], []);
1960+
}
1961+
1962+
public function testBug11418(): void
1963+
{
1964+
if (PHP_VERSION_ID < 80100) {
1965+
$this->markTestSkipped('Test requires PHP 8.1.');
1966+
}
1967+
1968+
$this->analyse([__DIR__ . '/data/bug-11418.php'], []);
1969+
}
1970+
19391971
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Bug11418;
4+
5+
function foo(int $a, int $b, int $c = 3, int $d = 4): int {
6+
return $a + $b + $c + $d;
7+
}
8+
9+
var_dump(foo(...[1, 2], d: 40)); // 46
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug8046;
4+
5+
function add(int $a, int $b): int {
6+
return $a + $b;
7+
}
8+
9+
$args = ['a' => 7];
10+
11+
var_dump(add(...$args, b: 8));
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace FunctionNamedArgumentsAfterUnpacking;
4+
5+
// https://www.php.net/manual/en/functions.arguments.php#example-180
6+
7+
function foo($a, $b, $c = 3, $d = 4) {
8+
return $a + $b + $c + $d;
9+
}
10+
11+
var_dump(foo(...[1, 2], d: 40)); // 46
12+
var_dump(foo(...['b' => 2, 'a' => 1], d: 40)); // 46
13+
14+
var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument

0 commit comments

Comments
 (0)