diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index bb25fe4f81..edfc170062 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2476,6 +2476,7 @@ public function enterClassMethod( bool $isInternal, bool $isFinal, ?bool $isPure = null, + bool $acceptsNamedArguments = true, ): self { if (!$this->isInClass()) { @@ -2499,6 +2500,7 @@ public function enterClassMethod( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, ), !$classMethod->isStatic(), ); @@ -2577,6 +2579,7 @@ public function enterFunction( bool $isInternal, bool $isFinal, ?bool $isPure = null, + bool $acceptsNamedArguments = true, ): self { return $this->enterFunctionLike( @@ -2595,6 +2598,7 @@ public function enterFunction( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, ), false, ); @@ -2610,7 +2614,7 @@ private function enterFunctionLike( foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameter) { $parameterType = $parameter->getType(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments()) { + if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()) { $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); } else { $parameterType = new ArrayType(new IntegerType(), $parameterType); @@ -2620,7 +2624,7 @@ private function enterFunctionLike( $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments()) { + if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()) { $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); } else { $nativeParameterType = new ArrayType(new IntegerType(), $nativeParameterType); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e05c42c656..f4c1670d34 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -398,7 +398,7 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($param, $scope, $nodeCallback); @@ -419,6 +419,7 @@ private function processStmtNode( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, ); $nodeCallback(new InFunctionNode($stmt), $functionScope); @@ -453,7 +454,7 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($param, $scope, $nodeCallback); @@ -474,6 +475,7 @@ private function processStmtNode( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, ); if ($stmt->name->toLowerString() === '__construct') { @@ -637,7 +639,7 @@ private function processStmtNode( foreach ($stmt->props as $prop) { $this->processStmtNode($prop, $scope, $nodeCallback); - [,,,,,,,,,$isReadOnly, $docComment] = $this->getPhpDocs($scope, $stmt); + [,,,,,,,,,,$isReadOnly, $docComment] = $this->getPhpDocs($scope, $stmt); $nodeCallback( new ClassPropertyNode( $prop->name->toString(), @@ -3828,7 +3830,7 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } /** - * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, string|null} + * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { @@ -3841,6 +3843,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $isInternal = false; $isFinal = false; $isPure = false; + $acceptsNamedArguments = true; $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable(); $docComment = $node->getDocComment() !== null ? $node->getDocComment()->getText() @@ -3948,10 +3951,11 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $isReadOnly, $docComment]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 02f6df5656..57c5a6b62a 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -491,6 +491,11 @@ public function resolveHasConsistentConstructor(PhpDocNode $phpDocNode): bool return false; } + public function resolveAcceptsNamedArguments(PhpDocNode $phpDocNode): bool + { + return count($phpDocNode->getTagsByName('@no-named-arguments')) === 0; + } + private function shouldSkipType(string $tagName, Type $type): bool { if (strpos($tagName, '@psalm-') !== 0) { diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 42e4f3b2fe..f69a49e254 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -102,6 +102,8 @@ class ResolvedPhpDocBlock private ?bool $hasConsistentConstructor = null; + private ?bool $acceptsNamedArguments = null; + private function __construct() { } @@ -161,6 +163,7 @@ public static function createEmpty(): self $self->isPure = null; $self->isReadOnly = false; $self->hasConsistentConstructor = false; + $self->acceptsNamedArguments = true; return $self; } @@ -177,9 +180,11 @@ public function merge(array $parents, array $parentPhpDocBlocks): self // skip $result->phpDocNode // skip $result->phpDocString - just for stubs $phpDocNodes = $this->phpDocNodes; + $acceptsNamedArguments = $this->acceptsNamedArguments(); foreach ($parents as $parent) { foreach ($parent->phpDocNodes as $phpDocNode) { $phpDocNodes[] = $phpDocNode; + $acceptsNamedArguments = $acceptsNamedArguments && $parent->acceptsNamedArguments(); } } $result->phpDocNodes = $phpDocNodes; @@ -207,6 +212,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->isPure = $this->isPure(); $result->isReadOnly = $this->isReadOnly(); $result->hasConsistentConstructor = $this->hasConsistentConstructor(); + $result->acceptsNamedArguments = $acceptsNamedArguments; return $result; } @@ -527,6 +533,16 @@ public function hasConsistentConstructor(): bool return $this->hasConsistentConstructor; } + public function acceptsNamedArguments(): bool + { + if ($this->acceptsNamedArguments === null) { + $this->acceptsNamedArguments = $this->phpDocNodeResolver->resolveAcceptsNamedArguments( + $this->phpDocNode, + ); + } + return $this->acceptsNamedArguments; + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 139fb11612..ebfb702389 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -872,7 +872,7 @@ private function inferAndCachePropertyTypes( $constructor, $namespace, )->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -884,6 +884,7 @@ private function inferAndCachePropertyTypes( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, ); $propertyTypes = []; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index cb4ec0b402..aefc759c37 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -53,6 +53,7 @@ public function __construct( private bool $isInternal, private bool $isFinal, private ?bool $isPure, + private bool $acceptsNamedArguments, ) { $this->functionLike = $functionLike; @@ -207,6 +208,11 @@ public function isGenerator(): bool return $this->nodeIsOrContainsYield($this->functionLike); } + public function acceptsNamedArguments(): bool + { + return $this->acceptsNamedArguments; + } + private function nodeIsOrContainsYield(Node $node): bool { if ($node instanceof Node\Expr\Yield_) { diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 26fa3a7833..9d07c70302 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -43,6 +43,7 @@ public function __construct( bool $isInternal, bool $isFinal, ?bool $isPure, + bool $acceptsNamedArguments, ) { $name = strtolower($classMethod->name->name); @@ -83,6 +84,7 @@ public function __construct( $isInternal, $isFinal || $classMethod->isFinal(), $isPure, + $acceptsNamedArguments, ); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 1b62bbab81..c0d0532052 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -535,6 +535,7 @@ public function dataFileAsserts(): iterable if (PHP_VERSION_ID >= 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/data/variadic-parameter-php8.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/no-named-arguments.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4896.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5843.php'); } diff --git a/tests/PHPStan/Analyser/data/no-named-arguments.php b/tests/PHPStan/Analyser/data/no-named-arguments.php new file mode 100644 index 0000000000..6b9e6507cb --- /dev/null +++ b/tests/PHPStan/Analyser/data/no-named-arguments.php @@ -0,0 +1,50 @@ +', $args); +} + +class Baz extends Foo implements Bar +{ + /** + * @no-named-arguments + */ + public function noNamedArgumentsInMethod(float ...$args) + { + assertType('array', $args); + } + + public function noNamedArgumentsInParent(float ...$args) + { + assertType('array', $args); + } + + public function noNamedArgumentsInInterface(float ...$args) + { + assertType('array', $args); + } +} + +abstract class Foo +{ + /** + * @no-named-arguments + */ + abstract public function noNamedArgumentsInParent(float ...$args); +} + +interface Bar +{ + /** + * @no-named-arguments + */ + public function noNamedArgumentsInInterface(); +}