Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6633e28

Browse files
committedFeb 14, 2025·
Address feedback, add MemberKey, harden tests
1 parent 5ae71ff commit 6633e28

15 files changed

+1020
-199
lines changed
 

‎src/OpenApi/build/Microsoft.AspNetCore.OpenApi.targets

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
<Project>
33
<PropertyGroup>
44
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated</InterceptorsNamespaces>
5-
<InterceptorsPreviewNamespaces>
6-
$(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.OpenApi.Generated</InterceptorsPreviewNamespaces>
75
</PropertyGroup>
86
<Target Name="GenerateAdditionalXmlFilesForOpenApi"
97
AfterTargets="ResolveReferences">

‎src/OpenApi/gen/XmlCommentGenerator.Emitter.cs

+148-19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.CodeAnalysis.CSharp;
99
using Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
1010
using System.Threading;
11+
using System.Linq;
1112

1213
namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
1314

@@ -74,28 +75,149 @@ file record XmlComment(
7475
{{GeneratedCodeAttribute}}
7576
file record XmlResponseComment(string Code, string? Description, string? Example);
7677
78+
{{GeneratedCodeAttribute}}
79+
file sealed record MemberKey(
80+
Type? DeclaringType,
81+
MemberType MemberKind,
82+
string? Name,
83+
Type? ReturnType,
84+
Type[]? Parameters) : IEquatable<MemberKey>
85+
{
86+
public bool Equals(MemberKey? other)
87+
{
88+
if (other is null) return false;
89+
90+
// Check member kind
91+
if (MemberKind != other.MemberKind) return false;
92+
93+
// Check declaring type, handling generic types
94+
if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
95+
96+
// Check name
97+
if (Name != other.Name) return false;
98+
99+
// For methods, check return type and parameters
100+
if (MemberKind == MemberType.Method)
101+
{
102+
if (!TypesEqual(ReturnType, other.ReturnType)) return false;
103+
if (Parameters is null || other.Parameters is null) return false;
104+
if (Parameters.Length != other.Parameters.Length) return false;
105+
106+
for (int i = 0; i < Parameters.Length; i++)
107+
{
108+
if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
109+
}
110+
}
111+
112+
return true;
113+
}
114+
115+
private static bool TypesEqual(Type? type1, Type? type2)
116+
{
117+
if (type1 == type2) return true;
118+
if (type1 == null || type2 == null) return false;
119+
120+
if (type1.IsGenericType && type2.IsGenericType)
121+
{
122+
return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
123+
}
124+
125+
return type1 == type2;
126+
}
127+
128+
public override int GetHashCode()
129+
{
130+
var hash = new HashCode();
131+
hash.Add(GetTypeHashCode(DeclaringType));
132+
hash.Add(MemberKind);
133+
hash.Add(Name);
134+
135+
if (MemberKind == MemberType.Method)
136+
{
137+
hash.Add(GetTypeHashCode(ReturnType));
138+
if (Parameters is not null)
139+
{
140+
foreach (var param in Parameters)
141+
{
142+
hash.Add(GetTypeHashCode(param));
143+
}
144+
}
145+
}
146+
147+
return hash.ToHashCode();
148+
}
149+
150+
private static int GetTypeHashCode(Type? type)
151+
{
152+
if (type == null) return 0;
153+
return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
154+
}
155+
156+
public static MemberKey FromMethodInfo(MethodInfo method)
157+
{
158+
return new MemberKey(
159+
method.DeclaringType,
160+
MemberType.Method,
161+
method.Name,
162+
method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
163+
method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
164+
}
165+
166+
public static MemberKey FromPropertyInfo(PropertyInfo property)
167+
{
168+
return new MemberKey(
169+
property.DeclaringType,
170+
MemberType.Property,
171+
property.Name,
172+
null,
173+
null);
174+
}
175+
176+
public static MemberKey FromTypeInfo(Type type)
177+
{
178+
return new MemberKey(
179+
type,
180+
MemberType.Type,
181+
null,
182+
null,
183+
null);
184+
}
185+
}
186+
187+
file enum MemberType
188+
{
189+
Type,
190+
Property,
191+
Method
192+
}
193+
77194
{{GeneratedCodeAttribute}}
78195
file static class XmlCommentCache
79196
{
80-
private static Dictionary<(Type?, string?), XmlComment>? _cache;
81-
public static Dictionary<(Type?, string?), XmlComment> Cache => _cache ??= GenerateCacheEntries();
197+
private static Dictionary<MemberKey, XmlComment>? _cache;
198+
public static Dictionary<MemberKey, XmlComment> Cache => _cache ??= GenerateCacheEntries();
82199
83-
private static Dictionary<(Type?, string?), XmlComment> GenerateCacheEntries()
200+
private static Dictionary<MemberKey, XmlComment> GenerateCacheEntries()
84201
{
85-
var _cache = new Dictionary<(Type?, string?), XmlComment>();
202+
var _cache = new Dictionary<MemberKey, XmlComment>();
86203
{{commentsFromXmlFile}}
87204
{{commentsFromCompilation}}
88205
return _cache;
89206
}
90207
91-
internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
208+
internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
92209
{
93-
if (type is not null && type.IsGenericType)
210+
if (methodInfo is null)
94211
{
95-
type = type.GetGenericTypeDefinition();
212+
return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
96213
}
97214
98-
return XmlCommentCache.Cache.TryGetValue((type, memberName), out xmlComment);
215+
return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
216+
}
217+
218+
internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
219+
{
220+
return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
99221
}
100222
}
101223
@@ -112,7 +234,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
112234
{
113235
return Task.CompletedTask;
114236
}
115-
if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo.Name, out var methodComment))
237+
if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, out var methodComment))
116238
{
117239
if (methodComment.Summary is { } summary)
118240
{
@@ -167,7 +289,6 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
167289
{
168290
response.Value.Description = responseComment.Description;
169291
}
170-
171292
}
172293
}
173294
}
@@ -192,8 +313,7 @@ public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext
192313
}
193314
}
194315
}
195-
System.Diagnostics.Debugger.Break();
196-
if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, null, out var typeComment))
316+
if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, out var typeComment))
197317
{
198318
schema.Description = typeComment.Summary;
199319
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
@@ -268,9 +388,9 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, Ac
268388
{
269389
return services.AddOpenApi("v1", options =>
270390
{
271-
configureOptions(options);
272391
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
273392
options.AddOperationTransformer(new XmlCommentOperationTransformer());
393+
configureOptions(options);
274394
});
275395
}
276396
""",
@@ -280,9 +400,9 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, st
280400
// This overload is not intercepted.
281401
return OpenApiServiceCollectionExtensions.AddOpenApi(services, documentName, options =>
282402
{
283-
configureOptions(options);
284403
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
285404
options.AddOperationTransformer(new XmlCommentOperationTransformer());
405+
configureOptions(options);
286406
});
287407
}
288408
""",
@@ -307,20 +427,29 @@ internal static string GenerateAddOpenApiInterceptions(ImmutableArray<(AddOpenAp
307427
return writer.ToString();
308428
}
309429

310-
internal static string EmitCommentsCache(IEnumerable<(string, string?, XmlComment?)> comments, CancellationToken cancellationToken)
430+
internal static string EmitCommentsCache(IEnumerable<(MemberKey MemberKey, XmlComment? Comment)> comments, CancellationToken cancellationToken)
311431
{
312432
var writer = new StringWriter();
313433
var codeWriter = new CodeWriter(writer, baseIndent: 3);
314-
foreach (var (type, member, comment) in comments)
434+
foreach (var (memberKey, comment) in comments)
315435
{
316436
if (comment is not null)
317437
{
318-
var typeKey = $"(typeof({type})";
319-
var memberKey = member is not null ? $"{SymbolDisplay.FormatLiteral(member, true)}" : "null";
320-
codeWriter.WriteLine($"_cache.Add({typeKey}, {memberKey}), {EmitSourceGeneratedXmlComment(comment)});");
438+
codeWriter.WriteLine($"_cache.Add(new MemberKey(" +
439+
$"{FormatLiteralOrNull(memberKey.DeclaringType)}, " +
440+
$"MemberType.{memberKey.MemberKind}, " +
441+
$"{FormatLiteralOrNull(memberKey.Name, true)}, " +
442+
$"{FormatLiteralOrNull(memberKey.ReturnType)}, " +
443+
$"[{(memberKey.Parameters != null ? string.Join(", ", memberKey.Parameters.Select(p => SymbolDisplay.FormatLiteral(p, false))) : "")}]), " +
444+
$"{EmitSourceGeneratedXmlComment(comment)});");
321445
}
322446
}
323447
return writer.ToString();
448+
449+
static string FormatLiteralOrNull(string? input, bool quote = false)
450+
{
451+
return input == null ? "null" : SymbolDisplay.FormatLiteral(input, quote);
452+
}
324453
}
325454

326455
private static string FormatStringForCode(string? input)

‎src/OpenApi/gen/XmlCommentGenerator.Parser.cs

+28-48
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,38 @@
33

44
using System.Collections.Generic;
55
using System.Globalization;
6-
using System.Linq;
76
using System.Threading;
87
using System.Xml.Linq;
98
using Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
109
using Microsoft.CodeAnalysis;
1110
using Microsoft.CodeAnalysis.CSharp;
1211
using Microsoft.CodeAnalysis.CSharp.Syntax;
13-
using System.Text;
1412

1513
namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
1614

1715
public sealed partial class XmlCommentGenerator
1816
{
19-
private static readonly SymbolDisplayFormat _typeKeyFormat = new(
20-
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
21-
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
22-
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters);
23-
2417
internal static List<(string, string)> ParseXmlFile(AdditionalText additionalText, CancellationToken cancellationToken)
2518
{
2619
var text = additionalText.GetText(cancellationToken);
2720
if (text is null)
2821
{
2922
return [];
3023
}
31-
var xml = XDocument.Parse(text.ToString());
24+
XElement xml;
25+
try
26+
{
27+
xml = XElement.Parse(text.ToString());
28+
}
29+
catch
30+
{
31+
return [];
32+
}
3233
var members = xml.Descendants("member");
3334
var comments = new List<(string, string)>();
3435
foreach (var member in members)
3536
{
36-
var name = member.Attribute("name")?.Value;
37+
var name = member.Attribute(DocumentationCommentXmlNames.NameAttributeName)?.Value;
3738
if (name is not null)
3839
{
3940
comments.Add((name, member.ToString()));
@@ -82,55 +83,30 @@ public sealed partial class XmlCommentGenerator
8283
return comments;
8384
}
8485

85-
// Type names are used in a `typeof()` expression, so we need to replace generic arguments
86-
// with empty strings to avoid compiler errors.
87-
private static string ReplaceGenericArguments(string typeName)
88-
{
89-
var stack = new Stack<int>();
90-
var result = new StringBuilder(typeName);
91-
92-
for (var i = 0; i < result.Length; i++)
93-
{
94-
if (result[i] == '<')
95-
{
96-
stack.Push(i);
97-
}
98-
else if (result[i] == '>' && stack.Count > 0)
99-
{
100-
var start = stack.Pop();
101-
// Replace everything between < and > with empty strings separated by commas
102-
var segment = result.ToString(start + 1, i - start - 1);
103-
var commaCount = segment.Count(c => c == ',');
104-
var replacement = new string(',', commaCount);
105-
result.Remove(start + 1, i - start - 1);
106-
result.Insert(start + 1, replacement);
107-
i = start + replacement.Length + 1;
108-
}
109-
}
110-
111-
return result.ToString();
112-
}
113-
114-
internal static IEnumerable<(string, string?, XmlComment?)> ParseComments(
86+
internal static IEnumerable<(MemberKey, XmlComment?)> ParseComments(
11587
(List<(string, string)> RawComments, Compilation Compilation) input,
11688
CancellationToken cancellationToken)
11789
{
11890
var compilation = input.Compilation;
119-
var comments = new List<(string, string?, XmlComment?)>();
91+
var comments = new List<(MemberKey, XmlComment?)>();
12092
foreach (var (name, value) in input.RawComments)
12193
{
12294
if (DocumentationCommentId.GetFirstSymbolForDeclarationId(name, compilation) is ISymbol symbol)
12395
{
12496
var parsedComment = XmlComment.Parse(symbol, compilation, value, cancellationToken);
12597
if (parsedComment is not null)
12698
{
127-
var typeInfo = symbol is IPropertySymbol or IMethodSymbol
128-
? ReplaceGenericArguments(symbol.ContainingType.OriginalDefinition.ToDisplayString(_typeKeyFormat))
129-
: ReplaceGenericArguments(symbol.OriginalDefinition.ToDisplayString(_typeKeyFormat));
130-
var propertyInfo = symbol is IPropertySymbol or IMethodSymbol
131-
? symbol.Name
132-
: null;
133-
comments.Add((typeInfo, propertyInfo, parsedComment));
99+
var memberKey = symbol switch
100+
{
101+
IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol),
102+
IPropertySymbol propertySymbol => MemberKey.FromPropertySymbol(propertySymbol),
103+
INamedTypeSymbol typeSymbol => MemberKey.FromTypeSymbol(typeSymbol),
104+
_ => null
105+
};
106+
if (memberKey is not null)
107+
{
108+
comments.Add((memberKey, parsedComment));
109+
}
134110
}
135111
}
136112
}
@@ -174,10 +150,14 @@ internal static AddOpenApiInvocation GetAddOpenApiOverloadVariant(GeneratorSynta
174150
{
175151
return new(AddOpenApiOverloadVariant.AddOpenApiDocumentName, invocationExpression, interceptableLocation);
176152
}
177-
else
153+
else if (argument.Expression is LambdaExpressionSyntax)
178154
{
179155
return new(AddOpenApiOverloadVariant.AddOpenApiConfigureOptions, invocationExpression, interceptableLocation);
180156
}
157+
else
158+
{
159+
return new(AddOpenApiOverloadVariant.Unknown, invocationExpression, null);
160+
}
181161
}
182162
}
183163
}

0 commit comments

Comments
 (0)
Please sign in to comment.