Skip to content

Commit

Permalink
Allow @phpstan-use and @use tags to be declared into class phpdoc
Browse files Browse the repository at this point in the history
  • Loading branch information
baptistepillot committed Sep 21, 2023
1 parent fbc09b5 commit 592ea4c
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 25 deletions.
18 changes: 10 additions & 8 deletions src/Rules/Generics/UsedTraitsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Type;
use function array_map;
use function array_merge;
use function is_null;
use function sprintf;
use function strtolower;
use function ucfirst;
Expand Down Expand Up @@ -44,18 +46,18 @@ public function processNode(Node $node, Scope $scope): array
if ($scope->isInTrait()) {
$traitName = $scope->getTraitReflection()->getName();
}
$useTags = [];
$docComment = $node->getDocComment();
if ($docComment !== null) {
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$classResolvedPhpDoc = $scope->getClassReflection()->getResolvedPhpDoc();
$docComment = $node->getDocComment()?->getText();
$useTags = array_merge(
is_null($classResolvedPhpDoc) ? [] : $classResolvedPhpDoc->getUsesTags(),
is_null($docComment) ? [] : $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$className,
$traitName,
null,
$docComment->getText(),
);
$useTags = $resolvedPhpDoc->getUsesTags();
}
$docComment,
)->getUsesTags(),
);

$typeDescription = strtolower($scope->getClassReflection()->getClassTypeDescription());
$description = sprintf('%s %s', $typeDescription, SprintfHelper::escapeFormatString($className));
Expand Down
48 changes: 31 additions & 17 deletions src/Type/FileTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use function is_array;
use function is_callable;
use function is_file;
use function is_null;
use function ltrim;
use function md5;
use function sprintf;
Expand Down Expand Up @@ -421,7 +422,7 @@ private function createNameScopeMap(
$constUses = [];
$this->processNodes(
$this->phpParser->parseFile($fileName),
function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?int {
function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses, &$classNode, &$classUseTags): ?int {
if ($node instanceof Node\Stmt\ClassLike) {
if ($traitFound && $fileName === $originalClassFileName) {
return self::SKIP_NODE;
Expand Down Expand Up @@ -460,6 +461,8 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
}
$classStack[] = $className;
$classNameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, null);
$classNode = $node;
$classUseTags = null;
if (array_key_exists($classNameScopeKey, $phpDocNodeMap)) {
$typeAliasStack[] = $this->getTypeAliasesMap($phpDocNodeMap[$classNameScopeKey]);
} else {
Expand Down Expand Up @@ -629,7 +632,7 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
);
$finalTraitPhpDocMap = [];
foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) {
$finalTraitPhpDocMap[$nameScopeTraitKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScope {
$finalTraitPhpDocMap[$nameScopeTraitKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment, &$classNode, &$classUseTags): NameScope {
/** @var NameScope $original */
$original = $callback();
if (!$traitReflection->isGeneric()) {
Expand All @@ -639,27 +642,38 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
$traitTemplateTypeMap = $traitReflection->getTemplateTypeMap();

$useType = null;
if ($useDocComment !== null) {
$useTags = $this->getResolvedPhpDoc(
if (is_null($classUseTags)) {
$classDocComment = $classNode?->getDocComment()?->getText();
$classUseTags = is_null($classDocComment) ? [] : $this->getResolvedPhpDoc(
$fileName,
$className,
null,
null,
$classDocComment,
)->getUsesTags();
}
$useTags = array_merge(
$classUseTags,
is_null($useDocComment) ? [] : $this->getResolvedPhpDoc(
$fileName,
$className,
$lookForTrait,
null,
$useDocComment,
)->getUsesTags();
foreach ($useTags as $useTag) {
$useTagType = $useTag->getType();
if (!$useTagType instanceof GenericObjectType) {
continue;
}

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

$useType = $useTagType;
break;
)->getUsesTags(),
);
foreach ($useTags as $useTag) {
$useTagType = $useTag->getType();
if (!$useTagType instanceof GenericObjectType) {
continue;
}

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

$useType = $useTagType;
break;
}

if ($useType === null) {
Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public function testRule(): void
'Call-site variance annotation of covariant Throwable in generic type UsedTraits\GenericTrait<covariant Throwable> in PHPDoc tag @use is not allowed.',
69,
],
[
'Type int in generic type UsedTraits\GenericTrait<int> in PHPDoc tag @use is not subtype of template type T of object of trait UsedTraits\GenericTrait.',
91,
],
]);
}

Expand Down
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/Generics/data/used-traits.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,25 @@ class Dolor
use GenericTrait;

}

/**
* @template T of object
* @use GenericTrait<T>
*/
class Sit
{

use GenericTrait;

}

/**
* @template T of int
* @use GenericTrait<T>
*/
class Amet
{

use GenericTrait;

}

0 comments on commit 592ea4c

Please sign in to comment.