1+ using System . Text . RegularExpressions ;
2+
3+ namespace BitzArt . XDoc ;
4+
5+ /// <summary>
6+ /// Provides utility methods for parsing and resolving information from fully qualified member signatures,
7+ /// such as extracting type names, member names, and method parameter types.
8+ /// Handles special cases including generic types and nested generic parameters.
9+ /// </summary>
10+ internal static partial class XmlMemberNameResolver
11+ {
12+ public record TypeAndMemberName ( string TypeName , string MemberName ) ;
13+
14+ /// <summary>
15+ /// Resolves a qualified member name into its associated type and member name.
16+ /// Handles special cases like generic types and methods with parameters.
17+ /// </summary>
18+ /// <param name="xmlDocumentationMemberName">
19+ /// The fully qualified member name with prefix (e.g. "P:Company.Name.Space.TypeName.MemberName")
20+ /// </param>
21+ /// <returns>A tuple containing the resolved Type and the simple member name</returns>
22+ /// <exception cref="InvalidOperationException">
23+ /// Thrown when the name doesn't contain a type/member separator or when the type cannot be found
24+ /// </exception>
25+ public static TypeAndMemberName ResolveTypeAndMemberName ( string xmlDocumentationMemberName )
26+ {
27+ // A member name containing an opening parenthesis indicates a method.
28+ if ( xmlDocumentationMemberName . Contains ( '(' ) )
29+ {
30+ // Remove method parameter information from the XML documentation member name by
31+ // truncating the string at the opening parenthesis, keeping only the method name part.
32+ // This ensures we extract just the method name without its parameter signature.
33+ xmlDocumentationMemberName = xmlDocumentationMemberName [ ..xmlDocumentationMemberName . IndexOf ( '(' ) ] ;
34+ }
35+
36+ if ( ! xmlDocumentationMemberName . Contains ( '.' ) )
37+ {
38+ throw new InvalidOperationException (
39+ $ "XML documentation member name '{ xmlDocumentationMemberName } ' does not contain a type separator.") ;
40+ }
41+
42+ // Find the position of the last dot in the member name, which separates
43+ // the type name from the member name (e.g., "Namespace.TypeName.MemberName" -> position of last dot)
44+ var indexOfLastDot = xmlDocumentationMemberName . LastIndexOf ( '.' ) ;
45+
46+ var typeName = xmlDocumentationMemberName [ ..indexOfLastDot ] ;
47+ var memberName = xmlDocumentationMemberName [ ( indexOfLastDot + 1 ) ..] ;
48+
49+ // Backtick (`) => generic type or method.
50+ if ( xmlDocumentationMemberName . Contains ( '`' ) )
51+ {
52+ // Opening parenthesis indicates a method with parameters.
53+ if ( memberName . Contains ( '(' ) )
54+ {
55+ memberName = memberName [ ..memberName . IndexOf ( '(' ) ] ;
56+ }
57+
58+ // A backtick (`) in the member name
59+ // indicates generic type parameters.
60+ if ( memberName . Contains ( '`' ) )
61+ {
62+ // Remove generic type parameters from the member name
63+ memberName = memberName [ ..memberName . IndexOf ( '`' ) ] ;
64+ }
65+ }
66+
67+ return new ( typeName , memberName ) ;
68+ }
69+
70+ /// <summary>
71+ /// Extracts the method parameter type names from a fully qualified member signature.
72+ /// Handles nested generic parameters and returns a collection of cleaned parameter type names.
73+ /// </summary>
74+ /// <param name="xmlDocumentationMemberName">
75+ /// The fully qualified member signature, including parameter list (e.g., "Namespace.TypeName.MethodName(System.String, System.Collections.Generic.List<System.Int32>)").
76+ /// </param>
77+ /// <returns>
78+ /// A read-only collection of parameter type names as strings. Returns an empty collection if no parameters are found.
79+ /// </returns>
80+ public static IReadOnlyCollection < string > ResolveMethodParameters ( string xmlDocumentationMemberName )
81+ {
82+ var parameterListStartIndex = xmlDocumentationMemberName . IndexOf ( '(' ) ;
83+
84+ if ( parameterListStartIndex == - 1 )
85+ {
86+ // No parameter list found
87+ return [ ] ;
88+ }
89+
90+ var parameterListEndIndex = xmlDocumentationMemberName . LastIndexOf ( ')' ) ;
91+
92+ if ( parameterListEndIndex <= parameterListStartIndex )
93+ {
94+ throw new InvalidOperationException (
95+ $ "XML documentation member '{ xmlDocumentationMemberName } ' parameter list is invalid.") ;
96+ }
97+
98+ var parametersString = xmlDocumentationMemberName . Substring (
99+ parameterListStartIndex + 1 ,
100+ parameterListEndIndex - parameterListStartIndex - 1 ) ;
101+
102+ if ( string . IsNullOrWhiteSpace ( parametersString ) )
103+ {
104+ // No parameters found
105+ return [ ] ;
106+ }
107+
108+ // Handle nested generic parameters while tracking nesting depth
109+ return ParseParameterList ( parametersString ) ;
110+ }
111+
112+ /// <summary>
113+ /// Parses a parameter list string into individual parameter type names.
114+ /// Handles nested generic arguments by tracking bracket depth.
115+ /// </summary>
116+ /// <param name="parametersString">String containing comma-separated parameter type names.</param>
117+ /// <returns>A collection of parsed and cleaned parameter type names.</returns>
118+ private static List < string > ParseParameterList ( string parametersString )
119+ {
120+ var result = new List < string > ( ) ;
121+ var currentParam = string . Empty ;
122+ var nestingDepth = 0 ;
123+
124+ foreach ( var c in parametersString )
125+ {
126+ switch ( c )
127+ {
128+ case '<' or '{' :
129+ nestingDepth ++ ;
130+ break ;
131+
132+ case '>' or '}' :
133+ nestingDepth -- ;
134+ break ;
135+
136+ case ',' when nestingDepth == 0 :
137+ {
138+ currentParam = currentParam . Trim ( ) ;
139+ currentParam = RemoveGenericMarkers ( currentParam ) ;
140+
141+ result . Add ( RemoveGenericMarkers ( currentParam ) ) ;
142+
143+ currentParam = string . Empty ;
144+
145+ continue ;
146+ }
147+ }
148+
149+ currentParam += c ;
150+ }
151+
152+ if ( ! string . IsNullOrWhiteSpace ( currentParam ) )
153+ {
154+ result . Add ( RemoveGenericMarkers ( currentParam . Trim ( ) ) ) ;
155+ }
156+
157+ return result ;
158+ }
159+
160+ /// <summary>
161+ /// Matches generic markers like `0, `1, etc. and any standalone backticks
162+ /// </summary>
163+ /// <returns></returns>
164+ [ GeneratedRegex ( @"\`\d+|\`" ) ]
165+ private static partial Regex GetGenericMarkerRegex ( ) ;
166+
167+ internal static string RemoveGenericMarkers ( string value ) => GetGenericMarkerRegex ( ) . Replace ( value , string . Empty ) ;
168+ }
0 commit comments