Skip to content

Commit 9c67617

Browse files
authoredMar 31, 2025
Harden handling for generic type arguments in source generator (#61145)
* Harden handling for generic type arguments in source generator * Address feedback * Switch to documentation IDs for cache keys * Remove Debugger and LINQ, format ints * Add array tests, remove unneeded formatting, rename _cache * Commit updated snapshots * Quarantine OpenApiReference tests
1 parent 25a9b32 commit 9c67617

13 files changed

+1693
-1024
lines changed
 

Diff for: ‎src/OpenApi/gen/XmlCommentGenerator.Emitter.cs

+189-119
Large diffs are not rendered by default.

Diff for: ‎src/OpenApi/gen/XmlCommentGenerator.Parser.cs

+4-14
Original file line numberDiff line numberDiff line change
@@ -83,36 +83,26 @@ public sealed partial class XmlCommentGenerator
8383
return comments;
8484
}
8585

86-
internal static IEnumerable<(MemberKey, XmlComment?)> ParseComments(
86+
internal static IEnumerable<(string, XmlComment?)> ParseComments(
8787
(List<(string, string)> RawComments, Compilation Compilation) input,
8888
CancellationToken cancellationToken)
8989
{
9090
var compilation = input.Compilation;
91-
var comments = new List<(MemberKey, XmlComment?)>();
91+
var comments = new List<(string, XmlComment?)>();
9292
foreach (var (name, value) in input.RawComments)
9393
{
9494
if (DocumentationCommentId.GetFirstSymbolForDeclarationId(name, compilation) is ISymbol symbol &&
9595
// Only include symbols that are declared in the application assembly or are
9696
// accessible from the application assembly.
97-
(SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, input.Compilation.Assembly) || symbol.IsAccessibleType()) &&
97+
(SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, compilation.Assembly) || symbol.IsAccessibleType()) &&
9898
// Skip static classes that are just containers for members with annotations
9999
// since they cannot be instantiated.
100100
symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsStatic: true })
101101
{
102102
var parsedComment = XmlComment.Parse(symbol, compilation, value, cancellationToken);
103103
if (parsedComment is not null)
104104
{
105-
var memberKey = symbol switch
106-
{
107-
IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol, input.Compilation),
108-
IPropertySymbol propertySymbol => MemberKey.FromPropertySymbol(propertySymbol),
109-
INamedTypeSymbol typeSymbol => MemberKey.FromTypeSymbol(typeSymbol),
110-
_ => null
111-
};
112-
if (memberKey is not null)
113-
{
114-
comments.Add((memberKey, parsedComment));
115-
}
105+
comments.Add((name, parsedComment));
116106
}
117107
}
118108
}

Diff for: ‎src/OpenApi/gen/XmlComments/MemberKey.cs

-143
This file was deleted.

Diff for: ‎src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/CompletenessTests.cs

