diff --git a/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php b/src/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php index 6b89fc9ac3b..e406d0f2195 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; @@ -34,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; @@ -43,6 +45,11 @@ */ final class PhpDocInfo { + /** + * @see https://regex101.com/r/7GCrlj/2 + */ + private const string INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX = '#\{@(?:uses|used-by|see)\s+(?[^}\s]+)#'; + /** * @var array, string> */ @@ -66,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; @@ -480,6 +488,32 @@ public function getGenericTagClassNames(): array return $resolvedClasses; } + /** + * @return string[] + */ + public function getInlineGenericUsesTagClassNames(): array + { + $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) { + $reference = $match['class_name']; + $resolvedClassNames = $this->resolveInlineGenericUsesReferenceClassNames($reference); + if ($resolvedClassNames === []) { + continue; + } + + $classNames = [...$classNames, ...$resolvedClassNames]; + } + + return array_unique($classNames); + } + /** * @return string[] */ @@ -556,6 +590,40 @@ private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagV return null; } + /** + * @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 []; + } + + // fqcn not reference to any use statements + if (str_starts_with($referenceToResolve, '\\')) { + return [$referenceToResolve]; + } + + $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($this->node); + $resolvedClassName = $nameScope->resolveStringName($referenceToResolve); + + 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 { if (! $phpDocTagValueNode instanceof PhpDocTagValueNode) { 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/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index 8ba338c9ad6..120c30b3520 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]; + $inlineGenericUsesTagClassNames = $phpDocInfo->getInlineGenericUsesTagClassNames(); + $names = [...$names, ...$inlineGenericUsesTagClassNames]; + $arrayItemTagClassNames = $phpDocInfo->getArrayItemNodeClassNames(); $names = [...$names, ...$arrayItemTagClassNames]; } 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..a99bbaf75a4 --- /dev/null +++ b/tests/Issues/NamespacedUseAutoImport/Fixture/fqcn_inline_see.php.inc @@ -0,0 +1,33 @@ + +----- + 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..978f4ab0b51 --- /dev/null +++ b/tests/Issues/NamespacedUseAutoImport/Fixture/skip_inline_see.php.inc @@ -0,0 +1,19 @@ +