From c4ca385f269be70638733268c398dbab74e679c0 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 23 Jun 2026 18:40:23 +0700 Subject: [PATCH 1/9] [PostRector] Skip inline {@see } used use statement on remove unused imports --- .../PhpDocInfo/PhpDocInfo.php | 41 +++++++++++++++++++ .../Rector/UnusedImportRemovingPostRector.php | 3 ++ .../Fixture/skip_inline_see.php.inc | 19 +++++++++ 3 files changed, 63 insertions(+) create mode 100644 tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index 6b89fc9ac3b..f6ab9e04cb9 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -43,6 +43,12 @@ */ final class PhpDocInfo { + /** + * @var string + * @see https://regex101.com/r/7GCrlj/1 + */ + private const string INLINE_SEE_CLASS_REFERENCE_REGEX = '#\{@see\s+([^}\s]+)#'; + /** * @var array, string> */ @@ -480,6 +486,26 @@ public function getGenericTagClassNames(): array return $resolvedClasses; } + /** + * @return string[] + */ + public function getInlineSeeTagClassNames(): array + { + preg_match_all(self::INLINE_SEE_CLASS_REFERENCE_REGEX, (string) $this->phpDocNode, $matches); + + $classNames = []; + foreach ($matches[1] as $reference) { + $className = $this->resolveInlineSeeReferenceClassName($reference); + if ($className === null) { + continue; + } + + $classNames[] = $className; + } + + return array_unique($classNames); + } + /** * @return string[] */ @@ -556,6 +582,21 @@ private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagV return null; } + private function resolveInlineSeeReferenceClassName(string $reference): ?string + { + $reference = explode('|', $reference, 2)[0]; + $reference = explode('::', $reference, 2)[0]; + $reference = ltrim($reference, '\\'); + + try { + RectorAssert::className($reference); + } catch (InvalidArgumentException) { + return null; + } + + return $reference; + } + private function getTypeOrMixed(?PhpDocTagValueNode $phpDocTagValueNode): MixedType | Type { if (! $phpDocTagValueNode instanceof PhpDocTagValueNode) { diff --git a/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index 8ba338c9ad6..8ee995d7b8a 100644 --- a/src/PostRector/Rector/UnusedImportRemovingPostRector.php +++ b/src/PostRector/Rector/UnusedImportRemovingPostRector.php @@ -169,6 +169,9 @@ private function findNamesInDocBlocks(Namespace_|FileNode $rootNode): array $genericTagClassNames = $phpDocInfo->getGenericTagClassNames(); $names = [...$names, ...$genericTagClassNames]; + $inlineSeeTagClassNames = $phpDocInfo->getInlineSeeTagClassNames(); + $names = [...$names, ...$inlineSeeTagClassNames]; + $arrayItemTagClassNames = $phpDocInfo->getArrayItemNodeClassNames(); $names = [...$names, ...$arrayItemTagClassNames]; } diff --git a/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc new file mode 100644 index 00000000000..5b72e00e184 --- /dev/null +++ b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc @@ -0,0 +1,19 @@ + Date: Tue, 23 Jun 2026 11:42:27 +0000 Subject: [PATCH 2/9] [ci-review] Rector Rectify --- src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index f6ab9e04cb9..b621f34b69f 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -44,7 +44,6 @@ final class PhpDocInfo { /** - * @var string * @see https://regex101.com/r/7GCrlj/1 */ private const string INLINE_SEE_CLASS_REFERENCE_REGEX = '#\{@see\s+([^}\s]+)#'; From bd3463fba8ae3def3abb5f4efe582451690790cb Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 23 Jun 2026 18:47:43 +0700 Subject: [PATCH 3/9] add support for uses and used-by --- .../PhpDocInfo/PhpDocInfo.php | 17 ++++++++++------- .../Rector/UnusedImportRemovingPostRector.php | 4 ++-- .../Fixture/skip_inline_see.php.inc | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index b621f34b69f..e5eef2da4bd 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -4,6 +4,7 @@ namespace Rector\BetterPhpDocParser\PhpDocInfo; +use Nette\Utils\Strings; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; @@ -44,9 +45,10 @@ final class PhpDocInfo { /** - * @see https://regex101.com/r/7GCrlj/1 + * @var string + * @see https://regex101.com/r/7GCrlj/2 */ - private const string INLINE_SEE_CLASS_REFERENCE_REGEX = '#\{@see\s+([^}\s]+)#'; + private const string INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX = '#\{@(?:uses|used-by|see)\s+(?[^}\s]+)#'; /** * @var array, string> @@ -488,13 +490,14 @@ public function getGenericTagClassNames(): array /** * @return string[] */ - public function getInlineSeeTagClassNames(): array + public function getInlineGenericUsesTagClassNames(): array { - preg_match_all(self::INLINE_SEE_CLASS_REFERENCE_REGEX, (string) $this->phpDocNode, $matches); + $matches = Strings::matchAll((string) $this->phpDocNode, self::INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX); $classNames = []; - foreach ($matches[1] as $reference) { - $className = $this->resolveInlineSeeReferenceClassName($reference); + foreach ($matches as $match) { + $reference = $match['class_name']; + $className = $this->resolveInlineGenericUsesReferenceClassName($reference); if ($className === null) { continue; } @@ -581,7 +584,7 @@ private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagV return null; } - private function resolveInlineSeeReferenceClassName(string $reference): ?string + private function resolveInlineGenericUsesReferenceClassName(string $reference): ?string { $reference = explode('|', $reference, 2)[0]; $reference = explode('::', $reference, 2)[0]; diff --git a/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index 8ee995d7b8a..120c30b3520 100644 --- a/src/PostRector/Rector/UnusedImportRemovingPostRector.php +++ b/src/PostRector/Rector/UnusedImportRemovingPostRector.php @@ -169,8 +169,8 @@ private function findNamesInDocBlocks(Namespace_|FileNode $rootNode): array $genericTagClassNames = $phpDocInfo->getGenericTagClassNames(); $names = [...$names, ...$genericTagClassNames]; - $inlineSeeTagClassNames = $phpDocInfo->getInlineSeeTagClassNames(); - $names = [...$names, ...$inlineSeeTagClassNames]; + $inlineGenericUsesTagClassNames = $phpDocInfo->getInlineGenericUsesTagClassNames(); + $names = [...$names, ...$inlineGenericUsesTagClassNames]; $arrayItemTagClassNames = $phpDocInfo->getArrayItemNodeClassNames(); $names = [...$names, ...$arrayItemTagClassNames]; diff --git a/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc index 5b72e00e184..978f4ab0b51 100644 --- a/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc +++ b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc @@ -11,8 +11,8 @@ use Rector\Tests\Issues\NamespacedUse\Source\SomeClassThird; final class SkipInlineSee { /** - * See {@see SomeClassFirst} for more information. - * See {@see SomeClassSecond::test()} for more information. + * See {@uses SomeClassFirst} for more information. + * See {@used-by SomeClassSecond::test()} for more information. * See {@see SomeClassThird::$property} for more information. */ public function test(): void {} From cf45de724461378d5101ecee74ab6dedebacd001 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 23 Jun 2026 11:50:35 +0000 Subject: [PATCH 4/9] [ci-review] Rector Rectify --- src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index e5eef2da4bd..78956d9e196 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -45,7 +45,6 @@ final class PhpDocInfo { /** - * @var string * @see https://regex101.com/r/7GCrlj/2 */ private const string INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX = '#\{@(?:uses|used-by|see)\s+(?[^}\s]+)#'; From c337573d39b47fbe6c9e9e2760f780fbc6dca134 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 24 Jun 2026 09:35:18 +0700 Subject: [PATCH 5/9] skip fqcn inline {@see \FQCN} --- .../PhpDocInfo/PhpDocInfo.php | 26 ++++++++++++++----- .../PhpDocInfo/PhpDocInfoFactory.php | 7 +++-- .../Fixture/skip_fqcn_inline_see.php.inc | 15 +++++++++++ ..._inline_see_prefixed_partial_alias.php.inc | 15 +++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc create mode 100644 tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see_prefixed_partial_alias.php.inc diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index 78956d9e196..8ae3fae3cff 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -35,6 +35,7 @@ use Rector\BetterPhpDocParser\ValueObject\Type\ShortenedIdentifierTypeNode; use Rector\Exception\ShouldNotHappenException; use Rector\PhpDocParser\PhpDocParser\PhpDocNodeTraverser; +use Rector\StaticTypeMapper\Naming\NameScopeFactory; use Rector\StaticTypeMapper\StaticTypeMapper; use Rector\Validation\RectorAssert; use Webmozart\Assert\InvalidArgumentException; @@ -72,7 +73,8 @@ public function __construct( private readonly StaticTypeMapper $staticTypeMapper, private readonly \PhpParser\Node $node, private readonly AnnotationNaming $annotationNaming, - private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder + private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder, + private readonly NameScopeFactory $nameScopeFactory ) { $this->originalPhpDocNode = clone $phpDocNode; @@ -496,12 +498,12 @@ public function getInlineGenericUsesTagClassNames(): array $classNames = []; foreach ($matches as $match) { $reference = $match['class_name']; - $className = $this->resolveInlineGenericUsesReferenceClassName($reference); - if ($className === null) { + $resolvedClassNames = $this->resolveInlineGenericUsesReferenceClassNames($reference); + if ($resolvedClassNames === []) { continue; } - $classNames[] = $className; + $classNames = [...$classNames, ...$resolvedClassNames]; } return array_unique($classNames); @@ -583,19 +585,29 @@ private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagV return null; } - private function resolveInlineGenericUsesReferenceClassName(string $reference): ?string + /** + * @return string[] + */ + private function resolveInlineGenericUsesReferenceClassNames(string $reference): array { $reference = explode('|', $reference, 2)[0]; $reference = explode('::', $reference, 2)[0]; + + $referenceToResolve = $reference; $reference = ltrim($reference, '\\'); try { RectorAssert::className($reference); } catch (InvalidArgumentException) { - return null; + return []; } - return $reference; + $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($this->node); + $resolvedClassName = $nameScope->resolveStringName($referenceToResolve); + + // Keep both forms: resolved class for namespace-aware matching and original class + // for alias-partial matching in unused import checks. + return array_unique([$resolvedClassName, $reference]); } private function getTypeOrMixed(?PhpDocTagValueNode $phpDocTagValueNode): MixedType | Type diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfoFactory.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfoFactory.php index 9a3375021f6..da18a89d026 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfoFactory.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfoFactory.php @@ -16,6 +16,7 @@ use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey; use Rector\BetterPhpDocParser\ValueObject\StartAndEnd; use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\StaticTypeMapper\Naming\NameScopeFactory; use Rector\StaticTypeMapper\StaticTypeMapper; final class PhpDocInfoFactory @@ -31,7 +32,8 @@ public function __construct( private readonly BetterPhpDocParser $betterPhpDocParser, private readonly StaticTypeMapper $staticTypeMapper, private readonly AnnotationNaming $annotationNaming, - private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder + private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder, + private readonly NameScopeFactory $nameScopeFactory ) { } @@ -127,7 +129,8 @@ private function createFromPhpDocNode( $this->staticTypeMapper, $node, $this->annotationNaming, - $this->phpDocNodeByTypeFinder + $this->phpDocNodeByTypeFinder, + $this->nameScopeFactory ); $node->setAttribute(AttributeKey::PHP_DOC_INFO, $phpDocInfo); diff --git a/tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc new file mode 100644 index 00000000000..ae6a0db751d --- /dev/null +++ b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc @@ -0,0 +1,15 @@ + Date: Wed, 24 Jun 2026 09:47:34 +0700 Subject: [PATCH 6/9] alias fix --- src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index 8ae3fae3cff..890da28efd2 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -605,9 +605,13 @@ private function resolveInlineGenericUsesReferenceClassNames(string $reference): $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($this->node); $resolvedClassName = $nameScope->resolveStringName($referenceToResolve); - // Keep both forms: resolved class for namespace-aware matching and original class - // for alias-partial matching in unused import checks. - return array_unique([$resolvedClassName, $reference]); + if (str_contains($reference, '\\')) { + // Keep both forms: resolved class for namespace-aware matching and original class + // for alias-partial matching in unused import checks. + return array_unique([$resolvedClassName, $reference]); + } + + return [$resolvedClassName]; } private function getTypeOrMixed(?PhpDocTagValueNode $phpDocTagValueNode): MixedType | Type From fc3d723968e6cf5a6ce8cc8ade7c160306d00d29 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 24 Jun 2026 10:02:49 +0700 Subject: [PATCH 7/9] final touch: fqcn is not reference to any use statements --- .../PhpDocInfo/PhpDocInfo.php | 5 +++ .../Fixture/fqcn_inline_see.php.inc | 33 +++++++++++++++++++ .../Fixture/skip_fqcn_inline_see.php.inc | 15 --------- 3 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc delete mode 100644 tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index 890da28efd2..7c4000245b5 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -602,6 +602,11 @@ private function resolveInlineGenericUsesReferenceClassNames(string $reference): return []; } + // fqcn not reference to any use statements + if (str_starts_with($referenceToResolve, '\\')) { + return []; + } + $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($this->node); $resolvedClassName = $nameScope->resolveStringName($referenceToResolve); diff --git a/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc b/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc new file mode 100644 index 00000000000..602fff9799b --- /dev/null +++ b/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc @@ -0,0 +1,33 @@ + +----- + \ No newline at end of file diff --git a/tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc deleted file mode 100644 index ae6a0db751d..00000000000 --- a/tests/Issues/NamespacedUseAutoImport/Fixture/skip_fqcn_inline_see.php.inc +++ /dev/null @@ -1,15 +0,0 @@ - Date: Wed, 24 Jun 2026 10:03:44 +0700 Subject: [PATCH 8/9] final touch: eof --- .../NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc b/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc index 602fff9799b..a99bbaf75a4 100644 --- a/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc +++ b/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc @@ -30,4 +30,4 @@ final class FqcnInlineSee public function test(): void {} } -?> \ No newline at end of file +?> From befc6a0ad75add29b99976eead7131ef964dc0b2 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 24 Jun 2026 19:26:21 +0700 Subject: [PATCH 9/9] final touch: avoid regex lookup when no { char found --- src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index 7c4000245b5..e406d0f2195 100644 --- a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php +++ b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php @@ -493,7 +493,12 @@ public function getGenericTagClassNames(): array */ public function getInlineGenericUsesTagClassNames(): array { - $matches = Strings::matchAll((string) $this->phpDocNode, self::INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX); + $printedPhpDocNode = (string) $this->phpDocNode; + if (! str_contains($printedPhpDocNode, '{')) { + return []; + } + + $matches = Strings::matchAll($printedPhpDocNode, self::INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX); $classNames = []; foreach ($matches as $match) { @@ -604,7 +609,7 @@ private function resolveInlineGenericUsesReferenceClassNames(string $reference): // fqcn not reference to any use statements if (str_starts_with($referenceToResolve, '\\')) { - return []; + return [$referenceToResolve]; } $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($this->node);