Skip to content
Closed
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
38 changes: 38 additions & 0 deletions src/Analyser/ArgumentsNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PhpParser\Node\Scalar\Int_;
use PhpParser\Node\Scalar\String_;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
Expand All @@ -22,11 +23,13 @@
use function array_keys;
use function array_values;
use function count;
use function explode;
use function is_string;
use function key;
use function ksort;
use function max;
use function sprintf;
use function str_contains;

/**
* @api
Expand Down Expand Up @@ -323,6 +326,8 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array
$argumentPositions[$parameter->getName()] = $i;
}

self::mapCombinedParameterPositions($signatureParameters, $argumentPositions);

$reorderedArgs = [];
$additionalNamedArgs = [];
$appendArgs = [];
Expand Down Expand Up @@ -439,4 +444,37 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array
return $reorderedArgs;
}

/**
* @param list<ParameterReflection> $signatureParameters
* @param array<string, int> $argumentPositions
*/
private static function mapCombinedParameterPositions(array $signatureParameters, array &$argumentPositions): void
{
foreach ($signatureParameters as $i => $parameter) {
$parameterName = $parameter->getName();
if (!str_contains($parameterName, '|')) {
continue;
}
$primaryName = explode('|', $parameterName, 2)[0];
if (array_key_exists($primaryName, $argumentPositions)) {
continue;
}

$argumentPositions[$primaryName] = $i;
}

foreach ($signatureParameters as $i => $parameter) {
$parameterName = $parameter->getName();
if (!str_contains($parameterName, '|')) {
continue;
}
foreach (explode('|', $parameterName) as $name) {
if (array_key_exists($name, $argumentPositions)) {
continue;
}
$argumentPositions[$name] = $i;
}
}
}

}
55 changes: 52 additions & 3 deletions src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
use function array_key_exists;
use function array_last;
use function count;
use function explode;
use function implode;
use function in_array;
use function is_int;
use function is_string;
use function max;
use function sprintf;
use function str_contains;

#[AutowiredService]
final class FunctionCallParametersCheck
Expand Down Expand Up @@ -581,15 +583,20 @@ private function processArguments(
$errors = [];
$isNativelyVariadic = false;
foreach ($parameters as $i => $parameter) {
$parametersByName[$parameter->getName()] = $parameter;
$originalParametersByName[$parameter->getName()] = $originalParameters[$i];
$parameterName = $parameter->getName();
$parametersByName[$parameterName] = $parameter;
$originalParametersByName[$parameterName] = $originalParameters[$i];

if ($parameter->isVariadic()) {
$isNativelyVariadic = true;
continue;
}

$unusedParametersByName[$parameter->getName()] = $parameter;
$unusedParametersByName[$parameterName] = $parameter;
}

if ($hasNamedArguments) {
self::mapCombinedParameterNames($parameters, $originalParameters, $parametersByName, $originalParametersByName);
}

$newArguments = [];
Expand Down Expand Up @@ -686,6 +693,48 @@ private function processArguments(
return [$errors, $newArguments];
}

/**
* @param list<ParameterReflection> $parameters
* @param array<int, ParameterReflection|null> $originalParameters
* @param array<string, ParameterReflection> $parametersByName
* @param array<string, ParameterReflection|null> $originalParametersByName
*/
private static function mapCombinedParameterNames(
array $parameters,
array $originalParameters,
array &$parametersByName,
array &$originalParametersByName,
): void
{
foreach ($parameters as $i => $parameter) {
$parameterName = $parameter->getName();
if (!str_contains($parameterName, '|')) {
continue;
}
$primaryName = explode('|', $parameterName, 2)[0];
if (array_key_exists($primaryName, $parametersByName)) {
continue;
}

$parametersByName[$primaryName] = $parameter;
$originalParametersByName[$primaryName] = $originalParameters[$i];
}

foreach ($parameters as $i => $parameter) {
$parameterName = $parameter->getName();
if (!str_contains($parameterName, '|')) {
continue;
}
foreach (explode('|', $parameterName) as $name) {
if (array_key_exists($name, $parametersByName)) {
continue;
}
$parametersByName[$name] = $parameter;
$originalParametersByName[$name] = $originalParameters[$i];
}
}
}

private function describeParameter(ParameterReflection $parameter, int|string|null $positionOrNamed): string
{
$parts = [];
Expand Down
21 changes: 21 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4106,4 +4106,25 @@ public function testBug14596(): void
]);
}

public function testBug14661(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-14661.php'], [
[
'Unknown parameter $unknown in call to method Bug14661\A::mixedOrder().',
47,
],
[
'Missing parameter $b|a (int|string) in call to method Bug14661\C::foo().',
64,
],
[
'Missing parameter $a|b (int|string) in call to method Bug14661\C::foo().',
65,
],
]);
}

}
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1033,4 +1033,15 @@ public function testBug14596(): void
]);
}

public function testBug14661(): void
{
$this->checkThisOnly = false;
$this->analyse([__DIR__ . '/data/bug-14661-static.php'], [
[
'Unknown parameter $unknown in call to static method Bug14661Static\E::bar().',
26,
],
]);
}

}
27 changes: 27 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-14661-static.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace Bug14661Static;

class E
{
public static function bar(
?string $x = null,
?string $y = null,
): void {}
}

class F
{
public static function bar(
?string $y = null,
?string $x = null,
): void {}
}

function staticMethodCall(E|F $obj): void
{
$obj::bar(x: 'value');
$obj::bar(y: 'value');
$obj::bar(x: 'v1', y: 'v2');
$obj::bar(unknown: 'value');
}
89 changes: 89 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-14661.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php declare(strict_types = 1);

namespace Bug14661;

class A
{
public function mixedOrder(
?string $other = null,
?string $target = null,
): void {}

public function sameOrder(
?string $other = null,
?string $target = null,
): void {}
}

class B
{
public function mixedOrder(
?string $target = null,
?string $other = null,
): void {}

public function sameOrder(
?string $other = null,
?string $target = null,
): void {}
}

function mixedOrder(A|B $obj): void
{
$obj->mixedOrder(target: 'value');
$obj->mixedOrder(other: 'value');
$obj->mixedOrder(target: 'value1', other: 'value2');
$obj->mixedOrder(other: 'value1', target: 'value2');
}

function sameOrder(A|B $obj): void
{
$obj->sameOrder(target: 'value');
$obj->sameOrder(other: 'value');
}

function unknownParam(A|B $obj): void
{
$obj->mixedOrder(unknown: 'value');
}

class C
{
public function foo(string $a, int $b): void {}
}

class D
{
public function foo(int $b, string $a): void {}
}

function differentTypes(C|D $obj): void
{
$obj->foo(a: 'hello', b: 42);
$obj->foo(b: 42, a: 'hello');
$obj->foo(a: 'hello');
$obj->foo(b: 42);
}

class E
{
public static function bar(
?string $x = null,
?string $y = null,
): void {}
}

class F
{
public static function bar(
?string $y = null,
?string $x = null,
): void {}
}

function staticMethodCall(E|F $obj): void
{
$obj::bar(x: 'value');
$obj::bar(y: 'value');
$obj::bar(x: 'v1', y: 'v2');
}
Loading