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
12 changes: 12 additions & 0 deletions src/Dependency/DependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,18 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
}
}
}

if ($scope->isInClass()) {
$declaringReflection = $scope->isInTrait() ? $scope->getTraitReflection() : $scope->getClassReflection();
$resolvedPhpDoc = $declaringReflection->getResolvedPhpDoc();
if ($resolvedPhpDoc !== null) {
foreach ($resolvedPhpDoc->getUsesTags() as $usesTag) {
foreach ($usesTag->getType()->getReferencedClasses() as $referencedClass) {
$this->addClassToDependencies($referencedClass, $dependenciesReflections);
}
}
}
}
} elseif ($node instanceof Node\Expr\Instanceof_) {
if ($node->class instanceof Name) {
$this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections);
Expand Down
129 changes: 129 additions & 0 deletions src/Rules/Generics/ClassLevelUsedTraitsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Generics;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\DependencyInjection\ValidatesStubFiles;
use PHPStan\Internal\SprintfHelper;
use PHPStan\PhpDoc\PhpDocStringResolver;
use PHPStan\PhpDoc\Tag\UsesTag;
use PHPStan\Rules\Rule;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Type;
use function array_map;
use function count;
use function sprintf;
use function ucfirst;

/**
* Validates `@use` tags in class-level PHPDocs, the counterpart of UsedTraitsRule
* which validates `@use` tags above individual trait use statements.
*
* @implements Rule<Node\Stmt\ClassLike>
*/
#[RegisteredRule(level: 2)]
#[ValidatesStubFiles]
final class ClassLevelUsedTraitsRule implements Rule
{

public function __construct(
private PhpDocStringResolver $phpDocStringResolver,
private FileTypeMapper $fileTypeMapper,
private GenericAncestorsCheck $genericAncestorsCheck,
)
{
}

public function getNodeType(): string
{
return Node\Stmt\ClassLike::class;
}

public function processNode(Node $node, Scope $scope): array
{
$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}

if (!isset($node->namespacedName)) {
// anonymous class
return [];
}

if ($node instanceof Node\Stmt\Class_) {
$typeDescription = 'class';
} elseif ($node instanceof Node\Stmt\Interface_) {
$typeDescription = 'interface';
} elseif ($node instanceof Node\Stmt\Trait_) {
$typeDescription = 'trait';
} elseif ($node instanceof Node\Stmt\Enum_) {
$typeDescription = 'enum';
} else {
return [];
}

// resolving the PHPDoc types of every class-like is not for free and can
// change the outcome of recursion detection in phpdoc type resolution,
// so bail out early unless the doc comment really contains @use tags
$phpDocNode = $this->phpDocStringResolver->resolve($docComment->getText());
$hasUsesTagValues = false;
foreach (['@use', '@template-use', '@phpstan-use'] as $tagName) {
if (count($phpDocNode->getUsesTagValues($tagName)) > 0) {
$hasUsesTagValues = true;
break;
}
}
if (!$hasUsesTagValues) {
return [];
}

$className = (string) $node->namespacedName;
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$className,
null,
null,
$docComment->getText(),
);
$useTags = $resolvedPhpDoc->getUsesTags();
if (count($useTags) === 0) {
return [];
}

$traitNames = [];
foreach ($node->getTraitUses() as $traitUse) {
foreach ($traitUse->traits as $traitNameNode) {
$traitNames[] = $traitNameNode;
}
}

$description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($className));
$escapedDescription = SprintfHelper::escapeFormatString($description);
$upperCaseDescription = ucfirst($description);
$escapedUpperCaseDescription = SprintfHelper::escapeFormatString($upperCaseDescription);

