Skip to content

Commit 7215cca

Browse files
Copilotcaptainsafia
andcommitted
Implement NormalizeDocId fix for XML comment generator
Co-authored-by: captainsafia <[email protected]>
1 parent 4d97c9b commit 7215cca

6 files changed

+2152
-4
lines changed

src/OpenApi/gen/XmlCommentGenerator.Parser.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,32 @@ namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
1414

1515
public sealed partial class XmlCommentGenerator
1616
{
17+
/// <summary>
18+
/// Normalizes a documentation comment ID to match the compiler-style format.
19+
/// Strips the return type suffix for ordinary methods but retains it for conversion operators.
20+
/// </summary>
21+
/// <param name="docId">The documentation comment ID to normalize.</param>
22+
/// <returns>The normalized documentation comment ID.</returns>
23+
internal static string NormalizeDocId(string docId)
24+
{
25+
// Find the tilde character that indicates the return type suffix
26+
var tildeIndex = docId.IndexOf('~');
27+
if (tildeIndex == -1)
28+
{
29+
// No return type suffix, return as-is
30+
return docId;
31+
}
32+
33+
// Check if this is a conversion operator (op_Implicit or op_Explicit)
34+
// For these operators, we need to keep the return type suffix
35+
if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
36+
{
37+
return docId;
38+
}
39+
40+
// For ordinary methods, strip the return type suffix
41+
return docId.Substring(0, tildeIndex);
42+
}
1743
internal static List<(string, string)> ParseXmlFile(AdditionalText additionalText, CancellationToken cancellationToken)
1844
{
1945
var text = additionalText.GetText(cancellationToken);
@@ -37,7 +63,7 @@ public sealed partial class XmlCommentGenerator
3763
var name = member.Attribute(DocumentationCommentXmlNames.NameAttributeName)?.Value;
3864
if (name is not null)
3965
{
40-
comments.Add((name, member.ToString()));
66+
comments.Add((NormalizeDocId(name), member.ToString()));
4167
}
4268
}
4369
return comments;
@@ -54,7 +80,7 @@ public sealed partial class XmlCommentGenerator
5480
if (DocumentationCommentId.CreateDeclarationId(type) is string name &&
5581
type.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
5682
{
57-
comments.Add((name, xml));
83+
comments.Add((NormalizeDocId(name), xml));
5884
}
5985
}
6086
var properties = visitor.GetPublicProperties();
@@ -63,7 +89,7 @@ public sealed partial class XmlCommentGenerator
6389
if (DocumentationCommentId.CreateDeclarationId(property) is string name &&
6490
property.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
6591
{
66-
comments.Add((name, xml));
92+
comments.Add((NormalizeDocId(name), xml));
6793
}
6894
}
6995
var methods = visitor.GetPublicMethods();
@@ -77,7 +103,7 @@ public sealed partial class XmlCommentGenerator
77103
if (DocumentationCommentId.CreateDeclarationId(method) is string name &&
78104
method.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
79105
{
80-
comments.Add((name, xml));
106+
comments.Add((NormalizeDocId(name), xml));
81107
}
82108
}
83109
return comments;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Net.Http;
5+
using System.Text.Json.Nodes;
6+
7+
namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
8+
9+
[UsesVerify]
10+
public class XmlCommentDocumentationIdTests
11+
{
12+
[Fact]
13+
public async Task CanMergeXmlCommentsWithDifferentDocumentationIdFormats()
14+
{
15+
// This test verifies that XML comments from referenced assemblies (without return type suffix)
16+
// are properly merged with in-memory symbols (with return type suffix)
17+
var source = """
18+
using System;
19+
using System.Threading.Tasks;
20+
using Microsoft.AspNetCore.Builder;
21+
using Microsoft.Extensions.DependencyInjection;
22+
using ReferencedLibrary;
23+
24+
var builder = WebApplication.CreateBuilder();
25+
26+
builder.Services.AddOpenApi();
27+
28+
var app = builder.Build();
29+
30+
app.MapPost("/test-method", ReferencedLibrary.TestApi.TestMethod);
31+
32+
app.Run();
33+
""";
34+
35+
var referencedLibrarySource = """
36+
using System;
37+
using System.Threading.Tasks;
38+
39+
namespace ReferencedLibrary;
40+
41+
public static class TestApi
42+
{
43+
/// <summary>
44+
/// This method should have its XML comment merged properly.
45+
/// </summary>
46+
/// <param name="id">The identifier for the test.</param>
47+
/// <returns>A task representing the asynchronous operation.</returns>
48+
public static Task TestMethod(int id)
49+
{
50+
return Task.CompletedTask;
51+
}
52+
}
53+
""";
54+
55+
var references = new Dictionary<string, List<string>>
56+
{
57+
{ "ReferencedLibrary", [referencedLibrarySource] }
58+
};
59+
60+
var generator = new XmlCommentGenerator();
61+
await SnapshotTestHelper.Verify(source, generator, references, out var compilation, out var additionalAssemblies);
62+
await SnapshotTestHelper.VerifyOpenApi(compilation, additionalAssemblies, document =>
63+
{
64+
var path = document.Paths["/test-method"].Operations[HttpMethod.Post];
65+
66+
// Verify that the XML comment from the referenced library was properly merged
67+
// This would fail before the fix because the documentation IDs didn't match
68+
Assert.NotNull(path.Summary);
69+
Assert.Equal("This method should have its XML comment merged properly.", path.Summary);
70+
71+
// Verify the parameter comment is also available
72+
Assert.NotNull(path.Parameters);
73+
Assert.Single(path.Parameters);
74+
Assert.Equal("The identifier for the test.", path.Parameters[0].Description);
75+
});
76+
}
77+
78+
[Theory]
79+
[InlineData("M:Sample.MyMethod(System.Int32)~System.Threading.Tasks.Task", "M:Sample.MyMethod(System.Int32)")]
80+
[InlineData("M:Sample.MyMethod(System.Int32)", "M:Sample.MyMethod(System.Int32)")]
81+
[InlineData("M:Sample.op_Implicit(System.Int32)~Sample.MyClass", "M:Sample.op_Implicit(System.Int32)~Sample.MyClass")]
82+
[InlineData("M:Sample.op_Explicit(System.Int32)~Sample.MyClass", "M:Sample.op_Explicit(System.Int32)~Sample.MyClass")]
83+
[InlineData("T:Sample.MyClass", "T:Sample.MyClass")]
84+
[InlineData("P:Sample.MyClass.MyProperty", "P:Sample.MyClass.MyProperty")]
85+
public void NormalizeDocId_ReturnsExpectedResult(string input, string expected)
86+
{
87+
var result = XmlCommentGenerator.NormalizeDocId(input);
88+
Assert.Equal(expected, result);
89+
}
90+
}

0 commit comments

Comments
 (0)