Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.

Commit d1704ad

Browse files
a-gubskiyCopilotYuriyDurov
authored
Fix generics and ctors processing (#42)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Yuriy Durov <yu.durov@gmail.com>
1 parent 8df6cfb commit d1704ad

11 files changed

Lines changed: 363 additions & 131 deletions

File tree

src/BitzArt.XDoc/DocumentationElements/MemberDocumentation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ internal FieldDocumentation(XDoc source, FieldInfo field, XmlNode? node)
5959
}
6060

6161
/// <inheritdoc/>
62-
public sealed class MethodDocumentation : MemberDocumentation<MethodInfo>
62+
public sealed class MethodDocumentation : MemberDocumentation<MethodBase>
6363
{
64-
internal MethodDocumentation(XDoc source, MethodInfo method, XmlNode? node)
64+
internal MethodDocumentation(XDoc source, MethodBase method, XmlNode? node)
6565
: base(source, method, node) { }
6666
}
6767

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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&lt;System.Int32&gt;)").
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

Comments
 (0)