Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Php/PhpVersions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
51 changes: 35 additions & 16 deletions src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,22 @@ public function check(
$functionParametersMaxCount = -1;
}

/** @var array<int, array{Expr, Type|null, bool, string|null, int}> $arguments */
/** @var array<int, array{Expr, Type|null, bool, string|null, int, bool}> $arguments */
$arguments = [];
/** @var array<int, Node\Arg> $args */
$args = $funcCall->getArgs();
$hasNamedArguments = false;
$hasUnpackedArgument = false;
$errors = [];
foreach ($args as $arg) {
$argumentName = null;
if ($arg->name !== null) {
$hasNamedArguments = true;
$argumentName = $arg->name->toString();
}

$nonUnpackAfterUnpacked = false;

if ($hasNamedArguments && $arg->unpack) {
$errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.')
->identifier('argument.unpackAfterNamed')
Expand All @@ -109,20 +117,19 @@ 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();
$nonUnpackAfterUnpacked = true;

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();
Expand Down Expand Up @@ -176,6 +183,7 @@ public function check(
false,
$keyArgumentName,
$arg->getStartLine(),
$nonUnpackAfterUnpacked,
];
}
} else {
Expand All @@ -185,6 +193,7 @@ public function check(
true,
null,
$arg->getStartLine(),
$nonUnpackAfterUnpacked,
];
}
continue;
Expand All @@ -196,6 +205,7 @@ public function check(
false,
$argumentName,
$arg->getStartLine(),
$nonUnpackAfterUnpacked,
];
}

Expand Down Expand Up @@ -547,7 +557,7 @@ private function processArguments(
$newArguments = [];

$namedArgumentAlreadyOccurred = false;
foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine]) {
foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $nonUnpackAfterUnpacked]) {
if ($argumentName === null) {
if (!isset($parameters[$i])) {
if (!$parametersAcceptor->isVariadic() || count($parameters) === 0) {
Expand Down Expand Up @@ -607,10 +617,19 @@ private function processArguments(
&& !$parameter->isVariadic()
&& !array_key_exists($parameter->getName(), $unusedParametersByName)
) {
$errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName()))
->identifier('argument.duplicate')
->line($argumentLine)
->build();
if ($nonUnpackAfterUnpacked) {
$errors[] = RuleErrorBuilder::message(sprintf('Named parameter cannot overwrite already unpacked argument $%s.', $parameter->getName()))
->identifier('argument.namedOverwriteAfterUnpacked')
->line($argumentLine)
->nonIgnorable()
->build();
} else {
$errors[] = RuleErrorBuilder::message(sprintf('Argument for parameter $%s has already been passed.', $parameter->getName()))
->identifier('argument.duplicate')
->line($argumentLine)
->build();
}

continue;
}

Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'], []);
Expand Down Expand Up @@ -1936,4 +1950,13 @@ public function testBug12051(): void
$this->analyse([__DIR__ . '/data/bug-12051.php'], []);
}

public function testBug11418(): void
{
if (PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Test requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/bug-11418.php'], []);
}

}
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-11418.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Bug11418;

function foo(int $a, int $b, int $c = 3, int $d = 4): int {
return $a + $b + $c + $d;
}

var_dump(foo(...[1, 2], d: 40)); // 46
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace FunctionNamedArgumentsAfterUnpacking;

// https://www.php.net/manual/en/functions.arguments.php#example-180

function foo($a, $b, $c = 3, $d = 4) {
return $a + $b + $c + $d;
}

var_dump(foo(...[1, 2], d: 40)); // 46
var_dump(foo(...['b' => 2, 'a' => 1], d: 40)); // 46

var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument
Loading