33declare (strict_types=1 );
44namespace Rector \BetterPhpDocParser \PhpDocInfo ;
55
6+ use RectorPrefix202606 \Nette \Utils \Strings ;
67use PHPStan \PhpDocParser \Ast \ConstExpr \ConstFetchNode ;
78use PHPStan \PhpDocParser \Ast \Node ;
89use PHPStan \PhpDocParser \Ast \PhpDoc \ExtendsTagValueNode ;
3334use Rector \BetterPhpDocParser \ValueObject \Type \ShortenedIdentifierTypeNode ;
3435use Rector \Exception \ShouldNotHappenException ;
3536use Rector \PhpDocParser \PhpDocParser \PhpDocNodeTraverser ;
37+ use Rector \StaticTypeMapper \Naming \NameScopeFactory ;
3638use Rector \StaticTypeMapper \StaticTypeMapper ;
3739use Rector \Validation \RectorAssert ;
3840use RectorPrefix202606 \Webmozart \Assert \InvalidArgumentException ;
@@ -65,6 +67,15 @@ final class PhpDocInfo
6567 * @readonly
6668 */
6769 private PhpDocNodeByTypeFinder $ phpDocNodeByTypeFinder ;
70+ /**
71+ * @readonly
72+ */
73+ private NameScopeFactory $ nameScopeFactory ;
74+ /**
75+ * @see https://regex101.com/r/7GCrlj/2
76+ * @var string
77+ */
78+ private const INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX = '#\{@(?:uses|used-by|see)\s+(?<class_name>[^}\s]+)# ' ;
6879 /**
6980 * @var array<class-string<PhpDocTagValueNode>, string>
7081 */
@@ -74,14 +85,15 @@ final class PhpDocInfo
7485 * @readonly
7586 */
7687 private PhpDocNode $ originalPhpDocNode ;
77- public function __construct (PhpDocNode $ phpDocNode , BetterTokenIterator $ betterTokenIterator , StaticTypeMapper $ staticTypeMapper , \PhpParser \Node $ node , AnnotationNaming $ annotationNaming , PhpDocNodeByTypeFinder $ phpDocNodeByTypeFinder )
88+ public function __construct (PhpDocNode $ phpDocNode , BetterTokenIterator $ betterTokenIterator , StaticTypeMapper $ staticTypeMapper , \PhpParser \Node $ node , AnnotationNaming $ annotationNaming , PhpDocNodeByTypeFinder $ phpDocNodeByTypeFinder, NameScopeFactory $ nameScopeFactory )
7889 {
7990 $ this ->phpDocNode = $ phpDocNode ;
8091 $ this ->betterTokenIterator = $ betterTokenIterator ;
8192 $ this ->staticTypeMapper = $ staticTypeMapper ;
8293 $ this ->node = $ node ;
8394 $ this ->annotationNaming = $ annotationNaming ;
8495 $ this ->phpDocNodeByTypeFinder = $ phpDocNodeByTypeFinder ;
96+ $ this ->nameScopeFactory = $ nameScopeFactory ;
8597 $ this ->originalPhpDocNode = clone $ phpDocNode ;
8698 if (!$ betterTokenIterator ->containsTokenType (Lexer::TOKEN_PHPDOC_EOL )) {
8799 $ this ->isSingleLine = \true;
@@ -411,6 +423,27 @@ public function getGenericTagClassNames(): array
411423 }
412424 return $ resolvedClasses ;
413425 }
426+ /**
427+ * @return string[]
428+ */
429+ public function getInlineGenericUsesTagClassNames (): array
430+ {
431+ $ printedPhpDocNode = (string ) $ this ->phpDocNode ;
432+ if (strpos ($ printedPhpDocNode , '{ ' ) === \false) {
433+ return [];
434+ }
435+ $ matches = Strings::matchAll ($ printedPhpDocNode , self ::INLINE_GENERIC_USES_CLASS_REFERENCE_REGEX );
436+ $ classNames = [];
437+ foreach ($ matches as $ match ) {
438+ $ reference = $ match ['class_name ' ];
439+ $ resolvedClassNames = $ this ->resolveInlineGenericUsesReferenceClassNames ($ reference );
440+ if ($ resolvedClassNames === []) {
441+ continue ;
442+ }
443+ $ classNames = array_merge ($ classNames , $ resolvedClassNames );
444+ }
445+ return array_unique ($ classNames );
446+ }
414447 /**
415448 * @return string[]
416449 */
@@ -468,6 +501,33 @@ private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagV
468501 }
469502 return null ;
470503 }
504+ /**
505+ * @return string[]
506+ */
507+ private function resolveInlineGenericUsesReferenceClassNames (string $ reference ): array
508+ {
509+ $ reference = explode ('| ' , $ reference , 2 )[0 ];
510+ $ reference = explode (':: ' , $ reference , 2 )[0 ];
511+ $ referenceToResolve = $ reference ;
512+ $ reference = ltrim ($ reference , '\\' );
513+ try {
514+ RectorAssert::className ($ reference );
515+ } catch (InvalidArgumentException $ exception ) {
516+ return [];
517+ }
518+ // fqcn not reference to any use statements
519+ if (strncmp ($ referenceToResolve , '\\' , strlen ('\\' )) === 0 ) {
520+ return [$ referenceToResolve ];
521+ }
522+ $ nameScope = $ this ->nameScopeFactory ->createNameScopeFromNodeWithoutTemplateTypes ($ this ->node );
523+ $ resolvedClassName = $ nameScope ->resolveStringName ($ referenceToResolve );
524+ if (strpos ($ reference , '\\' ) !== \false) {
525+ // Keep both forms: resolved class for namespace-aware matching and original class
526+ // for alias-partial matching in unused import checks.
527+ return array_unique ([$ resolvedClassName , $ reference ]);
528+ }
529+ return [$ resolvedClassName ];
530+ }
471531 /**
472532 * @return \PHPStan\Type\MixedType|\PHPStan\Type\Type
473533 */
0 commit comments