Skip to content

Commit a181f3f

Browse files
[PostRector] Skip inline {@see } used use statement on remove unused imports (#8080)
* [PostRector] Skip inline {@see } used use statement on remove unused imports * [ci-review] Rector Rectify * add support for uses and used-by * [ci-review] Rector Rectify * skip fqcn inline {@see \FQCN} * alias fix * final touch: fqcn is not reference to any use statements * final touch: eof * final touch: avoid regex lookup when no { char found --------- Co-authored-by: GitHub Action <actions@github.com>
1 parent a7b3eb3 commit a181f3f

6 files changed

Lines changed: 144 additions & 3 deletions

File tree

src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Rector\BetterPhpDocParser\PhpDocInfo;
66

7+
use Nette\Utils\Strings;
78
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
89
use PHPStan\PhpDocParser\Ast\Node;
910
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
@@ -34,6 +35,7 @@
3435
use Rector\BetterPhpDocParser\ValueObject\Type\ShortenedIdentifierTypeNode;
3536
use Rector\Exception\ShouldNotHappenException;
3637
use Rector\PhpDocParser\PhpDocParser\PhpDocNodeTraverser;
38+
use Rector\StaticTypeMapper\Naming\NameScopeFactory;
3739
use Rector\StaticTypeMapper\StaticTypeMapper;
3840
use Rector\Validation\RectorAssert;
3941
use Webmozart\Assert\InvalidArgumentException;
@@ -43,6 +45,11 @@
4345
*/
4446
final class PhpDocInfo
4547
{
48+
/**
49+
* @see https://regex101.com/r/7GCrlj/2
50+
*/
51+
private const string INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX = '#\{@(?:uses|used-by|see)\s+(?<class_name>[^}\s]+)#';
52+
4653
/**
4754
* @var array<class-string<PhpDocTagValueNode>, string>
4855
*/
@@ -66,7 +73,8 @@ public function __construct(
6673
private readonly StaticTypeMapper $staticTypeMapper,
6774
private readonly \PhpParser\Node $node,
6875
private readonly AnnotationNaming $annotationNaming,
69-
private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder
76+
private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder,
77+
private readonly NameScopeFactory $nameScopeFactory
7078
) {
7179
$this->originalPhpDocNode = clone $phpDocNode;
7280

@@ -480,6 +488,32 @@ public function getGenericTagClassNames(): array
480488
return $resolvedClasses;
481489
}
482490

491+
/**
492+
* @return string[]
493+
*/
494+
public function getInlineGenericUsesTagClassNames(): array
495+
{
496+
$printedPhpDocNode = (string) $this->phpDocNode;
497+
if (! str_contains($printedPhpDocNode, '{')) {
498+
return [];
499+
}
500+
501+
$matches = Strings::matchAll($printedPhpDocNode, self::INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX);
502+
503+
$classNames = [];
504+
foreach ($matches as $match) {
505+
$reference = $match['class_name'];
506+
$resolvedClassNames = $this->resolveInlineGenericUsesReferenceClassNames($reference);
507+
if ($resolvedClassNames === []) {
508+
continue;
509+
}
510+
511+
$classNames = [...$classNames, ...$resolvedClassNames];
512+
}
513+
514+
return array_unique($classNames);
515+
}
516+
483517
/**
484518
* @return string[]
485519
*/
@@ -556,6 +590,40 @@ private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagV
556590
return null;
557591
}
558592

593+
/**
594+
* @return string[]
595+
*/
596+
private function resolveInlineGenericUsesReferenceClassNames(string $reference): array
597+
{
598+
$reference = explode('|', $reference, 2)[0];
599+
$reference = explode('::', $reference, 2)[0];
600+
601+
$referenceToResolve = $reference;
602+
$reference = ltrim($reference, '\\');
603+
604+
try {
605+
RectorAssert::className($reference);
606+
} catch (InvalidArgumentException) {
607+
return [];
608+
}
609+
610+
// fqcn not reference to any use statements
611+
if (str_starts_with($referenceToResolve, '\\')) {
612+
return [$referenceToResolve];
613+
}
614+
615+
$nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($this->node);
616+
$resolvedClassName = $nameScope->resolveStringName($referenceToResolve);
617+
618+
if (str_contains($reference, '\\')) {
619+
// Keep both forms: resolved class for namespace-aware matching and original class
620+
// for alias-partial matching in unused import checks.
621+
return array_unique([$resolvedClassName, $reference]);
622+
}
623+
624+
return [$resolvedClassName];
625+
}
626+
559627
private function getTypeOrMixed(?PhpDocTagValueNode $phpDocTagValueNode): MixedType | Type
560628
{
561629
if (! $phpDocTagValueNode instanceof PhpDocTagValueNode) {

src/BetterPhpDocParser/PhpDocInfo/PhpDocInfoFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey;
1717
use Rector\BetterPhpDocParser\ValueObject\StartAndEnd;
1818
use Rector\NodeTypeResolver\Node\AttributeKey;
19+
use Rector\StaticTypeMapper\Naming\NameScopeFactory;
1920
use Rector\StaticTypeMapper\StaticTypeMapper;
2021

2122
final class PhpDocInfoFactory
@@ -31,7 +32,8 @@ public function __construct(
3132
private readonly BetterPhpDocParser $betterPhpDocParser,
3233
private readonly StaticTypeMapper $staticTypeMapper,
3334
private readonly AnnotationNaming $annotationNaming,
34-
private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder
35+
private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder,
36+
private readonly NameScopeFactory $nameScopeFactory
3537
) {
3638
}
3739

@@ -127,7 +129,8 @@ private function createFromPhpDocNode(
127129
$this->staticTypeMapper,
128130
$node,
129131
$this->annotationNaming,
130-
$this->phpDocNodeByTypeFinder
132+
$this->phpDocNodeByTypeFinder,
133+
$this->nameScopeFactory
131134
);
132135

133136
$node->setAttribute(AttributeKey::PHP_DOC_INFO, $phpDocInfo);

src/PostRector/Rector/UnusedImportRemovingPostRector.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ private function findNamesInDocBlocks(Namespace_|FileNode $rootNode): array
169169
$genericTagClassNames = $phpDocInfo->getGenericTagClassNames();
170170
$names = [...$names, ...$genericTagClassNames];
171171

172+
$inlineGenericUsesTagClassNames = $phpDocInfo->getInlineGenericUsesTagClassNames();
173+
$names = [...$names, ...$inlineGenericUsesTagClassNames];
174+
172175
$arrayItemTagClassNames = $phpDocInfo->getArrayItemNodeClassNames();
173176
$names = [...$names, ...$arrayItemTagClassNames];
174177
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Issues\NamespacedUseAutoImport\Fixture;
6+
7+
use Rector\Tests\Issues\NamespacedUse\Source\SomeClass;
8+
9+
final class FqcnInlineSee
10+
{
11+
/**
12+
* See {@see \Rector\Tests\Issues\NamespacedUse\Source\SomeClass::$property} for more information.
13+
*/
14+
public function test(): void {}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
declare(strict_types=1);
22+
23+
namespace Rector\Tests\Issues\NamespacedUseAutoImport\Fixture;
24+
25+
final class FqcnInlineSee
26+
{
27+
/**
28+
* See {@see \Rector\Tests\Issues\NamespacedUse\Source\SomeClass::$property} for more information.
29+
*/
30+
public function test(): void {}
31+
}
32+
33+
?>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Issues\NamespacedUseAutoImport\Fixture;
6+
7+
use Rector\Tests\Issues\NamespacedUse\Source\SomeClassFirst;
8+
use Rector\Tests\Issues\NamespacedUse\Source\SomeClassSecond;
9+
use Rector\Tests\Issues\NamespacedUse\Source\SomeClassThird;
10+
11+
final class SkipInlineSee
12+
{
13+
/**
14+
* See {@uses SomeClassFirst} for more information.
15+
* See {@used-by SomeClassSecond::test()} for more information.
16+
* See {@see SomeClassThird::$property} for more information.
17+
*/
18+
public function test(): void {}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Issues\NamespacedUseAutoImport\Fixture;
6+
7+
use Rector\Tests\Issues\NamespacedUseAutoImport as Alias;
8+
9+
final class SkipInlineSeePrefixedPartialAlias
10+
{
11+
/**
12+
* See {@see Alias\Source\SomeClass} for more information.
13+
*/
14+
public function test(): void {}
15+
}

0 commit comments

Comments
 (0)