Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
129 changes: 126 additions & 3 deletions src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules;

use Closure;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PHPStan\Analyser\CollectedDataEmitter;
Expand Down Expand Up @@ -43,6 +44,8 @@
use function count;
use function implode;
use function in_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function lcfirst;
Expand Down Expand Up @@ -336,6 +339,7 @@ public function check(
return $errors;
}

$positionalArgumentsFix = null;
foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter]) {
if ($this->checkArgumentTypes && $unpack) {
$iterableTypeResult = $this->ruleLevelHelper->findTypeToCheck(
Expand Down Expand Up @@ -375,10 +379,15 @@ public function check(

if (!$acceptsNamedArguments->yes()) {
if ($argumentName !== null) {
$errors[] = RuleErrorBuilder::message(sprintf($namedArgumentMessage, sprintf('named argument $%s', $argumentName)))
$errorBuilder = RuleErrorBuilder::message(sprintf($namedArgumentMessage, sprintf('named argument $%s', $argumentName)))
->identifier('argument.named')
->line($argumentLine)
->build();
->line($argumentLine);
$positionalArgumentsFix ??= $this->createPositionalArgumentsFix($parametersAcceptor, $funcCall);
if ($positionalArgumentsFix !== false) {
// every named argument of the call reports the same fix, the identical changes are applied once
$errorBuilder->fixNode($funcCall, $positionalArgumentsFix);
}
$errors[] = $errorBuilder->build();
} elseif ($unpack) {
$unpackedArrayType = $scope->getType($argumentValue);
$hasStringKey = $unpackedArrayType->getIterableKeyType()->isString();
Expand Down Expand Up @@ -892,6 +901,120 @@ private function resolveConstantReflections(Expr $expr, Scope $scope): ?array
return null;
}

/**
* Rewrites the call to pass every argument positionally, skipped optional parameters get their default values.
*
* @return (Closure(Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_): (Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_))|false
*/
private function createPositionalArgumentsFix(
ParametersAcceptor $parametersAcceptor,
Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall,
): Closure|false
{
if ((bool) $funcCall->getAttribute('isAttribute', false)) {
// the call was created from an attribute, it is not in the file
return false;
}

$parameters = $parametersAcceptor->getParameters();
$parameterPositions = [];
foreach ($parameters as $i => $parameter) {
$parameterPositions[$parameter->getName()] = $i;
}

/** @var array<int, int> $argIndexByPosition */
$argIndexByPosition = [];
$lastPosition = 0;
foreach ($funcCall->getArgs() as $i => $arg) {
if ($arg->unpack) {
return false;
}
if ($arg->name === null) {
$argIndexByPosition[$i] = $i;
$lastPosition = max($lastPosition, $i);
continue;
}

$position = $parameterPositions[$arg->name->toString()] ?? null;
if (
$position === null
// a named argument that is collected by the variadic parameter keeps its string key
|| $parameters[$position]->isVariadic()
|| array_key_exists($position, $argIndexByPosition)
) {
return false;
}
$argIndexByPosition[$position] = $i;
$lastPosition = max($lastPosition, $position);
}

/** @var array<int, Expr> $defaultValues */
$defaultValues = [];
for ($position = 0; $position < $lastPosition; $position++) {
if (array_key_exists($position, $argIndexByPosition)) {
continue;
}

$parameter = $parameters[$position];
$defaultValue = $parameter->getDefaultValue();
if (!$parameter->isOptional() || !$parameter->passedByReference()->no() || $defaultValue === null) {
return false;
}

$defaultValueExpr = $this->createDefaultValueExpr($defaultValue);
if ($defaultValueExpr === null) {
return false;
}
$defaultValues[$position] = $defaultValueExpr;
}

return static function (Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $call) use ($argIndexByPosition, $defaultValues, $lastPosition): Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ {
$callArgs = $call->getArgs();
$args = [];
for ($position = 0; $position <= $lastPosition; $position++) {
if (!array_key_exists($position, $argIndexByPosition)) {
$args[] = new Node\Arg(clone $defaultValues[$position]);
continue;
}

$arg = clone $callArgs[$argIndexByPosition[$position]];
$arg->name = null;
$args[] = $arg;
}
$call->args = $args;

return $call;
};
}

private function createDefaultValueExpr(Type $type): ?Expr
{
if ($type->isConstantArray()->yes() && $type->isIterableAtLeastOnce()->no()) {
return new Expr\Array_([]);
}

$scalarValues = $type->getConstantScalarValues();
if (count($scalarValues) !== 1) {
return null;
}

$scalarValue = $scalarValues[0];
if ($scalarValue === null) {
return new Expr\ConstFetch(new Node\Name('null'));
}
if (is_bool($scalarValue)) {
return new Expr\ConstFetch(new Node\Name($scalarValue ? 'true' : 'false'));
}
if (is_int($scalarValue)) {
return new Node\Scalar\Int_($scalarValue);
}
if (is_float($scalarValue)) {
return new Node\Scalar\Float_($scalarValue);
}

return new Node\Scalar\String_($scalarValue);
}

private function callReturnsByReference(Expr $expr, Scope $scope): bool
{
if ($expr instanceof Node\Expr\MethodCall) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,12 @@ public function testNoNamedArguments(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testFixNoNamedArguments(): void
{
$this->fix(__DIR__ . '/data/fix-no-named-arguments.php', __DIR__ . '/data/fix-no-named-arguments.php.fixed');
}

public function testBug11056(): void
{
$this->checkExplicitMixed = true;
Expand Down
54 changes: 54 additions & 0 deletions tests/PHPStan/Rules/Functions/data/fix-no-named-arguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php // lint >= 8.0

namespace FixNoNamedArgumentsFunction;

/**
* @no-named-arguments
*/
function foo(int $a, int $b = 2, ?string $c = null, bool $d = false, array $e = [], float $f = -1.5, string $g = 'x'): void
{

}

/**
* @no-named-arguments
*/
function byRef(int $a, &$b = null, int $c = 3): void
{

}

/**
* @no-named-arguments
*/
function variadic(int $a, int ...$rest): void
{

}

/**
* @no-named-arguments
*/
function objectDefault(int $a, \stdClass $b = new \stdClass(), int $c = 3): void
{

}

function (): void {
foo(a: 1);
foo(b: 5, a: 1);
foo(1, c: 'c');
foo(
1,
g: 'y',
d: true,
);
foo(a: foo(b: 3, a: 4), e: [1]);
byRef(1, c: 4);
variadic(a: 1);
variadic(1, 2, x: 3);
objectDefault(1, c: 4);
objectDefault(1, b: new \stdClass());
foo(1, unknown: 2);
foo(b: 1, b: 2, a: 3);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php // lint >= 8.0

namespace FixNoNamedArgumentsFunction;

/**
* @no-named-arguments
*/
function foo(int $a, int $b = 2, ?string $c = null, bool $d = false, array $e = [], float $f = -1.5, string $g = 'x'): void
{

}

/**
* @no-named-arguments
*/
function byRef(int $a, &$b = null, int $c = 3): void
{

}

/**
* @no-named-arguments
*/
function variadic(int $a, int ...$rest): void
{

}

/**
* @no-named-arguments
*/
function objectDefault(int $a, \stdClass $b = new \stdClass(), int $c = 3): void
{

}

function (): void {
foo(1);
foo(1, 5);
foo(1, 2, 'c');
foo(
1,
2,
null,
true,
[],
-1.5,
'y',
);
foo(foo(4, 3), 2, null, false, [1]);
byRef(1, c: 4);
variadic(1);
variadic(1, 2, x: 3);
objectDefault(1, c: 4);
objectDefault(1, new \stdClass());
foo(1, unknown: 2);
foo(b: 1, b: 2, a: 3);
};
Loading