+94
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public async Task SupportsAllXmlTagsOnSchemas()
1717
{
1818
var source = """
1919
using System;
20+
using System.Collections.Generic;
2021
using System.Threading.Tasks;
2122
using Microsoft.AspNetCore.Builder;
2223
using Microsoft.Extensions.DependencyInjection;
@@ -36,6 +37,7 @@ public async Task SupportsAllXmlTagsOnSchemas()
3637
app.MapPost("/inherit-only-returns", (InheritOnlyReturns returns) => { });
3738
app.MapPost("/inherit-all-but-remarks", (InheritAllButRemarks remarks) => { });
3839
app.MapPost("/generic-class", (GenericClass<string> generic) => { });
40+
app.MapPost("/generic-parent", (GenericParent parent) => { });
3941
app.MapPost("/params-and-param-refs", (ParamsAndParamRefs refs) => { });
4042
4143
@@ -335,6 +337,89 @@ public class GenericClass<T>
335337
// Fields and members.
336338
}
337339
340+
/// <summary>
341+
/// This class validates the behavior for mapping
342+
/// generic types to open generics for use in
343+
/// typeof expressions.
344+
/// </summary>
345+
public class GenericParent
346+
{
347+
/// <summary>
348+
/// This property is a nullable value type.
349+
/// </summary>
350+
public int? Id { get; set; }
351+
352+
/// <summary>
353+
/// This property is a nullable reference type.
354+
/// </summary>
355+
public string? Name { get; set; }
356+
357+
/// <summary>
358+
/// This property is a generic type containing a tuple.
359+
/// </summary>
360+
public Task<(int, string)> TaskOfTupleProp { get; set; }
361+
362+
/// <summary>
363+
/// This property is a tuple with a generic type inside.
364+
/// </summary>
365+
public (int, Dictionary<int, string>) TupleWithGenericProp { get; set; }
366+
367+
/// <summary>
368+
/// This property is a tuple with a nested generic type inside.
369+
/// </summary>
370+
public (int, Dictionary<int, Dictionary<string, int>>) TupleWithNestedGenericProp { get; set; }
371+
372+
/// <summary>
373+
/// This method returns a generic type containing a tuple.
374+
/// </summary>
375+
public static Task<(int, string)> GetTaskOfTuple()
376+
{
377+
return Task.FromResult((1, "test"));
378+
}
379+
380+
/// <summary>
381+
/// This method returns a tuple with a generic type inside.
382+
/// </summary>
383+
public static (int, Dictionary<int, string>) GetTupleOfTask()
384+
{
385+
return (1, new Dictionary<int, string>());
386+
}
387+
388+
/// <summary>
389+
/// This method return a tuple with a generic type containing a
390+
/// type parameter inside.
391+
/// </summary>
392+
public static (int, Dictionary<int, T>) GetTupleOfTask1<T>()
393+
{
394+
return (1, new Dictionary<int, T>());
395+
}
396+
397+
/// <summary>
398+
/// This method return a tuple with a generic type containing a
399+
/// type parameter inside.
400+
/// </summary>
401+
public static (T, Dictionary<int, string>) GetTupleOfTask2<T>()
402+
{
403+
return (default, new Dictionary<int, string>());
404+
}
405+
406+
/// <summary>
407+
/// This method returns a nested generic with all types resolved.
408+
/// </summary>
409+
public static Dictionary<int, Dictionary<int, string>> GetNestedGeneric()
410+
{
411+
return new Dictionary<int, Dictionary<int, string>>();
412+
}
413+
414+
/// <summary>
415+
/// This method returns a nested generic with a type parameter.
416+
/// </summary>
417+
public static Dictionary<int, Dictionary<int, T>> GetNestedGeneric1<T>()
418+
{
419+
return new Dictionary<int, Dictionary<int, T>>();
420+
}
421+
}
422+
338423
/// <summary>
339424
/// This shows examples of typeparamref and typeparam tags
340425
/// </summary>
@@ -394,6 +479,15 @@ await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
394479
var genericClass = path.RequestBody.Content["application/json"].Schema;
395480
Assert.Equal("This is a generic class.", genericClass.Description);
396481

482+
path = document.Paths["/generic-parent"].Operations[OperationType.Post];
483+
var genericParent = path.RequestBody.Content["application/json"].Schema;
484+
Assert.Equal("This class validates the behavior for mapping\ngeneric types to open generics for use in\ntypeof expressions.", genericParent.Description, ignoreLineEndingDifferences: true);
485+
Assert.Equal("This property is a nullable value type.", genericParent.Properties["id"].Description);
486+
Assert.Equal("This property is a nullable reference type.", genericParent.Properties["name"].Description);
487+
Assert.Equal("This property is a generic type containing a tuple.", genericParent.Properties["taskOfTupleProp"].Description);
488+
Assert.Equal("This property is a tuple with a generic type inside.", genericParent.Properties["tupleWithGenericProp"].Description);
489+
Assert.Equal("This property is a tuple with a nested generic type inside.", genericParent.Properties["tupleWithNestedGenericProp"].Description);
490+
397491
path = document.Paths["/params-and-param-refs"].Operations[OperationType.Post];
398492
var paramsAndParamRefs = path.RequestBody.Content["application/json"].Schema;
399493
Assert.Equal("This shows examples of typeparamref and typeparam tags", paramsAndParamRefs.Description);

0 commit comments

Comments
 (0)