Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,21 @@ public function resolveAcceptsNamedArguments(PhpDocNode $phpDocNode): bool
return count($phpDocNode->getTagsByName('@no-named-arguments')) === 0;
}

public function resolveRequiresArgumentsByName(PhpDocNode $phpDocNode): bool
{
return count($phpDocNode->getTagsByName('@arguments-by-name')) > 0;
}

public function resolveRequiresOptionalArgumentsByName(PhpDocNode $phpDocNode): bool
{
return count($phpDocNode->getTagsByName('@optional-arguments-by-name')) > 0;
}

public function resolveHasStableParameterNames(PhpDocNode $phpDocNode): bool
{
return count($phpDocNode->getTagsByName('@stable-parameter-names')) > 0;
}

private function shouldSkipType(string $tagName, Type $type): bool
{
if (!str_starts_with($tagName, '@psalm-')) {
Expand Down
36 changes: 35 additions & 1 deletion src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ final class ResolvedPhpDocBlock

private ?bool $acceptsNamedArguments = null;

private ?bool $requiresArgumentsByName = null;

private ?bool $requiresOptionalArgumentsByName = null;

private ?bool $hasStableParameterNames = null;

private function __construct()
{
}
Expand Down Expand Up @@ -255,6 +261,9 @@ public static function createEmpty(): self
$self->isAllowedPrivateMutation = false;
$self->hasConsistentConstructor = false;
$self->acceptsNamedArguments = true;
$self->requiresArgumentsByName = false;
$self->requiresOptionalArgumentsByName = false;
$self->hasStableParameterNames = false;

return $self;
}
Expand All @@ -267,9 +276,10 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
// skip $result->phpDocNode
$phpDocNodes = $this->phpDocNodes;
$acceptsNamedArguments = $this->acceptsNamedArguments();
$hasArgumentsByNameTag = $this->requiresArgumentsByName() || $this->requiresOptionalArgumentsByName() || $this->hasStableParameterNames();
foreach ($parent->phpDocNodes as $phpDocNode) {
$phpDocNodes[] = $phpDocNode;
$acceptsNamedArguments = $acceptsNamedArguments && $parent->acceptsNamedArguments();
$acceptsNamedArguments = $acceptsNamedArguments && ($hasArgumentsByNameTag || $parent->acceptsNamedArguments());
}
$result->phpDocNodes = $phpDocNodes;
$result->phpDocString = $this->phpDocString;
Expand Down Expand Up @@ -313,6 +323,9 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
$result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation();
$result->hasConsistentConstructor = $this->hasConsistentConstructor();
$result->acceptsNamedArguments = $acceptsNamedArguments;
$result->requiresArgumentsByName = $this->requiresArgumentsByName();
$result->requiresOptionalArgumentsByName = $this->requiresOptionalArgumentsByName();
$result->hasStableParameterNames = $this->hasStableParameterNames();

return $result;
}
Expand Down Expand Up @@ -835,6 +848,27 @@ public function acceptsNamedArguments(): bool
);
}

public function requiresArgumentsByName(): bool
{
return $this->requiresArgumentsByName ??= $this->phpDocNodeResolver->resolveRequiresArgumentsByName(
$this->phpDocNode,
);
}

public function requiresOptionalArgumentsByName(): bool
{
return $this->requiresOptionalArgumentsByName ??= $this->phpDocNodeResolver->resolveRequiresOptionalArgumentsByName(
$this->phpDocNode,
);
}

public function hasStableParameterNames(): bool
{
return $this->hasStableParameterNames ??= $this->phpDocNodeResolver->resolveHasStableParameterNames(
$this->phpDocNode,
);
}

