From 0f33a29578cb80987878214debf2a3d1327435f8 Mon Sep 17 00:00:00 2001 From: Jeremiasz Major Date: Wed, 25 May 2022 17:28:15 +0200 Subject: [PATCH 1/3] Understand variadic arg type with `@no-named-args` --- src/Analyser/MutatingScope.php | 12 ++++++---- src/Analyser/NodeScopeResolver.php | 14 +++++++---- src/PhpDoc/PhpDocNodeResolver.php | 5 ++++ src/PhpDoc/ResolvedPhpDocBlock.php | 14 +++++++++++ .../Php/PhpClassReflectionExtension.php | 3 ++- .../PhpFunctionFromParserNodeReflection.php | 6 +++++ .../Php/PhpMethodFromParserNodeReflection.php | 2 ++ .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/no-named-arguments.php | 24 +++++++++++++++++++ 9 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/no-named-arguments.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index bb25fe4f81..3fb33fdfcb 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2475,7 +2475,8 @@ public function enterClassMethod( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null, + ?bool $isPure, + bool $acceptsNamedArguments, ): self { if (!$this->isInClass()) { @@ -2499,6 +2500,7 @@ public function enterClassMethod( $isInternal, $isFinal, $isPure, + $acceptsNamedArguments, ), !$classMethod->isStatic(), ); @@ -2576,7 +2578,8 @@ public function enterFunction( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null, + ?bool $isPure, + bool $acceptsNamedArguments, ): 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..67715871fd 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; } @@ -207,6 +210,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->isPure = $this->isPure(); $result->isReadOnly = $this->isReadOnly(); $result->hasConsistentConstructor = $this->hasConsistentConstructor(); + $result->acceptsNamedArguments = $this->acceptsNamedArguments(); return $result; } @@ -527,6 +531,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..9e0ef479d6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/no-named-arguments.php @@ -0,0 +1,24 @@ +', $args); +} + +class Baz +{ + /** + * @no-named-arguments + */ + public function noNamedArgumentsInMethod(float ...$args) + { + assertType('array', $args); + } +} From a4cae9c0adb9af7d9c7b74b279f00af92c84d139 Mon Sep 17 00:00:00 2001 From: Jeremiasz Major Date: Wed, 25 May 2022 17:28:47 +0200 Subject: [PATCH 2/3] Make child classes inherit `@no-named-args` --- src/PhpDoc/ResolvedPhpDocBlock.php | 4 ++- .../Analyser/data/no-named-arguments.php | 28 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 67715871fd..f69a49e254 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -180,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; @@ -210,7 +212,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->isPure = $this->isPure(); $result->isReadOnly = $this->isReadOnly(); $result->hasConsistentConstructor = $this->hasConsistentConstructor(); - $result->acceptsNamedArguments = $this->acceptsNamedArguments(); + $result->acceptsNamedArguments = $acceptsNamedArguments; return $result; } diff --git a/tests/PHPStan/Analyser/data/no-named-arguments.php b/tests/PHPStan/Analyser/data/no-named-arguments.php index 9e0ef479d6..6b9e6507cb 100644 --- a/tests/PHPStan/Analyser/data/no-named-arguments.php +++ b/tests/PHPStan/Analyser/data/no-named-arguments.php @@ -12,7 +12,7 @@ function noNamedArgumentsInFunction(float ...$args) assertType('array', $args); } -class Baz +class Baz extends Foo implements Bar { /** * @no-named-arguments @@ -21,4 +21,30 @@ 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(); } From 6b7f84e2ec15b8b5808e2d10379ba26b9ece5d10 Mon Sep 17 00:00:00 2001 From: Jeremiasz Major Date: Wed, 25 May 2022 17:56:33 +0200 Subject: [PATCH 3/3] Fix BC break --- src/Analyser/MutatingScope.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 3fb33fdfcb..edfc170062 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2475,8 +2475,8 @@ public function enterClassMethod( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure, - bool $acceptsNamedArguments, + ?bool $isPure = null, + bool $acceptsNamedArguments = true, ): self { if (!$this->isInClass()) { @@ -2578,8 +2578,8 @@ public function enterFunction( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure, - bool $acceptsNamedArguments, + ?bool $isPure = null, + bool $acceptsNamedArguments = true, ): self { return $this->enterFunctionLike(