Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Understand variadic arg types with @no-named-arguments #1349

Merged
merged 3 commits into from May 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Analyser/MutatingScope.php
Expand Up @@ -2476,6 +2476,7 @@ public function enterClassMethod(
bool $isInternal,
bool $isFinal,
?bool $isPure = null,
bool $acceptsNamedArguments = true,
): self
{
if (!$this->isInClass()) {
Expand All @@ -2499,6 +2500,7 @@ public function enterClassMethod(
$isInternal,
$isFinal,
$isPure,
$acceptsNamedArguments,
),
!$classMethod->isStatic(),
);
Expand Down Expand Up @@ -2577,6 +2579,7 @@ public function enterFunction(
bool $isInternal,
bool $isFinal,
?bool $isPure = null,
bool $acceptsNamedArguments = true,
): self
{
return $this->enterFunctionLike(
Expand All @@ -2595,6 +2598,7 @@ public function enterFunction(
$isInternal,
$isFinal,
$isPure,
$acceptsNamedArguments,
),
false,
);
Expand All @@ -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()) {
Comment on lines -2613 to +2617
Copy link
Contributor Author

@jrmajor jrmajor May 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a simillar check in getFunctionType(). I'm not sure whether I need to change anything there, as it seems to work as is.

$parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType);
} else {
$parameterType = new ArrayType(new IntegerType(), $parameterType);
Expand All @@ -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);
Expand Down
14 changes: 9 additions & 5 deletions src/Analyser/NodeScopeResolver.php
Expand Up @@ -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);
Expand All @@ -419,6 +419,7 @@ private function processStmtNode(
$isInternal,
$isFinal,
$isPure,
$acceptsNamedArguments,
);
$nodeCallback(new InFunctionNode($stmt), $functionScope);

Expand Down Expand Up @@ -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);
Expand All @@ -474,6 +475,7 @@ private function processStmtNode(
$isInternal,
$isFinal,
$isPure,
$acceptsNamedArguments,
);

if ($stmt->name->toLowerString() === '__construct') {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
{
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Expand Up @@ -102,6 +102,8 @@ class ResolvedPhpDocBlock

private ?bool $hasConsistentConstructor = null;

private ?bool $acceptsNamedArguments = null;

private function __construct()
{
}
Expand Down Expand Up @@ -161,6 +163,7 @@ public static function createEmpty(): self
$self->isPure = null;
$self->isReadOnly = false;
$self->hasConsistentConstructor = false;
$self->acceptsNamedArguments = true;

return $self;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/Reflection/Php/PhpClassReflectionExtension.php
Expand Up @@ -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,
Expand All @@ -884,6 +884,7 @@ private function inferAndCachePropertyTypes(
$isInternal,
$isFinal,
$isPure,
$acceptsNamedArguments,
);

$propertyTypes = [];
Expand Down
6 changes: 6 additions & 0 deletions src/Reflection/Php/PhpFunctionFromParserNodeReflection.php
Expand Up @@ -53,6 +53,7 @@ public function __construct(
private bool $isInternal,
private bool $isFinal,
private ?bool $isPure,
private bool $acceptsNamedArguments,
)
{
$this->functionLike = $functionLike;
Expand Down Expand Up @@ -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_) {
Expand Down
2 changes: 2 additions & 0 deletions src/Reflection/Php/PhpMethodFromParserNodeReflection.php
Expand Up @@ -43,6 +43,7 @@ public function __construct(
bool $isInternal,
bool $isFinal,
?bool $isPure,
bool $acceptsNamedArguments,
)
{
$name = strtolower($classMethod->name->name);
Expand Down Expand Up @@ -83,6 +84,7 @@ public function __construct(
$isInternal,
$isFinal || $classMethod->isFinal(),
$isPure,
$acceptsNamedArguments,
);
}

Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -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');
}
Expand Down
50 changes: 50 additions & 0 deletions tests/PHPStan/Analyser/data/no-named-arguments.php
@@ -0,0 +1,50 @@
<?php

namespace NoNamedArguments;

use function PHPStan\Testing\assertType;

/**
* @no-named-arguments
*/
function noNamedArgumentsInFunction(float ...$args)
{
assertType('array<int, float>', $args);
}

class Baz extends Foo implements Bar
{
/**
* @no-named-arguments
*/
public function noNamedArgumentsInMethod(float ...$args)
{
assertType('array<int, float>', $args);
}

public function noNamedArgumentsInParent(float ...$args)
{
assertType('array<int, float>', $args);
}

public function noNamedArgumentsInInterface(float ...$args)
{
assertType('array<int, float>', $args);
}
}

abstract class Foo
{
/**
* @no-named-arguments
*/
abstract public function noNamedArgumentsInParent(float ...$args);
}

interface Bar
{
/**
* @no-named-arguments
*/
public function noNamedArgumentsInInterface();
}