public function getTemplateTypeMap(): TemplateTypeMap
{
return $this->templateTypeMap;
Expand Down
122 changes: 122 additions & 0 deletions src/Reflection/ArgumentsByNameTagResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php declare(strict_types = 1);

namespace PHPStan\Reflection;

use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\Php\PhpFunctionReflection;
use function strtolower;

/**
* Finds which of @arguments-by-name, @optional-arguments-by-name and @stable-parameter-names applies to a function or a method.
*
* For a method, the tag on the method comes first. Then comes the tag of the method it overrides or implements,
* except for constructors. The class-level tag of the declaring class comes last, except for private methods.
* A docblock with @no-named-arguments has none of these tags.
*/
final class ArgumentsByNameTagResolver
{

public const ARGUMENTS_BY_NAME = '@arguments-by-name';

public const OPTIONAL_ARGUMENTS_BY_NAME = '@optional-arguments-by-name';

public const STABLE_PARAMETER_NAMES = '@stable-parameter-names';

/**
* @return self::*|null
*/
public static function resolveForFunction(FunctionReflection $function): ?string
{
if (!$function instanceof PhpFunctionReflection) {
return null;
}

return self::resolveFromPhpDoc($function->getResolvedPhpDoc());
}

/**
* @return self::*|null
*/
public static function resolveForMethod(ExtendedMethodReflection $method): ?string
{
$phpDoc = $method->getResolvedPhpDoc();
if ($phpDoc !== null && !$phpDoc->acceptsNamedArguments()) {
return null;
}

$tag = self::resolveFromPhpDoc($phpDoc);
if ($tag !== null) {
return $tag;
}

$declaringClass = $method->getDeclaringClass();
if (strtolower($method->getName()) !== '__construct') {
$tag = self::resolveFromOverriddenMethods($declaringClass, $method->getName());
if ($tag !== null) {
return $tag;
}
}

// private methods are only called by their class
if ($method->isPrivate()) {
return null;
}

return self::resolveFromPhpDoc($declaringClass->getResolvedPhpDoc());
}

/**
* @return self::*|null
*/
private static function resolveFromOverriddenMethods(ClassReflection $class, string $methodName): ?string
{
$parentClass = $class->getParentClass();
$ancestors = $parentClass !== null ? [$parentClass] : [];
foreach ($class->getImmediateInterfaces() as $interface) {
$ancestors[] = $interface;
}

foreach ($ancestors as $ancestor) {
if (!$ancestor->hasNativeMethod($methodName)) {
continue;
}

$overriddenMethod = $ancestor->getNativeMethod($methodName);
if ($overriddenMethod->isPrivate()) {
continue;
}

$tag = self::resolveForMethod($overriddenMethod);
if ($tag !== null) {
return $tag;
}
}

return null;
}

/**
* @return self::*|null
*/
private static function resolveFromPhpDoc(?ResolvedPhpDocBlock $phpDoc): ?string
{
if ($phpDoc === null || !$phpDoc->acceptsNamedArguments()) {
return null;
}

if ($phpDoc->requiresArgumentsByName()) {
return self::ARGUMENTS_BY_NAME;
}

if ($phpDoc->requiresOptionalArgumentsByName()) {
return self::OPTIONAL_ARGUMENTS_BY_NAME;
}

if ($phpDoc->hasStableParameterNames()) {
return self::STABLE_PARAMETER_NAMES;
}

return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection
$this->attributeReflectionFactory->fromNativeReflection($reflectionFunction->getAttributes(), InitializerExprContext::fromFunction($reflectionFunction->getName(), $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null)),
$phpDocParameterPureUnlessCallableIsImpure,
$phpDocParameterPureUnlessParameterPassed,
$resolvedPhpDoc,
);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Reflection/FunctionReflectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Reflection;

use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\Php\PhpFunctionReflection;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -39,6 +40,7 @@ public function create(
array $attributes,
array $phpDocParameterPureUnlessCallableIsImpure,
array $phpDocParameterPureUnlessParameterPassed,
?ResolvedPhpDocBlock $resolvedPhpDocBlock,
): PhpFunctionReflection;

}
7 changes: 7 additions & 0 deletions src/Reflection/Php/PhpFunctionReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter;
use PHPStan\DependencyInjection\GenerateFactory;
use PHPStan\Internal\DeprecatedAttributeHelper;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\AttributeReflection;
use PHPStan\Reflection\AttributeReflectionFactory;
Expand Down Expand Up @@ -66,6 +67,7 @@ public function __construct(
private array $attributes,
private array $phpDocParameterPureUnlessCallableIsImpure,
private array $phpDocParameterPureUnlessParameterPassed,
private ?ResolvedPhpDocBlock $resolvedPhpDocBlock,
)
{
}
Expand Down Expand Up @@ -275,4 +277,9 @@ public function mustUseReturnValue(): TrinaryLogic
return TrinaryLogic::createNo();
}

public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
{
return $this->resolvedPhpDocBlock;
}

}
9 changes: 8 additions & 1 deletion src/Reflection/Php/PhpMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\DependencyInjection\GenerateFactory;
use PHPStan\Internal\DeprecatedAttributeHelper;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\ArgumentsByNameTagResolver;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\AttributeReflection;
use PHPStan\Reflection\AttributeReflectionFactory;
Expand Down Expand Up @@ -390,8 +391,14 @@ public function getAsserts(): Assertions

public function acceptsNamedArguments(): TrinaryLogic
{
if (!$this->acceptsNamedArguments) {
return TrinaryLogic::createNo();
}

// a tag of the method overrides @no-named-arguments on the class
return TrinaryLogic::createFromBoolean(
$this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments,
$this->declaringClass->acceptsNamedArguments()
|| ArgumentsByNameTagResolver::resolveForMethod($this) !== null,
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/Rules/AttributesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ArgumentsByNameTagResolver;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use function array_key_exists;
Expand Down Expand Up @@ -163,6 +164,8 @@ public function check(
'Constants %s cannot be combined for %s of attribute class ' . $attributeClassName . ' constructor.',
'Combining constants with | is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.',
null,
ArgumentsByNameTagResolver::resolveForMethod($attributeConstructor),
'Argument for %s of attribute class ' . $attributeClassName . ' constructor must be passed by name.',
);

foreach ($parameterErrors as $error) {
Expand Down
3 changes: 3 additions & 0 deletions src/Rules/Classes/InstantiationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\DependencyInjection\ExtensionsCollection;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ArgumentsByNameTagResolver;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Php\PhpMethodReflection;
Expand Down Expand Up @@ -338,6 +339,8 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope&N
'Constants %s cannot be combined for %s of class ' . $classDisplayName . ' constructor.',
'Combining constants with | is not allowed for %s of class ' . $classDisplayName . ' constructor.',
null,
ArgumentsByNameTagResolver::resolveForMethod($constructorReflection),
'Argument for %s of class ' . $classDisplayName . ' constructor must be passed by name.',
));
}

Expand Down
Loading
Loading