44
55namespace Rector \BetterPhpDocParser \PhpDocInfo ;
66
7+ use Nette \Utils \Strings ;
78use PHPStan \PhpDocParser \Ast \ConstExpr \ConstFetchNode ;
89use PHPStan \PhpDocParser \Ast \Node ;
910use PHPStan \PhpDocParser \Ast \PhpDoc \ExtendsTagValueNode ;
3435use Rector \BetterPhpDocParser \ValueObject \Type \ShortenedIdentifierTypeNode ;
3536use Rector \Exception \ShouldNotHappenException ;
3637use Rector \PhpDocParser \PhpDocParser \PhpDocNodeTraverser ;
38+ use Rector \StaticTypeMapper \Naming \NameScopeFactory ;
3739use Rector \StaticTypeMapper \StaticTypeMapper ;
3840use Rector \Validation \RectorAssert ;
3941use Webmozart \Assert \InvalidArgumentException ;
4345 */
4446final 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) {
0 commit comments