return $this->genericAncestorsCheck->check(
$traitNames,
array_map(static fn (UsesTag $tag): Type => $tag->getType(), $useTags),
sprintf('%s @use tag contains incompatible type %%s.', $escapedUpperCaseDescription),
sprintf('%s @use tag contains unresolvable type.', $upperCaseDescription),
sprintf('%s has @use tag, but does not use any trait.', $upperCaseDescription),
sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $escapedDescription, $typeDescription),
'PHPDoc tag @use contains generic type %s but %s %s is not generic.',
'Generic type %s in PHPDoc tag @use does not specify all template types of %s %s: %s',
'Generic type %s in PHPDoc tag @use specifies %d template types, but %s %s supports only %d: %s',
'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of %s %s.',
'Call-site variance annotation of %s in generic type %s in PHPDoc tag @use is not allowed.',
'PHPDoc tag @use has invalid type %s.',
sprintf('%s uses generic trait %%s but does not specify its types: %%s', $escapedUpperCaseDescription),
sprintf('in used type %%s of %s', $escapedDescription),
// the missing-generics check is left entirely to UsedTraitsRule, which runs
// for every trait use statement and takes class-level @use tags into account
array_map(static fn (Node\Name $traitNameNode): string => $traitNameNode->toString(), $traitNames),
);
}

}
5 changes: 5 additions & 0 deletions src/Rules/Generics/GenericAncestorsCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
/**
* @param array<Node\Name> $nameNodes
* @param array<Type> $ancestorTypes
* @param list<string> $namesWithTypesSpecifiedElsewhere names exempt from the missing-generics check because a tag for them exists in another location (e.g. a class-level `@use` tag)
* @return list<IdentifierRuleError>
*/
public function check(
Expand All @@ -66,6 +67,7 @@ public function check(
string $invalidTypeMessage,
string $genericClassInNonGenericObjectType,
string $invalidVarianceMessage,
array $namesWithTypesSpecifiedElsewhere = [],
): array
{
$names = array_fill_keys(array_map(static fn (Name $nameNode): string => $nameNode->toString(), $nameNodes), true);
Expand Down Expand Up @@ -165,6 +167,9 @@ public function check(

if ($this->checkMissingTypehints) {
foreach (array_keys($unusedNames) as $unusedName) {
if (in_array($unusedName, $namesWithTypesSpecifiedElsewhere, true)) {
continue;
}
if (!$this->reflectionProvider->hasClass($unusedName)) {
continue;
}
Expand Down
9 changes: 9 additions & 0 deletions src/Rules/Generics/UsedTraitsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Type;
use function array_keys;
use function array_map;
use function sprintf;
use function strtolower;
Expand Down Expand Up @@ -61,6 +62,13 @@ public function processNode(Node $node, Scope $scope): array
$useTags = $resolvedPhpDoc->getUsesTags();
}

$classLevelUseTags = [];
$declaringReflection = $scope->isInTrait() ? $scope->getTraitReflection() : $scope->getClassReflection();
$classLevelResolvedPhpDoc = $declaringReflection->getResolvedPhpDoc();
if ($classLevelResolvedPhpDoc !== null) {
$classLevelUseTags = $classLevelResolvedPhpDoc->getUsesTags();
}

$typeDescription = strtolower($scope->getClassReflection()->getClassTypeDescription());
$description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($className));
if ($traitName !== null) {
Expand All @@ -87,6 +95,7 @@ public function processNode(Node $node, Scope $scope): array
'PHPDoc tag @use has invalid type %s.',
sprintf('%s uses generic trait %%s but does not specify its types: %%s', $escapedUpperCaseDescription),
sprintf('in used type %%s of %s', $escapedDescription),
array_keys($classLevelUseTags),
);
}

Expand Down
51 changes: 39 additions & 12 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPStan\PhpDoc\PhpDocStringResolver;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDoc\Tag\UsesTag;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
Expand Down Expand Up @@ -255,19 +256,24 @@ public function getNameScope(
null,
$traitDocComment,
)->getUsesTags();
$useType = null;
foreach ($useTags as $useTag) {
$useTagType = $useTag->getType();
if (!$useTagType instanceof GenericObjectType) {
continue;
}

if ($useTagType->getClassName() !== $traitReflection->getName()) {
continue;
$useType = self::findUseTagType($useTags, $traitReflection->getName());
if ($useType === null) {
// no statement-level @use tag matched the trait - fall back
// to @use tags in the PHPDoc of the class-like using the trait
$traitUseClassLikeName = $lookForTraitName ?? $traitClassName;
if ($reflectionProvider->hasClass($traitUseClassLikeName)) {
$traitUseClassLikeDocComment = $reflectionProvider->getClass($traitUseClassLikeName)->getNativeReflection()->getDocComment();
if ($traitUseClassLikeDocComment !== false) {
$classLevelUseTags = $this->getResolvedPhpDoc(
$traitFileName,
$traitClassName,
$lookForTraitName,
null,
$traitUseClassLikeDocComment,
)->getUsesTags();
$useType = self::findUseTagType($classLevelUseTags, $traitReflection->getName());
}
}

$useType = $useTagType;
break;
}
$traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();
$namesToUnset = [];
Expand Down Expand Up @@ -783,6 +789,27 @@ private function chooseTemplateTagValueNodesByPriority(array $tags): array
return $resolved;
}

/**
* @param array<string, UsesTag> $useTags
*/
private static function findUseTagType(array $useTags, string $traitName): ?GenericObjectType
{
foreach ($useTags as $useTag) {
$useTagType = $useTag->getType();
if (!$useTagType instanceof GenericObjectType) {
continue;
}

if ($useTagType->getClassName() !== $traitName) {
continue;
}

return $useTagType;
}

return null;
}

/**
* @return array<string, true>
*/
Expand Down
Loading
Loading