From 4bc93c4f372876c4b34c36104fc946fc7b2ade57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 24 Oct 2026 22:01:01 +0200 Subject: [PATCH] Ignore @no-named-arguments in the native type of variadic parameters The native type of a variadic parameter used acceptsNamedArguments(), which comes from the @no-named-arguments PHPDoc tag. That contradicts getNativeType(), which is documented as ignoring PHPDoc, and makes an @phpstan-assert on such a parameter report as redundant under treatPhpDocTypesAsCertain: false. Only the PHPDoc-aware type now uses acceptsNamedArguments(). The native type keeps string keys whenever the PHP version supports named arguments. --- src/Analyser/MutatingScope.php | 2 +- .../Analyser/nsrt/no-named-arguments.php | 8 ++--- ...sibleCheckTypeStaticMethodCallRuleTest.php | 7 +++++ .../data/no-named-arguments-native-type.php | 31 +++++++++++++++++++ turbo-ext/src/MutatingScope.cpp | 2 +- 5 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/no-named-arguments-native-type.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 85867200a20..b0f87233b08 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2240,7 +2240,7 @@ private function enterFunctionLike( $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { - if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { $nativeParameterType = new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $nativeParameterType); } else { $nativeParameterType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $nativeParameterType), new AccessoryArrayListType()]); diff --git a/tests/PHPStan/Analyser/nsrt/no-named-arguments.php b/tests/PHPStan/Analyser/nsrt/no-named-arguments.php index d16a28a5dd4..9f7d23b46d1 100644 --- a/tests/PHPStan/Analyser/nsrt/no-named-arguments.php +++ b/tests/PHPStan/Analyser/nsrt/no-named-arguments.php @@ -11,7 +11,7 @@ function noNamedArgumentsInFunction(float ...$args) { assertType('list', $args); - assertNativeType('list', $args); + assertNativeType('array|string, float>', $args); } class Baz extends Foo implements Bar @@ -22,19 +22,19 @@ class Baz extends Foo implements Bar public function noNamedArgumentsInMethod(float ...$args) { assertType('list', $args); - assertNativeType('list', $args); + assertNativeType('array|string, float>', $args); } public function noNamedArgumentsInParent(float ...$args) { assertType('list', $args); - assertNativeType('list', $args); + assertNativeType('array|string, float>', $args); } public function noNamedArgumentsInInterface(float ...$args) { assertType('list', $args); - assertNativeType('list', $args); + assertNativeType('array|string, float>', $args); } } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 1cf941dafd9..6c3610beaf0 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -207,6 +207,13 @@ public function testInTrait(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testNoNamedArgumentsNativeTypeWithUncertainPhpDocTypes(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/no-named-arguments-native-type.php'], []); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/data/no-named-arguments-native-type.php b/tests/PHPStan/Rules/Comparison/data/no-named-arguments-native-type.php new file mode 100644 index 00000000000..7dee3c01922 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/no-named-arguments-native-type.php @@ -0,0 +1,31 @@ += 8.0 +declare(strict_types = 1); + +namespace NoNamedArgumentsNativeType; + +final class QueryBuilder +{ + + /** + * @no-named-arguments + */ + public function groupBy(string ...$groupBy): void + { + self::validateVariadicParameter($groupBy); + } + + /** + * @template TItem + * @param TItem[] $parameter + * @phpstan-assert list $parameter + */ + private static function validateVariadicParameter(array $parameter): void + { + if (array_is_list($parameter)) { + return; + } + + throw new \InvalidArgumentException(); + } + +} diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index 1138b46ed9a..4ef2dab159a 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -4513,7 +4513,7 @@ class MutatingScope if (UNEXPECTED(nativeParameterType.isUndef())) return zv::Val(); if (zend_is_true(isVariadic.raw())) { bool named; - if (UNEXPECTED(!acceptsNamedArgumentsHere(reflection, named))) return zv::Val(); + if (UNEXPECTED(!phpVersionSupportsNamedArguments(named))) return zv::Val(); zv::Val wrapped = variadicArrayType(nativeParameterType.raw(), named); if (UNEXPECTED(wrapped.isUndef())) return zv::Val(); nativeParameterType = std::move(wrapped);