Skip to content

Commit 8afa421

Browse files
committed
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.
1 parent acd0511 commit 8afa421

5 files changed

Lines changed: 57 additions & 6 deletions

File tree

‎src/Analyser/MutatingScope.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2240,7 +2240,10 @@ private function enterFunctionLike(
22402240

22412241
$nativeParameterType = $parameter->getNativeType();
22422242
if ($parameter->isVariadic()) {
2243-
if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) {
2243+
// acceptsNamedArguments() is read from PHPDoc, so it must not narrow the native type.
2244+
// PHP still accepts named arguments (and therefore string keys) at runtime,
2245+
// even when the function is marked with @no-named-arguments.
2246+
if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
22442247
$nativeParameterType = new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $nativeParameterType);
22452248
} else {
22462249
$nativeParameterType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $nativeParameterType), new AccessoryArrayListType()]);

‎tests/PHPStan/Analyser/nsrt/no-named-arguments.php‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
function noNamedArgumentsInFunction(float ...$args)
1212
{
1313
assertType('list<float>', $args);
14-
assertNativeType('list<float>', $args);
14+
assertNativeType('array<int<0, max>|string, float>', $args);
1515
}
1616

1717
class Baz extends Foo implements Bar
@@ -22,19 +22,19 @@ class Baz extends Foo implements Bar
2222
public function noNamedArgumentsInMethod(float ...$args)
2323
{
2424
assertType('list<float>', $args);
25-
assertNativeType('list<float>', $args);
25+
assertNativeType('array<int<0, max>|string, float>', $args);
2626
}
2727

2828
public function noNamedArgumentsInParent(float ...$args)
2929
{
3030
assertType('list<float>', $args);
31-
assertNativeType('list<float>', $args);
31+
assertNativeType('array<int<0, max>|string, float>', $args);
3232
}
3333

3434
public function noNamedArgumentsInInterface(float ...$args)
3535
{
3636
assertType('list<float>', $args);
37-
assertNativeType('list<float>', $args);
37+
assertNativeType('array<int<0, max>|string, float>', $args);
3838
}
3939
}
4040

‎tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ public function testInTrait(): void
207207
]);
208208
}
209209

210+
#[RequiresPhp('>= 8.0.0')]
211+
public function testNoNamedArgumentsNativeTypeWithUncertainPhpDocTypes(): void
212+
{
213+
$this->treatPhpDocTypesAsCertain = false;
214+
$this->analyse([__DIR__ . '/data/no-named-arguments-native-type.php'], []);
215+
}
216+
210217
public static function getAdditionalConfigFiles(): array
211218
{
212219
return [
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php // lint >= 8.0
2+
declare(strict_types = 1);
3+
4+
namespace NoNamedArgumentsNativeType;
5+
6+
final class QueryBuilder
7+
{
8+
9+
/**
10+
* @no-named-arguments
11+
*/
12+
public function select(mixed ...$select): void
13+
{
14+
self::validateVariadicParameter($select);
15+
}
16+
17+
/**
18+
* @no-named-arguments
19+
*/
20+
public function groupBy(string ...$groupBy): void
21+
{
22+
self::validateVariadicParameter($groupBy);
23+
}
24+
25+
/**
26+
* @template TItem
27+
* @param TItem[] $parameter
28+
* @phpstan-assert list<TItem> $parameter
29+
*/
30+
private static function validateVariadicParameter(array $parameter): void
31+
{
32+
if (array_is_list($parameter)) {
33+
return;
34+
}
35+
36+
throw new \InvalidArgumentException();
37+
}
38+
39+
}

‎turbo-ext/src/MutatingScope.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4512,8 +4512,10 @@ class MutatingScope
45124512
zv::Val nativeParameterType = pt_type_call(parameterObject, PT_LC("getnativetype"), 0, NULL);
45134513
if (UNEXPECTED(nativeParameterType.isUndef())) return zv::Val();
45144514
if (zend_is_true(isVariadic.raw())) {
4515+
/* acceptsNamedArguments() is read from PHPDoc, so it must not narrow
4516+
* the native type: PHP still accepts named arguments at runtime. */
45154517
bool named;
4516-
if (UNEXPECTED(!acceptsNamedArgumentsHere(reflection, named))) return zv::Val();
4518+
if (UNEXPECTED(!phpVersionSupportsNamedArguments(named))) return zv::Val();
45174519
zv::Val wrapped = variadicArrayType(nativeParameterType.raw(), named);
45184520
if (UNEXPECTED(wrapped.isUndef())) return zv::Val();
45194521
nativeParameterType = std::move(wrapped);

0 commit comments

Comments
 (0)