diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a52b1c94f2d..374aba09970 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3513,7 +3513,35 @@ public function processArgs( $deferredInvalidateExpressions = []; /** @var ProcessClosureResult[] $deferredByRefClosureResults */ $deferredByRefClosureResults = []; - foreach ($args as $i => $arg) { + + $processingOrder = array_keys($args); + $hasReorderedArgs = false; + foreach ($args as $arg) { + if ($arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) !== null) { + $hasReorderedArgs = true; + break; + } + } + if ($hasReorderedArgs) { + usort($processingOrder, static function (int $a, int $b) use ($args): int { + $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + if ($aOriginal === null && $bOriginal === null) { + return $a <=> $b; + } + if ($aOriginal === null) { + return 1; + } + if ($bOriginal === null) { + return -1; + } + + return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos(); + }); + } + + foreach ($processingOrder as $i) { + $arg = $args[$i]; $assignByReference = false; $parameter = null; $parameterType = null; diff --git a/tests/PHPStan/Analyser/nsrt/bug-9392.php b/tests/PHPStan/Analyser/nsrt/bug-9392.php new file mode 100644 index 00000000000..e04ce83a378 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9392.php @@ -0,0 +1,26 @@ += 8.0 + +namespace Bug9392; + +use function PHPStan\Testing\assertType; + +class Range +{ + public function __construct( + public ?string $notInRangeMessage = null, + public mixed $min = null, + public mixed $max = null, + ) { + } +} + +function () { + new Range( + min: $min = 20 * 100, + max: $max = 5_000 * 100, + notInRangeMessage: sprintf('The price must be between %s and %s.', round($min / 100, 2), round($max / 100, 2)), + ); + + assertType('2000', $min); + assertType('500000', $max); +}; diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 67f77fa6bfe..73c3a906c27 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1626,4 +1626,14 @@ public function testBug6833(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testBug9392(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9392.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-9392.php b/tests/PHPStan/Rules/Variables/data/bug-9392.php new file mode 100644 index 00000000000..361e38ac768 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9392.php @@ -0,0 +1,78 @@ += 8.0 + +namespace Bug9392; + +class Range +{ + public function __construct( + public ?string $notInRangeMessage = null, + public mixed $min = null, + public mixed $max = null, + ) { + } +} + +new Range( + min: $min = 20 * 100, + max: $max = 5_000 * 100, + notInRangeMessage: sprintf('The price must be between %s and %s.', round($min / 100, 2), round($max / 100, 2)), +); + +function foo(?string $c = null, mixed $a = null, mixed $b = null): void +{ +} + +foo( + a: $a = 10, + b: $b = 20, + c: sprintf('%s %s', $a, $b), +); + +class Foo +{ + public function bar(?string $c = null, mixed $a = null, mixed $b = null): void + { + } + + public static function baz(?string $c = null, mixed $a = null, mixed $b = null): void + { + } +} + +$foo = new Foo(); + +$foo->bar( + a: $x = 10, + b: $y = 20, + c: sprintf('%s %s', $x, $y), +); + +Foo::baz( + a: $p = 10, + b: $q = 20, + c: sprintf('%s %s', $p, $q), +); + +// Mixed positional and named args +function mixed_args(int $first, ?string $c = null, mixed $a = null, mixed $b = null): void +{ +} + +mixed_args( + 1, + a: $m1 = 10, + b: $m2 = 20, + c: sprintf('%s %s', $m1, $m2), +); + +// Variable assigned in named arg used after the call +function after_call(?string $c = null, mixed $a = null): void +{ +} + +after_call( + a: $afterVar = 42, + c: (string) $afterVar, +); + +echo $afterVar;