diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index d5c6cab8daf..f57854da642 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +use Closure; use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\CollectedDataEmitter; @@ -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; @@ -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( @@ -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(); @@ -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 $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 $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) { diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 5c91f0b5c6c..a65228446cd 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -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; diff --git a/tests/PHPStan/Rules/Functions/data/fix-no-named-arguments.php b/tests/PHPStan/Rules/Functions/data/fix-no-named-arguments.php new file mode 100644 index 00000000000..e68c823b586 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/fix-no-named-arguments.php @@ -0,0 +1,54 @@ += 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); +}; diff --git a/tests/PHPStan/Rules/Functions/data/fix-no-named-arguments.php.fixed b/tests/PHPStan/Rules/Functions/data/fix-no-named-arguments.php.fixed new file mode 100644 index 00000000000..927744524cc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/fix-no-named-arguments.php.fixed @@ -0,0 +1,58 @@ += 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); +};