|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
using System.Threading;
namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
public sealed partial class XmlCommentGenerator : IIncrementalGenerator
{
public static string GeneratedCodeConstructor => $@"System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(XmlCommentGenerator).Assembly.FullName}"", ""{typeof(XmlCommentGenerator).Assembly.GetName().Version}"")";
public static string GeneratedCodeAttribute => $"[{GeneratedCodeConstructor}]";
internal static string GenerateXmlCommentSupportSource(string commentsFromXmlFile, string? commentsFromCompilation, ImmutableArray<(AddOpenApiInvocation Source, int Index, ImmutableArray<InterceptableLocation?> Elements)> groupedAddOpenApiInvocations) => $$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable
// Suppress warnings about obsolete types and members
// in generated code
#pragma warning disable CS0612, CS0618
namespace System.Runtime.CompilerServices
{
{{GeneratedCodeAttribute}}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : System.Attribute
{
public InterceptsLocationAttribute(int version, string data)
{
}
}
}
namespace Microsoft.AspNetCore.OpenApi.Generated
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.References;
using Microsoft.OpenApi.Any;
{{GeneratedCodeAttribute}}
file record XmlComment(
string? Summary,
string? Description,
string? Remarks,
string? Returns,
string? Value,
bool Deprecated,
List<string>? Examples,
List<XmlParameterComment>? Parameters,
List<XmlResponseComment>? Responses);
{{GeneratedCodeAttribute}}
file record XmlParameterComment(string? Name, string? Description, string? Example, bool Deprecated);
{{GeneratedCodeAttribute}}
file record XmlResponseComment(string Code, string? Description, string? Example);
{{GeneratedCodeAttribute}}
file static class XmlCommentCache
{
private static Dictionary<string, XmlComment>? _cache;
public static Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
private static Dictionary<string, XmlComment> GenerateCacheEntries()
{
var cache = new Dictionary<string, XmlComment>();
{{commentsFromXmlFile}}
{{commentsFromCompilation}}
return cache;
}
}
file static class DocumentationCommentIdHelper
{
/// <summary>
/// Generates a documentation comment ID for a type.
/// Example: T:Namespace.Outer+Inner`1 becomes T:Namespace.Outer.Inner`1
/// </summary>
public static string CreateDocumentationId(this Type type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
return "T:" + GetTypeDocId(type, includeGenericArguments: false, omitGenericArity: false);
}
/// <summary>
/// Generates a documentation comment ID for a property.
/// Example: P:Namespace.ContainingType.PropertyName or for an indexer P:Namespace.ContainingType.Item(System.Int32)
/// </summary>
public static string CreateDocumentationId(this PropertyInfo property)
{
if (property == null)
{
throw new ArgumentNullException(nameof(property));
}
var sb = new StringBuilder();
sb.Append("P:");
if (property.DeclaringType != null)
{
sb.Append(GetTypeDocId(property.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
}
sb.Append('.');
sb.Append(property.Name);
// For indexers, include the parameter list.
var indexParams = property.GetIndexParameters();
if (indexParams.Length > 0)
{
sb.Append('(');
for (int i = 0; i < indexParams.Length; i++)
{
if (i > 0)
{
sb.Append(',');
}
sb.Append(GetTypeDocId(indexParams[i].ParameterType, includeGenericArguments: true, omitGenericArity: false));
}
sb.Append(')');
}
return sb.ToString();
}
/// <summary>
/// Generates a documentation comment ID for a method (or constructor).
/// For example:
/// M:Namespace.ContainingType.MethodName(ParamType1,ParamType2)~ReturnType
/// M:Namespace.ContainingType.#ctor(ParamType)
/// </summary>
public static string CreateDocumentationId(this MethodInfo method)
{
if (method == null)
{
throw new ArgumentNullException(nameof(method));
}
var sb = new StringBuilder();
sb.Append("M:");
// Append the fully qualified name of the declaring type.
if (method.DeclaringType != null)
{
sb.Append(GetTypeDocId(method.DeclaringType, includeGenericArguments: false, omitGenericArity: false));
}
sb.Append('.');
// Append the method name, handling constructors specially.
if (method.IsConstructor)
{
sb.Append(method.IsStatic ? "#cctor" : "#ctor");
}
else
{
sb.Append(method.Name);
if (method.IsGenericMethod)
{
sb.Append("``");
sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", method.GetGenericArguments().Length);
}
}
// Append the parameter list, if any.
var parameters = method.GetParameters();
if (parameters.Length > 0)
{
sb.Append('(');
for (int i = 0; i < parameters.Length; i++)
{
if (i > 0)
{
sb.Append(',');
}
// Omit the generic arity for the parameter type.
sb.Append(GetTypeDocId(parameters[i].ParameterType, includeGenericArguments: true, omitGenericArity: true));
}
sb.Append(')');
}
// Append the return type after a '~' (if the method returns a value).
if (method.ReturnType != typeof(void))
{
sb.Append('~');
// Omit the generic arity for the return type.
sb.Append(GetTypeDocId(method.ReturnType, includeGenericArguments: true, omitGenericArity: true));
}
return sb.ToString();
}
/// <summary>
/// Generates a documentation ID string for a type.
/// This method handles nested types (replacing '+' with '.'),
/// generic types, arrays, pointers, by-ref types, and generic parameters.
/// The <paramref name="includeGenericArguments"/> flag controls whether
/// constructed generic type arguments are emitted, while <paramref name="omitGenericArity"/>
/// controls whether the generic arity marker (e.g. "`1") is appended.
/// </summary>
private static string GetTypeDocId(Type type, bool includeGenericArguments, bool omitGenericArity)
{
if (type.IsGenericParameter)
{
// Use `` for method-level generic parameters and ` for type-level.
if (type.DeclaringMethod != null)
{
return "``" + type.GenericParameterPosition;
}
else if (type.DeclaringType != null)
{
return "`" + type.GenericParameterPosition;
}
else
{
return type.Name;
}
}
if (type.IsGenericType)
{
Type genericDef = type.GetGenericTypeDefinition();
string fullName = genericDef.FullName ?? genericDef.Name;
var sb = new StringBuilder(fullName.Length);
// Replace '+' with '.' for nested types
for (var i = 0; i < fullName.Length; i++)
{
char c = fullName[i];
if (c == '+')
{
sb.Append('.');
}
else if (c == '`')
{
break;
}
else
{
sb.Append(c);
}
}
if (!omitGenericArity)
{
int arity = genericDef.GetGenericArguments().Length;
sb.Append('`');
sb.AppendFormat(CultureInfo.InvariantCulture, "{0}", arity);
}
if (includeGenericArguments && !type.IsGenericTypeDefinition)
{
var typeArgs = type.GetGenericArguments();
sb.Append('{');
for (int i = 0; i < typeArgs.Length; i++)
{
if (i > 0)
{
sb.Append(',');
}
sb.Append(GetTypeDocId(typeArgs[i], includeGenericArguments, omitGenericArity));
}
sb.Append('}');
}
return sb.ToString();
}
// For non-generic types, use FullName (if available) and replace nested type separators.
return (type.FullName ?? type.Name).Replace('+', '.');
}
}
{{GeneratedCodeAttribute}}
file class XmlCommentOperationTransformer : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
var methodInfo = context.Description.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
? controllerActionDescriptor.MethodInfo
: context.Description.ActionDescriptor.EndpointMetadata.OfType<MethodInfo>().SingleOrDefault();
if (methodInfo is null)
{
return Task.CompletedTask;
}
if (XmlCommentCache.Cache.TryGetValue(methodInfo.CreateDocumentationId(), out var methodComment))
{
if (methodComment.Summary is { } summary)
{
operation.Summary = summary;
}
if (methodComment.Description is { } description)
{
operation.Description = description;
}
if (methodComment.Remarks is { } remarks)
{
operation.Description = remarks;
}
if (methodComment.Parameters is { Count: > 0})
{
foreach (var parameterComment in methodComment.Parameters)
{
var parameterInfo = methodInfo.GetParameters().SingleOrDefault(info => info.Name == parameterComment.Name);
var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
if (operationParameter is not null)
{
var targetOperationParameter = operationParameter is OpenApiParameterReference reference
? reference.Target
: (OpenApiParameter)operationParameter;
targetOperationParameter.Description = parameterComment.Description;
if (parameterComment.Example is { } jsonString)
{
targetOperationParameter.Example = jsonString.Parse();
}
targetOperationParameter.Deprecated = parameterComment.Deprecated;
}
else
{
var requestBody = operation.RequestBody;
if (requestBody is not null)
{
requestBody.Description = parameterComment.Description;
if (parameterComment.Example is { } jsonString)
{
foreach (var mediaType in requestBody.Content.Values)
{
mediaType.Example = jsonString.Parse();
}
}
}
}
}
}
if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
{
foreach (var response in operation.Responses)
{
var responseComment = methodComment.Responses.SingleOrDefault(xmlResponse => xmlResponse.Code == response.Key);
if (responseComment is not null)
{
response.Value.Description = responseComment.Description;
}
}
}
}
return Task.CompletedTask;
}
}
{{GeneratedCodeAttribute}}
file class XmlCommentSchemaTransformer : IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context.JsonPropertyInfo is { AttributeProvider: PropertyInfo propertyInfo })
{
if (XmlCommentCache.Cache.TryGetValue(propertyInfo.CreateDocumentationId(), out var propertyComment))
{
schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
}
if (XmlCommentCache.Cache.TryGetValue(context.JsonTypeInfo.Type.CreateDocumentationId(), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples?.FirstOrDefault() is { } jsonString)
{
schema.Example = jsonString.Parse();
}
}
return Task.CompletedTask;
}
}
file static class JsonNodeExtensions
{
public static JsonNode? Parse(this string? json)
{
if (json is null)
{
return null;
}
try
{
return JsonNode.Parse(json);
}
catch (JsonException)
{
try
{
// If parsing fails, try wrapping in quotes to make it a valid JSON string
return JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
}
catch (JsonException)
{
return null;
}
}
}
}
{{GeneratedCodeAttribute}}
file static class GeneratedServiceCollectionExtensions
{
{{GenerateAddOpenApiInterceptions(groupedAddOpenApiInvocations)}}
}
}
""";
internal static string GetAddOpenApiInterceptor(AddOpenApiOverloadVariant overloadVariant) => overloadVariant switch
{
AddOpenApiOverloadVariant.AddOpenApi => """
public static IServiceCollection AddOpenApi(this IServiceCollection services)
{
return services.AddOpenApi("v1", options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
});
}
""",
AddOpenApiOverloadVariant.AddOpenApiDocumentName => """
public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName)
{
return services.AddOpenApi(documentName, options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
});
}
""",
AddOpenApiOverloadVariant.AddOpenApiConfigureOptions => """
public static IServiceCollection AddOpenApi(this IServiceCollection services, Action<OpenApiOptions> configureOptions)
{
return services.AddOpenApi("v1", options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
configureOptions(options);
});
}
""",
AddOpenApiOverloadVariant.AddOpenApiDocumentNameConfigureOptions => """
public static IServiceCollection AddOpenApi(this IServiceCollection services, string documentName, Action<OpenApiOptions> configureOptions)
{
// This overload is not intercepted.
return OpenApiServiceCollectionExtensions.AddOpenApi(services, documentName, options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
configureOptions(options);
});
}
""",
_ => string.Empty // Effectively no-op for AddOpenApi invocations that do not conform to a variant
};
internal static string GenerateAddOpenApiInterceptions(ImmutableArray<(AddOpenApiInvocation Source, int Index, ImmutableArray<InterceptableLocation?> Elements)> groupedAddOpenApiInvocations)
{
var writer = new StringWriter();
var codeWriter = new CodeWriter(writer, baseIndent: 2);
foreach (var (source, _, locations) in groupedAddOpenApiInvocations)
{
foreach (var location in locations)
{
if (location is not null)
{
codeWriter.WriteLine(location.GetInterceptsLocationAttributeSyntax());
}
}
codeWriter.WriteLine(GetAddOpenApiInterceptor(source.Variant));
}
return writer.ToString();
}
internal static string EmitCommentsCache(IEnumerable<(string MemberKey, XmlComment? Comment)> comments, CancellationToken cancellationToken)
{
var writer = new StringWriter();
var codeWriter = new CodeWriter(writer, baseIndent: 3);
foreach (var (memberKey, comment) in comments)
{
if (comment is not null)
{
codeWriter.WriteLine($"cache.Add({FormatStringForCode(memberKey)}, {EmitSourceGeneratedXmlComment(comment)});");
}
}
return writer.ToString();
}
private static string FormatStringForCode(string? input)
{
if (input == null)
{
return "null";
}
var formatted = input
.Replace("\"", "\"\""); // Escape double quotes
return $"@\"{formatted}\"";
}
internal static string EmitSourceGeneratedXmlComment(XmlComment comment)
{
var writer = new StringWriter();
var codeWriter = new CodeWriter(writer, baseIndent: 0);
codeWriter.Write($"new XmlComment(");
codeWriter.Write(FormatStringForCode(comment.Summary) + ", ");
codeWriter.Write(FormatStringForCode(comment.Description) + ", ");
codeWriter.Write(FormatStringForCode(comment.Remarks) + ", ");
codeWriter.Write(FormatStringForCode(comment.Returns) + ", ");
codeWriter.Write(FormatStringForCode(comment.Value) + ", ");
codeWriter.Write(comment.Deprecated == true ? "true" : "false" + ", ");
if (comment.Examples is null || comment.Examples.Count == 0)
{
codeWriter.Write("null, ");
}
else
{
codeWriter.Write("[");
for (var i = 0; i < comment.Examples.Count; i++)
{
var example = comment.Examples[i];
codeWriter.Write(FormatStringForCode(example));
if (i < comment.Examples.Count - 1)
{
codeWriter.Write(", ");
}
}
codeWriter.Write("], ");
}
if (comment.Parameters is null || comment.Parameters.Count == 0)
{
codeWriter.Write("null, ");
}
else
{
codeWriter.Write("[");
for (var i = 0; i < comment.Parameters.Count; i++)
{
var parameter = comment.Parameters[i];
var exampleLiteral = string.IsNullOrEmpty(parameter.Example)
? "null"
: FormatStringForCode(parameter.Example!);
codeWriter.Write("new XmlParameterComment(");
codeWriter.Write(FormatStringForCode(parameter.Name) + ", ");
codeWriter.Write(FormatStringForCode(parameter.Description) + ", ");
codeWriter.Write(exampleLiteral + ", ");
codeWriter.Write(parameter.Deprecated == true ? "true" : "false");
codeWriter.Write(")");
if (i < comment.Parameters.Count - 1)
{
codeWriter.Write(", ");
}
}
codeWriter.Write("], ");
}
if (comment.Responses is null || comment.Responses.Count == 0)
{
codeWriter.Write("null");
}
else
{
codeWriter.Write("[");
for (var i = 0; i < comment.Responses.Count; i++)
{
var response = comment.Responses[i];
codeWriter.Write("new XmlResponseComment(");
codeWriter.Write(FormatStringForCode(response.Code) + ", ");
codeWriter.Write(FormatStringForCode(response.Description) + ", ");
codeWriter.Write(response.Example is null ? "null)" : FormatStringForCode(response.Example) + ")");
if (i < comment.Responses.Count - 1)
{
codeWriter.Write(", ");
}
}
codeWriter.Write("]");
}
codeWriter.Write(")");
return writer.ToString();
}
internal static void Emit(SourceProductionContext context,
string commentsFromXmlFile,
string commentsFromCompilation,
ImmutableArray<(AddOpenApiInvocation Source, int Index, ImmutableArray<InterceptableLocation?> Elements)> groupedAddOpenApiInvocations)
{
context.AddSource("OpenApiXmlCommentSupport.generated.cs", GenerateXmlCommentSupportSource(commentsFromXmlFile, commentsFromCompilation, groupedAddOpenApiInvocations));
}
}
|