File: XmlCommentGenerator.Emitter.cs
Web Access
Project: src\src\OpenApi\gen\Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj (Microsoft.AspNetCore.OpenApi.SourceGenerators)
// 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));
    }
}