// 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}}
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : global::System.Attribute
{
public InterceptsLocationAttribute(int version, string data)
{
}
}
}
namespace Microsoft.AspNetCore.OpenApi.Generated
{
{{GeneratedCodeAttribute}}
file record XmlComment(
string? Summary,
string? Description,
string? Remarks,
string? Returns,
string? Value,
bool Deprecated,
global::System.Collections.Generic.List<string>? Examples,
global::System.Collections.Generic.List<XmlParameterComment>? Parameters,
global::System.Collections.Generic.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 global::System.Collections.Generic.Dictionary<string, XmlComment>? _cache;
public static global::System.Collections.Generic.Dictionary<string, XmlComment> Cache => _cache ??= GenerateCacheEntries();
private static global::System.Collections.Generic.Dictionary<string, XmlComment> GenerateCacheEntries()
{
var cache = new global::System.Collections.Generic.Dictionary<string, XmlComment>();
{{commentsFromXmlFile}}
{{commentsFromCompilation}}
return cache;
}
}
{{GeneratedCodeAttribute}}
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 global::System.Type type)
{
if (type == null)
{
throw new global::System.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 global::System.Reflection.PropertyInfo property)
{
if (property == null)
{
throw new global::System.ArgumentNullException(nameof(property));
}
var sb = new global::System.Text.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 global::System.Reflection.MethodInfo method)
{
if (method == null)
{
throw new global::System.ArgumentNullException(nameof(method));
}
var sb = new global::System.Text.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(global::System.Globalization.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(global::System.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)
{
global::System.Type genericDef = type.GetGenericTypeDefinition();
string fullName = genericDef.FullName ?? genericDef.Name;
var sb = new global::System.Text.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(global::System.Globalization.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('+', '.');
}
/// <summary>
/// Normalizes a documentation comment ID to match the compiler-style format.
/// Strips the return type suffix for ordinary methods but retains it for conversion operators.
/// </summary>
/// <param name="docId">The documentation comment ID to normalize.</param>
/// <returns>The normalized documentation comment ID.</returns>
public static string NormalizeDocId(string docId)
{
// Find the tilde character that indicates the return type suffix
var tildeIndex = docId.IndexOf('~');
if (tildeIndex == -1)
{
// No return type suffix, return as-is
return docId;
}
// Check if this is a conversion operator (op_Implicit or op_Explicit)
// For these operators, we need to keep the return type suffix
if (docId.Contains("op_Implicit") || docId.Contains("op_Explicit"))
{
return docId;
}
// For ordinary methods, strip the return type suffix
return docId.Substring(0, tildeIndex);
}
}
{{GeneratedCodeAttribute}}
file class XmlCommentOperationTransformer : global::Microsoft.AspNetCore.OpenApi.IOpenApiOperationTransformer
{
public global::System.Threading.Tasks.Task TransformAsync(global::Microsoft.OpenApi.OpenApiOperation operation, global::Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext context, global::System.Threading.CancellationToken cancellationToken)
{
var methodInfo = context.Description.ActionDescriptor is global::Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor controllerActionDescriptor
? controllerActionDescriptor.MethodInfo
: global::System.Linq.Enumerable.SingleOrDefault(global::System.Linq.Enumerable.OfType<global::System.Reflection.MethodInfo>(context.Description.ActionDescriptor.EndpointMetadata));
if (methodInfo is null)
{
return global::System.Threading.Tasks.Task.CompletedTask;
}
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(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 = global::System.Linq.Enumerable.SingleOrDefault(methodInfo.GetParameters(), info => info.Name == parameterComment.Name);
var modelName = GetModelName(parameterInfo, parameterComment.Name);
var operationParameter = GetOperationParameter(operation, modelName);
if (operationParameter is not null)
{
var targetOperationParameter = UnwrapOpenApiParameter(operationParameter);
targetOperationParameter.Description = parameterComment.Description;
if (parameterComment.Example is { } jsonString)
{
targetOperationParameter.Example = jsonString.Parse();
}
targetOperationParameter.Deprecated = parameterComment.Deprecated;
}
// Only fall back to the request body when the parameter is actually bound to it.
// This avoids applying documentation for parameters that aren't part of the
// OpenAPI surface (e.g. a `CancellationToken`) to the request body.
else if (IsRequestBodyParameter(context, modelName))
{
var requestBody = operation.RequestBody;
if (requestBody is not null)
{
requestBody.Description = parameterComment.Description;
if (parameterComment.Example is { } jsonString)
{
var content = requestBody?.Content?.Values;
if (content is null)
{
continue;
}
foreach (var mediaType in global::System.Linq.Enumerable.OfType<global::Microsoft.OpenApi.OpenApiMediaType>(content))
{
mediaType.Example = jsonString.Parse();
}
}
}
}
}
}
// Applies `<returns>` on XML comments for operation with single response value.
if (methodComment.Returns is { } returns && operation.Responses is { Count: 1 })
{
var response = global::System.Linq.Enumerable.First(operation.Responses);
response.Value.Description = returns;
}
// Applies `<response>` on XML comments for operation with multiple response values.
if (methodComment.Responses is { Count: > 0} && operation.Responses is { Count: > 0 })
{
foreach (var response in operation.Responses)
{
var responseComment = global::System.Linq.Enumerable.SingleOrDefault(methodComment.Responses, xmlResponse => xmlResponse.Code == response.Key);
if (responseComment is not null)
{
response.Value.Description = responseComment.Description;
}
}
}
}
foreach (var parameterDescription in context.Description.ParameterDescriptions)
{
var metadata = parameterDescription.ModelMetadata;
if (metadata is not null
&& metadata.MetadataKind == global::Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataKind.Property
&& metadata.ContainerType is { } containerType
&& metadata.PropertyName is { } propertyName)
{
var propertyInfo = GetPropertyInfo(containerType, propertyName);
if (propertyInfo is null)
{
continue;
}
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
var modelName = GetModelName(propertyInfo, propertyInfo.Name);
var parameter = GetOperationParameter(operation, modelName);
var description = propertyComment.Summary;
if (!string.IsNullOrEmpty(description) && !string.IsNullOrEmpty(propertyComment.Value))
{
description = $"{description}\n{propertyComment.Value}";
}
else if (string.IsNullOrEmpty(description))
{
description = propertyComment.Value;
}
if (parameter is null)
{
// Only fall back to the request body when the property is actually bound to it.
if (IsRequestBodyParameter(parameterDescription.Source) && operation.RequestBody is not null)
{
operation.RequestBody.Description = description;
if (propertyComment.Examples is { } examples && global::System.Linq.Enumerable.FirstOrDefault(examples) is { } jsonString)
{
var content = operation.RequestBody.Content?.Values;
if (content is null)
{
continue;
}
var parsedExample = jsonString.Parse();
foreach (var mediaType in global::System.Linq.Enumerable.OfType<global::Microsoft.OpenApi.OpenApiMediaType>(content))
{
mediaType.Example = parsedExample;
}
}
}
continue;
}
var targetOperationParameter = UnwrapOpenApiParameter(parameter);
if (targetOperationParameter is not null)
{
targetOperationParameter.Description = description;
if (propertyComment.Examples is { } examples && global::System.Linq.Enumerable.FirstOrDefault(examples) is { } jsonString)
{
targetOperationParameter.Example = jsonString.Parse();
}
}
}
}
}
return global::System.Threading.Tasks.Task.CompletedTask;
}
private static global::Microsoft.OpenApi.IOpenApiParameter? GetOperationParameter(global::Microsoft.OpenApi.OpenApiOperation operation, string? modelName)
{
if (string.IsNullOrEmpty(modelName))
{
return null;
}
var parameters = operation.Parameters;
if (parameters is null || parameters.Count == 0)
{
return null;
}
foreach (var parameter in parameters)
{
if (string.Equals(parameter.Name, modelName, global::System.StringComparison.Ordinal))
{
return parameter;
}
}
return null;
}
private static bool IsRequestBodyParameter(global::Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext context, string? modelName)
{
if (string.IsNullOrEmpty(modelName))
{
return false;
}
foreach (var parameterDescription in context.Description.ParameterDescriptions)
{
if (IsRequestBodyParameter(parameterDescription.Source)
&& string.Equals(parameterDescription.Name, modelName, global::System.StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool IsRequestBodyParameter(global::Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource? source)
=> source == global::Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Body
|| source == global::Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.Form
|| source == global::Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource.FormFile;
[global::System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Properties are looked up on the container type from ModelMetadata.ContainerType, which is not statically annotated for trimming. If the property has been trimmed, GetProperty returns null and the XML documentation is simply not applied.")]
[global::System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Properties are looked up on the container type from ModelMetadata.ContainerType, which is not statically annotated for trimming. If the property has been trimmed, GetProperty returns null and the XML documentation is simply not applied.")]
private static global::System.Reflection.PropertyInfo? GetPropertyInfo(global::System.Type containerType, string propertyName)
{
// Walk the type hierarchy explicitly with `DeclaredOnly` so that a property shadowing a
// base member (via `new`) doesn't throw an `AmbiguousMatchException`, while still
// resolving properties that are inherited from a base type.
for (var type = containerType; type is not null; type = type.BaseType)
{
var propertyInfo = type.GetProperty(propertyName, global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.DeclaredOnly);
if (propertyInfo is not null)
{
return propertyInfo;
}
}
return null;
}
private static string? GetModelName(global::System.Reflection.ICustomAttributeProvider? attributeProvider, string? name)
{
if (attributeProvider is not null)
{
foreach (var attribute in attributeProvider.GetCustomAttributes(inherit: true))
{
if (attribute is global::Microsoft.AspNetCore.Mvc.ModelBinding.IModelNameProvider modelNameProvider && !string.IsNullOrEmpty(modelNameProvider.Name))
{
return modelNameProvider.Name;
}
}
}
return name;
}
private static global::Microsoft.OpenApi.OpenApiParameter UnwrapOpenApiParameter(global::Microsoft.OpenApi.IOpenApiParameter sourceParameter)
{
if (sourceParameter is global::Microsoft.OpenApi.OpenApiParameterReference parameterReference)
{
if (parameterReference.Target is global::Microsoft.OpenApi.OpenApiParameter target)
{
return target;
}
else
{
throw new global::System.InvalidOperationException($"The input schema must be an {nameof(global::Microsoft.OpenApi.OpenApiParameter)} or {nameof(global::Microsoft.OpenApi.OpenApiParameterReference)}.");
}
}
else if (sourceParameter is global::Microsoft.OpenApi.OpenApiParameter directParameter)
{
return directParameter;
}
else
{
throw new global::System.InvalidOperationException($"The input schema must be an {nameof(global::Microsoft.OpenApi.OpenApiParameter)} or {nameof(global::Microsoft.OpenApi.OpenApiParameterReference)}.");
}
}
}
{{GeneratedCodeAttribute}}
file class XmlCommentSchemaTransformer : global::Microsoft.AspNetCore.OpenApi.IOpenApiSchemaTransformer
{
public global::System.Threading.Tasks.Task TransformAsync(global::Microsoft.OpenApi.OpenApiSchema schema, global::Microsoft.AspNetCore.OpenApi.OpenApiSchemaTransformerContext context, global::System.Threading.CancellationToken cancellationToken)
{
// Apply comments from the type
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(context.JsonTypeInfo.Type.CreateDocumentationId()), out var typeComment))
{
schema.Description = typeComment.Summary;
if (typeComment.Examples is { } examples && global::System.Linq.Enumerable.FirstOrDefault(examples) is { } jsonString)
{
schema.Examples = [jsonString.Parse()!];
}
}
if (context.JsonPropertyInfo is { AttributeProvider: global::System.Reflection.PropertyInfo propertyInfo })
{
// Apply comments from the property
if (XmlCommentCache.Cache.TryGetValue(DocumentationCommentIdHelper.NormalizeDocId(propertyInfo.CreateDocumentationId()), out var propertyComment))
{
var description = propertyComment.Summary;
if (!string.IsNullOrEmpty(description) && !string.IsNullOrEmpty(propertyComment.Value))
{
description = $"{description}\n{propertyComment.Value}";
}
else if (string.IsNullOrEmpty(description))
{
description = propertyComment.Value;
}
if (schema.Metadata is null
|| !schema.Metadata.TryGetValue("x-schema-id", out var schemaId)
|| string.IsNullOrEmpty(schemaId as string))
{
// Inlined schema
schema.Description = description;
if (propertyComment.Examples is { } examples && global::System.Linq.Enumerable.FirstOrDefault(examples) is { } jsonString)
{
schema.Examples = [jsonString.Parse()!];
}
}
else
{
// Schema Reference
if (!string.IsNullOrEmpty(description))
{
schema.Metadata["x-ref-description"] = description;
}
if (propertyComment.Examples is { } examples && global::System.Linq.Enumerable.FirstOrDefault(examples) is { } jsonString)
{
schema.Metadata["x-ref-example"] = jsonString.Parse()!;
}
}
}
}
return global::System.Threading.Tasks.Task.CompletedTask;
}
}
{{GeneratedCodeAttribute}}
file static class JsonNodeExtensions
{
public static global::System.Text.Json.Nodes.JsonNode? Parse(this string? json)
{
if (json is null)
{
return null;
}
try
{
return global::System.Text.Json.Nodes.JsonNode.Parse(json);
}
catch (global::System.Text.Json.JsonException)
{
try
{
// If parsing fails, try wrapping in quotes to make it a valid JSON string
return global::System.Text.Json.Nodes.JsonNode.Parse($"\"{json.Replace("\"", "\\\"")}\"");
}
catch (global::System.Text.Json.JsonException)
{
return null;
}
}
}
}
{{GeneratedCodeAttribute}}
file static class GeneratedServiceCollectionExtensions
{
{{GenerateAddOpenApiInterceptions(groupedAddOpenApiInvocations)}}
}
}
""";
internal static string GetAddOpenApiInterceptor(AddOpenApiOverloadVariant overloadVariant) => overloadVariant switch
{
AddOpenApiOverloadVariant.AddOpenApi => """
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddOpenApi(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services)
{
return global::Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi(services, "v1", options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
});
}
""",
AddOpenApiOverloadVariant.AddOpenApiDocumentName => """
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddOpenApi(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, string documentName)
{
return global::Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi(services, documentName, options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
});
}
""",
AddOpenApiOverloadVariant.AddOpenApiConfigureOptions => """
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddOpenApi(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action<global::Microsoft.AspNetCore.OpenApi.OpenApiOptions> configureOptions)
{
return global::Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi(services, "v1", options =>
{
options.AddSchemaTransformer(new XmlCommentSchemaTransformer());
options.AddOperationTransformer(new XmlCommentOperationTransformer());
configureOptions(options);
});
}
""",
AddOpenApiOverloadVariant.AddOpenApiDocumentNameConfigureOptions => """
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddOpenApi(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, string documentName, global::System.Action<global::Microsoft.AspNetCore.OpenApi.OpenApiOptions> configureOptions)
{
// This overload is not intercepted.
return global::Microsoft.Extensions.DependencyInjection.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));
}
}