GetBaseTypes(this ITypeSymbol? type)
+ {
+ var current = type?.BaseType;
+ while (current != null)
+ {
+ yield return current;
+ current = current.BaseType;
+ }
+ }
+}
diff --git a/src/OpenApi/gen/Helpers/StringExtensions.cs b/src/OpenApi/gen/Helpers/StringExtensions.cs
new file mode 100644
index 000000000000..ee4a81284295
--- /dev/null
+++ b/src/OpenApi/gen/Helpers/StringExtensions.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+
+///
+/// Extension methods for string manipulation.
+///
+public static class StringExtensions
+{
+ ///
+ /// Trims whitespace from each line of text while preserving relative indentation.
+ ///
+ /// The text to trim.
+ /// Optional indentation to apply.
+ /// The trimmed text with preserved indentation structure.
+ public static string TrimEachLine(this string text, string indent = "")
+ {
+ var minLeadingWhitespace = int.MaxValue;
+ var lines = text.ReadLines().ToList();
+ foreach (var line in lines)
+ {
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ var leadingWhitespace = 0;
+ while (leadingWhitespace < line.Length && char.IsWhiteSpace(line[leadingWhitespace]))
+ {
+ leadingWhitespace++;
+ }
+
+ minLeadingWhitespace = Math.Min(minLeadingWhitespace, leadingWhitespace);
+ }
+
+ var builder = new StringBuilder();
+
+ // Trim leading empty lines
+ var trimStart = true;
+
+ // Apply indentation to all lines except the first,
+ // since the first new line in is significant
+ var firstLine = true;
+
+ foreach (var line in lines)
+ {
+ if (trimStart && string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ if (firstLine)
+ {
+ firstLine = false;
+ }
+ else
+ {
+ builder.Append(indent);
+ }
+
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ builder.AppendLine();
+ continue;
+ }
+
+ trimStart = false;
+ builder.AppendLine(line.Substring(minLeadingWhitespace));
+ }
+
+ return builder.ToString().TrimEnd();
+ }
+
+ public static IEnumerable ReadLines(this string text)
+ {
+ string line;
+ using var sr = new StringReader(text);
+ while ((line = sr.ReadLine()) != null)
+ {
+ yield return line;
+ }
+ }
+}
diff --git a/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj b/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj
new file mode 100644
index 000000000000..8ca8ad11dd21
--- /dev/null
+++ b/src/OpenApi/gen/Microsoft.AspNetCore.OpenApi.SourceGenerators.csproj
@@ -0,0 +1,32 @@
+
+
+
+ netstandard2.0
+ true
+ true
+ true
+ false
+ enable
+ true
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
new file mode 100644
index 000000000000..f91854fa7825
--- /dev/null
+++ b/src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
@@ -0,0 +1,549 @@
+// 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;
+using System.Linq;
+
+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 Elements)> groupedAddOpenApiInvocations) => $$"""
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+
+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.Linq;
+ using System.Reflection;
+ 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.Any;
+
+ {{GeneratedCodeAttribute}}
+ file record XmlComment(
+ string? Summary,
+ string? Description,
+ string? Remarks,
+ string? Returns,
+ string? Value,
+ bool Deprecated,
+ List? Examples,
+ List? Parameters,
+ List? 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 sealed record MemberKey(
+ Type? DeclaringType,
+ MemberType MemberKind,
+ string? Name,
+ Type? ReturnType,
+ Type[]? Parameters) : IEquatable
+ {
+ public bool Equals(MemberKey? other)
+ {
+ if (other is null) return false;
+
+ // Check member kind
+ if (MemberKind != other.MemberKind) return false;
+
+ // Check declaring type, handling generic types
+ if (!TypesEqual(DeclaringType, other.DeclaringType)) return false;
+
+ // Check name
+ if (Name != other.Name) return false;
+
+ // For methods, check return type and parameters
+ if (MemberKind == MemberType.Method)
+ {
+ if (!TypesEqual(ReturnType, other.ReturnType)) return false;
+ if (Parameters is null || other.Parameters is null) return false;
+ if (Parameters.Length != other.Parameters.Length) return false;
+
+ for (int i = 0; i < Parameters.Length; i++)
+ {
+ if (!TypesEqual(Parameters[i], other.Parameters[i])) return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool TypesEqual(Type? type1, Type? type2)
+ {
+ if (type1 == type2) return true;
+ if (type1 == null || type2 == null) return false;
+
+ if (type1.IsGenericType && type2.IsGenericType)
+ {
+ return type1.GetGenericTypeDefinition() == type2.GetGenericTypeDefinition();
+ }
+
+ return type1 == type2;
+ }
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(GetTypeHashCode(DeclaringType));
+ hash.Add(MemberKind);
+ hash.Add(Name);
+
+ if (MemberKind == MemberType.Method)
+ {
+ hash.Add(GetTypeHashCode(ReturnType));
+ if (Parameters is not null)
+ {
+ foreach (var param in Parameters)
+ {
+ hash.Add(GetTypeHashCode(param));
+ }
+ }
+ }
+
+ return hash.ToHashCode();
+ }
+
+ private static int GetTypeHashCode(Type? type)
+ {
+ if (type == null) return 0;
+ return type.IsGenericType ? type.GetGenericTypeDefinition().GetHashCode() : type.GetHashCode();
+ }
+
+ public static MemberKey FromMethodInfo(MethodInfo method)
+ {
+ return new MemberKey(
+ method.DeclaringType,
+ MemberType.Method,
+ method.Name,
+ method.ReturnType.IsGenericParameter ? typeof(object) : method.ReturnType,
+ method.GetParameters().Select(p => p.ParameterType.IsGenericParameter ? typeof(object) : p.ParameterType).ToArray());
+ }
+
+ public static MemberKey FromPropertyInfo(PropertyInfo property)
+ {
+ return new MemberKey(
+ property.DeclaringType,
+ MemberType.Property,
+ property.Name,
+ null,
+ null);
+ }
+
+ public static MemberKey FromTypeInfo(Type type)
+ {
+ return new MemberKey(
+ type,
+ MemberType.Type,
+ null,
+ null,
+ null);
+ }
+ }
+
+ file enum MemberType
+ {
+ Type,
+ Property,
+ Method
+ }
+
+ {{GeneratedCodeAttribute}}
+ file static class XmlCommentCache
+ {
+ private static Dictionary? _cache;
+ public static Dictionary Cache => _cache ??= GenerateCacheEntries();
+
+ private static Dictionary GenerateCacheEntries()
+ {
+ var _cache = new Dictionary();
+{{commentsFromXmlFile}}
+{{commentsFromCompilation}}
+ return _cache;
+ }
+
+ internal static bool TryGetXmlComment(Type? type, MethodInfo? methodInfo, [NotNullWhen(true)] out XmlComment? xmlComment)
+ {
+ if (methodInfo is null)
+ {
+ return Cache.TryGetValue(new MemberKey(type, MemberType.Property, null, null, null), out xmlComment);
+ }
+
+ return Cache.TryGetValue(MemberKey.FromMethodInfo(methodInfo), out xmlComment);
+ }
+
+ internal static bool TryGetXmlComment(Type? type, string? memberName, [NotNullWhen(true)] out XmlComment? xmlComment)
+ {
+ return Cache.TryGetValue(new MemberKey(type, memberName is null ? MemberType.Type : MemberType.Property, memberName, null, null), out xmlComment);
+ }
+ }
+
+ {{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().SingleOrDefault();
+
+ if (methodInfo is null)
+ {
+ return Task.CompletedTask;
+ }
+ if (XmlCommentCache.TryGetXmlComment(methodInfo.DeclaringType, methodInfo, 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)
+ {
+ operationParameter.Description = parameterComment.Description;
+ if (parameterComment.Example is { } jsonString)
+ {
+ operationParameter.Example = jsonString.Parse();
+ }
+ operationParameter.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.TryGetXmlComment(propertyInfo.DeclaringType, propertyInfo.Name, out var propertyComment))
+ {
+ schema.Description = propertyComment.Value ?? propertyComment.Returns ?? propertyComment.Summary;
+ if (propertyComment.Examples?.FirstOrDefault() is { } jsonString)
+ {
+ schema.Example = jsonString.Parse();
+ }
+ }
+ }
+ if (XmlCommentCache.TryGetXmlComment(context.JsonTypeInfo.Type, (string?)null, 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 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 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 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<(MemberKey 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(new MemberKey(" +
+ $"{FormatLiteralOrNull(memberKey.DeclaringType)}, " +
+ $"MemberType.{memberKey.MemberKind}, " +
+ $"{FormatLiteralOrNull(memberKey.Name, true)}, " +
+ $"{FormatLiteralOrNull(memberKey.ReturnType)}, " +
+ $"[{(memberKey.Parameters != null ? string.Join(", ", memberKey.Parameters.Select(p => SymbolDisplay.FormatLiteral(p, false))) : "")}]), " +
+ $"{EmitSourceGeneratedXmlComment(comment)});");
+ }
+ }
+ return writer.ToString();
+
+ static string FormatLiteralOrNull(string? input, bool quote = false)
+ {
+ return input == null ? "null" : SymbolDisplay.FormatLiteral(input, quote);
+ }
+ }
+
+ 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 (int 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 (int 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(@\"{parameter.Name}\", @\"{parameter.Description}\", {exampleLiteral}, {(parameter.Deprecated == true ? "true" : "false")})");
+ 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 (int i = 0; i < comment.Responses.Count; i++)
+ {
+ var response = comment.Responses[i];
+ codeWriter.Write($"new XmlResponseComment(@\"{response.Code}\", @\"{response.Description}\", {(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 Elements)> groupedAddOpenApiInvocations)
+ {
+ context.AddSource("OpenApiXmlCommentSupport.generated.cs", GenerateXmlCommentSupportSource(commentsFromXmlFile, commentsFromCompilation, groupedAddOpenApiInvocations));
+ }
+}
diff --git a/src/OpenApi/gen/XmlCommentGenerator.Parser.cs b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
new file mode 100644
index 000000000000..fad7e9318e45
--- /dev/null
+++ b/src/OpenApi/gen/XmlCommentGenerator.Parser.cs
@@ -0,0 +1,163 @@
+// 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.Globalization;
+using System.Threading;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
+
+public sealed partial class XmlCommentGenerator
+{
+ internal static List<(string, string)> ParseXmlFile(AdditionalText additionalText, CancellationToken cancellationToken)
+ {
+ var text = additionalText.GetText(cancellationToken);
+ if (text is null)
+ {
+ return [];
+ }
+ XElement xml;
+ try
+ {
+ xml = XElement.Parse(text.ToString());
+ }
+ catch
+ {
+ return [];
+ }
+ var members = xml.Descendants("member");
+ var comments = new List<(string, string)>();
+ foreach (var member in members)
+ {
+ var name = member.Attribute(DocumentationCommentXmlNames.NameAttributeName)?.Value;
+ if (name is not null)
+ {
+ comments.Add((name, member.ToString()));
+ }
+ }
+ return comments;
+ }
+
+ internal static List<(string, string)> ParseCompilation(Compilation compilation, CancellationToken cancellationToken)
+ {
+ var visitor = new AssemblyTypeSymbolsVisitor(compilation.Assembly, cancellationToken);
+ visitor.VisitAssembly();
+ var types = visitor.GetPublicTypes();
+ var comments = new List<(string, string)>();
+ foreach (var type in types)
+ {
+ if (DocumentationCommentId.CreateDeclarationId(type) is string name &&
+ type.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
+ {
+ comments.Add((name, xml));
+ }
+ }
+ var properties = visitor.GetPublicProperties();
+ foreach (var property in properties)
+ {
+ if (DocumentationCommentId.CreateDeclarationId(property) is string name &&
+ property.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
+ {
+ comments.Add((name, xml));
+ }
+ }
+ var methods = visitor.GetPublicMethods();
+ foreach (var method in methods)
+ {
+ // If the method is a constructor for a record, skip it because we will have already processed the type.
+ if (method.MethodKind == MethodKind.Constructor)
+ {
+ continue;
+ }
+ if (DocumentationCommentId.CreateDeclarationId(method) is string name &&
+ method.GetDocumentationCommentXml(CultureInfo.InvariantCulture, expandIncludes: true, cancellationToken: cancellationToken) is string xml)
+ {
+ comments.Add((name, xml));
+ }
+ }
+ return comments;
+ }
+
+ internal static IEnumerable<(MemberKey, XmlComment?)> ParseComments(
+ (List<(string, string)> RawComments, Compilation Compilation) input,
+ CancellationToken cancellationToken)
+ {
+ var compilation = input.Compilation;
+ var comments = new List<(MemberKey, XmlComment?)>();
+ foreach (var (name, value) in input.RawComments)
+ {
+ if (DocumentationCommentId.GetFirstSymbolForDeclarationId(name, compilation) is ISymbol symbol)
+ {
+ var parsedComment = XmlComment.Parse(symbol, compilation, value, cancellationToken);
+ if (parsedComment is not null)
+ {
+ var memberKey = symbol switch
+ {
+ IMethodSymbol methodSymbol => MemberKey.FromMethodSymbol(methodSymbol),
+ IPropertySymbol propertySymbol => MemberKey.FromPropertySymbol(propertySymbol),
+ INamedTypeSymbol typeSymbol => MemberKey.FromTypeSymbol(typeSymbol),
+ _ => null
+ };
+ if (memberKey is not null)
+ {
+ comments.Add((memberKey, parsedComment));
+ }
+ }
+ }
+ }
+ return comments;
+ }
+
+ internal static bool FilterInvocations(SyntaxNode node, CancellationToken _)
+ => node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: "AddOpenApi" } };
+
+ internal static AddOpenApiInvocation GetAddOpenApiOverloadVariant(GeneratorSyntaxContext context, CancellationToken cancellationToken)
+ {
+ var invocationExpression = (InvocationExpressionSyntax)context.Node;
+
+ // Soft check to validate that the method is from the OpenApiServiceCollectionExtensions class
+ // in the Microsoft.AspNetCore.OpenApi assembly.
+ var symbol = context.SemanticModel.GetSymbolInfo(invocationExpression, cancellationToken).Symbol;
+ if (symbol is not IMethodSymbol methodSymbol
+ || methodSymbol.ContainingType.Name != "OpenApiServiceCollectionExtensions"
+ || methodSymbol.ContainingAssembly.Name != "Microsoft.AspNetCore.OpenApi")
+ {
+ return new(AddOpenApiOverloadVariant.Unknown, invocationExpression, null);
+ }
+
+ var interceptableLocation = context.SemanticModel.GetInterceptableLocation(invocationExpression, cancellationToken);
+ var argumentsCount = invocationExpression.ArgumentList.Arguments.Count;
+ if (argumentsCount == 0)
+ {
+ return new(AddOpenApiOverloadVariant.AddOpenApi, invocationExpression, interceptableLocation);
+ }
+ else if (argumentsCount == 2)
+ {
+ return new(AddOpenApiOverloadVariant.AddOpenApiDocumentNameConfigureOptions, invocationExpression, interceptableLocation);
+ }
+ else
+ {
+ // We need to disambiguate between the two overloads that take a string and a delegate
+ // AddOpenApi("v1") vs. AddOpenApi(options => { }). The implementation here is pretty naive and
+ // won't handle cases where the document name is provided by a variable or a method call.
+ var argument = invocationExpression.ArgumentList.Arguments[0];
+ if (argument.Expression is LiteralExpressionSyntax)
+ {
+ return new(AddOpenApiOverloadVariant.AddOpenApiDocumentName, invocationExpression, interceptableLocation);
+ }
+ else if (argument.Expression is LambdaExpressionSyntax)
+ {
+ return new(AddOpenApiOverloadVariant.AddOpenApiConfigureOptions, invocationExpression, interceptableLocation);
+ }
+ else
+ {
+ return new(AddOpenApiOverloadVariant.Unknown, invocationExpression, null);
+ }
+ }
+ }
+}
diff --git a/src/OpenApi/gen/XmlCommentGenerator.cs b/src/OpenApi/gen/XmlCommentGenerator.cs
new file mode 100644
index 000000000000..25f716493b83
--- /dev/null
+++ b/src/OpenApi/gen/XmlCommentGenerator.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;
+
+[Generator]
+public sealed partial class XmlCommentGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Pull out XML comments from referenced assemblies passed in as AdditionalFiles.
+ var commentsFromXmlFile = context.AdditionalTextsProvider
+ .Where(file => file.Path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
+ .Select(ParseXmlFile);
+ // Pull out XML comments from the target assembly using the information produced
+ // by Roslyn into the compilation.
+ var commentsFromTargetAssembly = context.CompilationProvider
+ .Select(ParseCompilation);
+ // Map string XML comments to structured data from both the AdditionalFiles
+ // and the target assembly.
+ var parsedCommentsFromXmlFile = commentsFromXmlFile
+ .Combine(context.CompilationProvider)
+ .Select(ParseComments);
+ var parsedCommentsFromCompilation = commentsFromTargetAssembly
+ .Combine(context.CompilationProvider)
+ .Select(ParseComments);
+ // Discover AddOpenApi invocations so that we can intercept them with an implicit
+ // registration of the transformers for mapping XML doc comments to the OpenAPI file.
+ var groupedAddOpenApiInvocations = context.SyntaxProvider
+ .CreateSyntaxProvider(FilterInvocations, GetAddOpenApiOverloadVariant)
+ .GroupWith((variantDetails) => variantDetails.Location, AddOpenApiInvocationComparer.Instance)
+ .Collect();
+
+ var generatedCommentsFromXmlFile = parsedCommentsFromXmlFile
+ .Select(EmitCommentsCache);
+ var generatedCommentsFromCompilation = parsedCommentsFromCompilation
+ .Select(EmitCommentsCache);
+
+ var result = generatedCommentsFromXmlFile.Collect()
+ .Combine(generatedCommentsFromCompilation)
+ .Combine(groupedAddOpenApiInvocations);
+
+ context.RegisterSourceOutput(result, (context, output) =>
+ {
+ var groupedAddOpenApiInvocations = output.Right;
+ var (generatedCommentsFromXmlFile, generatedCommentsFromCompilation) = output.Left;
+ var compiledXmlFileComments = !generatedCommentsFromXmlFile.IsDefaultOrEmpty
+ ? string.Join("\n", generatedCommentsFromXmlFile)
+ : string.Empty;
+ Emit(context, compiledXmlFileComments, generatedCommentsFromCompilation, groupedAddOpenApiInvocations);
+ });
+ }
+}
diff --git a/src/OpenApi/gen/XmlComments/MemberKey.cs b/src/OpenApi/gen/XmlComments/MemberKey.cs
new file mode 100644
index 000000000000..c6cb3d0741ab
--- /dev/null
+++ b/src/OpenApi/gen/XmlComments/MemberKey.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+
+internal sealed record MemberKey(
+ string? DeclaringType,
+ MemberType MemberKind,
+ string? Name,
+ string? ReturnType,
+ string[]? Parameters) : IEquatable
+{
+ private static readonly SymbolDisplayFormat _typeKeyFormat = new(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters);
+
+ public static MemberKey FromMethodSymbol(IMethodSymbol method)
+ {
+ return new MemberKey(
+ $"typeof({ReplaceGenericArguments(method.ContainingType.ToDisplayString(_typeKeyFormat))})",
+ MemberType.Method,
+ method.MetadataName,
+ method.ReturnType.TypeKind == TypeKind.TypeParameter
+ ? "typeof(object)"
+ : $"typeof({method.ReturnType.ToDisplayString(_typeKeyFormat)})",
+ [.. method.Parameters.Select(p =>
+ p.Type.TypeKind == TypeKind.TypeParameter
+ ? "typeof(object)"
+ : $"typeof({p.Type.ToDisplayString(_typeKeyFormat)})")]);
+ }
+
+ public static MemberKey FromPropertySymbol(IPropertySymbol property)
+ {
+ return new MemberKey(
+ $"typeof({ReplaceGenericArguments(property.ContainingType.ToDisplayString(_typeKeyFormat))})",
+ MemberType.Property,
+ property.Name,
+ null,
+ null);
+ }
+
+ public static MemberKey FromTypeSymbol(INamedTypeSymbol type)
+ {
+ return new MemberKey(
+ $"typeof({ReplaceGenericArguments(type.ToDisplayString(_typeKeyFormat))})",
+ MemberType.Type,
+ null,
+ null,
+ null);
+ }
+
+ /// Supports replacing generic type arguments to support use of open
+ /// generics in `typeof` expressions for the declaring type.
+ private static string ReplaceGenericArguments(string typeName)
+ {
+ var stack = new Stack();
+ var result = new StringBuilder(typeName);
+ for (var i = 0; i < result.Length; i++)
+ {
+ if (result[i] == '<')
+ {
+ stack.Push(i);
+ }
+ else if (result[i] == '>' && stack.Count > 0)
+ {
+ var start = stack.Pop();
+ // Replace everything between < and > with empty strings separated by commas
+ var segment = result.ToString(start + 1, i - start - 1);
+ var commaCount = segment.Count(c => c == ',');
+ var replacement = new string(',', commaCount);
+ result.Remove(start + 1, i - start - 1);
+ result.Insert(start + 1, replacement);
+ i = start + replacement.Length + 1;
+ }
+ }
+ return result.ToString();
+ }
+}
+
+internal enum MemberType
+{
+ Type,
+ Property,
+ Method
+}
diff --git a/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs b/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
new file mode 100644
index 000000000000..ee6d503449a0
--- /dev/null
+++ b/src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
@@ -0,0 +1,443 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using Microsoft.AspNetCore.Analyzers.Infrastructure;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
+
+///
+/// Source code in this class is derived from Roslyn's Xml documentation comment processing
+/// to support the full resolution of tags.
+/// For the original code, see https://github.com/dotnet/roslyn/blob/ef1d7fe925c94e96a93e4c9af50983e0f675a9fd/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs.
+///
+internal sealed partial class XmlComment
+{
+ private static string? GetDocumentationComment(ISymbol symbol, string xmlText, HashSet? visitedSymbols, Compilation compilation, CancellationToken cancellationToken)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(xmlText))
+ {
+ if (IsEligibleForAutomaticInheritdoc(symbol))
+ {
+ xmlText = $@"";
+ }
+ else
+ {
+ return string.Empty;
+ }
+ }
+
+ try
+ {
+ var element = XElement.Parse(xmlText, LoadOptions.PreserveWhitespace);
+ element.ReplaceNodes(RewriteMany(symbol, visitedSymbols, compilation, [.. element.Nodes()], cancellationToken));
+ xmlText = element.ToString(SaveOptions.DisableFormatting);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ return xmlText;
+ }
+ catch (XmlException)
+ {
+ return null;
+ }
+ }
+
+ private static bool IsEligibleForAutomaticInheritdoc(ISymbol symbol)
+ {
+ // Only the following symbols are eligible to inherit documentation without an element:
+ //
+ // * Members that override an inherited member
+ // * Members that implement an interface member
+ if (symbol.IsOverride)
+ {
+ return true;
+ }
+
+ if (symbol.ContainingType is null)
+ {
+ // Observed with certain implicit operators, such as operator==(void*, void*).
+ return false;
+ }
+
+ switch (symbol.Kind)
+ {
+ case SymbolKind.Method:
+ case SymbolKind.Property:
+ case SymbolKind.Event:
+ if (symbol.ExplicitOrImplicitInterfaceImplementations().Any())
+ {
+ return true;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ return false;
+ }
+
+ private static XNode[] RewriteMany(ISymbol symbol, HashSet? visitedSymbols, Compilation compilation, XNode[] nodes, CancellationToken cancellationToken)
+ {
+ var result = new List();
+ foreach (var child in nodes)
+ {
+ result.AddRange(RewriteInheritdocElements(symbol, visitedSymbols, compilation, child, cancellationToken));
+ }
+
+ return [.. result];
+ }
+
+ private static XNode[] RewriteInheritdocElements(ISymbol symbol, HashSet? visitedSymbols, Compilation compilation, XNode node, CancellationToken cancellationToken)
+ {
+ if (node.NodeType == XmlNodeType.Element)
+ {
+ var element = (XElement)node;
+ if (ElementNameIs(element, DocumentationCommentXmlNames.InheritdocElementName))
+ {
+ var rewritten = RewriteInheritdocElement(symbol, visitedSymbols, compilation, element, cancellationToken);
+ if (rewritten is not null)
+ {
+ return rewritten;
+ }
+ }
+ }
+
+ if (node is not XContainer container)
+ {
+ return [Copy(node, copyAttributeAnnotations: false)];
+ }
+
+ var oldNodes = container.Nodes();
+
+ // Do this after grabbing the nodes, so we don't see copies of them.
+ container = Copy(container, copyAttributeAnnotations: false);
+
+ // WARN: don't use node after this point - use container since it's already been copied.
+
+ if (oldNodes != null)
+ {
+ var rewritten = RewriteMany(symbol, visitedSymbols, compilation, [.. oldNodes], cancellationToken);
+ container.ReplaceNodes(rewritten);
+ }
+
+ return [container];
+ }
+
+ private static XNode[]? RewriteInheritdocElement(ISymbol memberSymbol, HashSet? visitedSymbols, Compilation compilation, XElement element, CancellationToken cancellationToken)
+ {
+ var crefAttribute = element.Attribute(XName.Get(DocumentationCommentXmlNames.CrefAttributeName));
+ var pathAttribute = element.Attribute(XName.Get(DocumentationCommentXmlNames.PathAttributeName));
+
+ var candidate = GetCandidateSymbol(memberSymbol);
+ var hasCandidateCref = candidate is object;
+
+ var hasCrefAttribute = crefAttribute is not null;
+ var hasPathAttribute = pathAttribute is not null;
+ if (!hasCrefAttribute && !hasCandidateCref)
+ {
+ // No cref available
+ return null;
+ }
+
+ ISymbol? symbol;
+ if (crefAttribute is null)
+ {
+ if (candidate is null)
+ {
+ return null;
+ }
+ symbol = candidate;
+ }
+ else
+ {
+ var crefValue = crefAttribute.Value;
+ symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefValue, compilation);
+ if (symbol is null)
+ {
+ return null;
+ }
+ }
+
+ visitedSymbols ??= [];
+ if (!visitedSymbols.Add(symbol!))
+ {
+ // Prevent recursion
+ return null;
+ }
+
+ try
+ {
+ var xmlDocumentation = symbol.GetDocumentationCommentXml(cancellationToken: cancellationToken);
+ if (xmlDocumentation is null)
+ {
+ return [];
+ }
+ var inheritedDocumentation = GetDocumentationComment(symbol, xmlDocumentation, visitedSymbols, compilation, cancellationToken);
+ if (inheritedDocumentation == string.Empty)
+ {
+ return [];
+ }
+
+ var document = XDocument.Parse(inheritedDocumentation, LoadOptions.PreserveWhitespace);
+ string xpathValue;
+ if (string.IsNullOrEmpty(pathAttribute?.Value))
+ {
+ xpathValue = BuildXPathForElement(element.Parent!);
+ }
+ else
+ {
+ xpathValue = pathAttribute!.Value;
+ if (xpathValue.StartsWith("/", StringComparison.InvariantCulture))
+ {
+ // Account for the root or element
+ xpathValue = "/*" + xpathValue;
+ }
+ }
+
+ // Consider the following code, we want Test.Clone to say "Clones a Test" instead of "Clones a int", thus
+ // we rewrite `typeparamref`s as cref pointing to the correct type:
+ /*
+ public class Test : ICloneable>
+ {
+ ///
+ public Test Clone() => new();
+ }
+
+ /// A type that has clonable instances.
+ /// The type of instances that can be cloned.
+ public interface ICloneable
+ {
+ /// Clones a .
+ public T Clone();
+ }
+ */
+ // Note: there is no way to cref an instantiated generic type. See https://github.com/dotnet/csharplang/issues/401
+ var typeParameterRefs = document.Descendants(DocumentationCommentXmlNames.TypeParameterReferenceElementName).ToImmutableArray();
+ foreach (var typeParameterRef in typeParameterRefs)
+ {
+ if (typeParameterRef.Attribute(DocumentationCommentXmlNames.NameAttributeName) is XAttribute typeParamName)
+ {
+ var targetTypeParameter = symbol.GetAllTypeParameters().FirstOrDefault(p => p.Name == typeParamName.Value);
+ if (targetTypeParameter is not null
+ && symbol.OriginalDefinition.GetAllTypeParameters().IndexOf(targetTypeParameter) is int index
+ && index >= 0)
+ {
+ var typeArgs = symbol.GetAllTypeArguments();
+ if (index < typeArgs.Length)
+ {
+ var docId = typeArgs[index].GetDocumentationCommentId();
+ if (docId != null && !docId.StartsWith("!", StringComparison.OrdinalIgnoreCase))
+ {
+ var replacement = new XElement(DocumentationCommentXmlNames.SeeElementName);
+ replacement.SetAttributeValue(DocumentationCommentXmlNames.CrefAttributeName, docId);
+ typeParameterRef.ReplaceWith(replacement);
+ }
+ }
+ }
+ }
+ }
+
+ var loadedElements = TrySelectNodes(document, xpathValue);
+ return loadedElements ?? [];
+ }
+ catch (XmlException)
+ {
+ return [];
+ }
+ finally
+ {
+ visitedSymbols.Remove(symbol);
+ }
+
+ // Local functions
+ static ISymbol? GetCandidateSymbol(ISymbol memberSymbol)
+ {
+ if (memberSymbol.ExplicitInterfaceImplementations().Any())
+ {
+ return memberSymbol.ExplicitInterfaceImplementations().First();
+ }
+ else if (memberSymbol.IsOverride)
+ {
+ return memberSymbol.GetOverriddenMember();
+ }
+
+ if (memberSymbol is IMethodSymbol methodSymbol)
+ {
+ if (methodSymbol.MethodKind is MethodKind.Constructor or MethodKind.StaticConstructor)
+ {
+ var baseType = memberSymbol.ContainingType.BaseType;
+#nullable disable // Can 'baseType' be null here? https://github.com/dotnet/roslyn/issues/39166
+ return baseType.Constructors.Where(c => IsSameSignature(methodSymbol, c)).FirstOrDefault();
+#nullable enable
+ }
+ else
+ {
+ // check for implicit interface
+ return methodSymbol.ExplicitOrImplicitInterfaceImplementations().FirstOrDefault();
+ }
+ }
+ else if (memberSymbol is INamedTypeSymbol typeSymbol)
+ {
+ if (typeSymbol.TypeKind == TypeKind.Class)
+ {
+ // Classes use the base type as the default inheritance candidate. A different target (e.g. an
+ // interface) can be provided via the 'path' attribute.
+ return typeSymbol.BaseType;
+ }
+ else if (typeSymbol.TypeKind == TypeKind.Interface)
+ {
+ return typeSymbol.Interfaces.FirstOrDefault();
+ }
+ else
+ {
+ // This includes structs, enums, and delegates as mentioned in the inheritdoc spec
+ return null;
+ }
+ }
+
+ return memberSymbol.ExplicitOrImplicitInterfaceImplementations().FirstOrDefault();
+ }
+
+ static bool IsSameSignature(IMethodSymbol left, IMethodSymbol right)
+ {
+ if (left.Parameters.Length != right.Parameters.Length)
+ {
+ return false;
+ }
+
+ if (left.IsStatic != right.IsStatic)
+ {
+ return false;
+ }
+
+ if (!SymbolEqualityComparer.Default.Equals(left.ReturnType, right.ReturnType))
+ {
+ return false;
+ }
+
+ for (var i = 0; i < left.Parameters.Length; i++)
+ {
+ if (!SymbolEqualityComparer.Default.Equals(left.Parameters[i].Type, right.Parameters[i].Type))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ static string BuildXPathForElement(XElement element)
+ {
+ if (ElementNameIs(element, "member") || ElementNameIs(element, "doc"))
+ {
+ // Avoid string concatenation allocations for inheritdoc as a top-level element
+ return "/*/node()[not(self::overloads)]";
+ }
+
+ var path = "/node()[not(self::overloads)]";
+ for (var current = element; current != null; current = current.Parent)
+ {
+ var currentName = current.Name.ToString();
+ if (ElementNameIs(current, "member") || ElementNameIs(current, "doc"))
+ {
+ // Allow and to be used interchangeably
+ currentName = "*";
+ }
+
+ path = "/" + currentName + path;
+ }
+
+ return path;
+ }
+ }
+
+ private static bool ElementNameIs(XElement element, string name)
+ => string.IsNullOrEmpty(element.Name.NamespaceName) && DocumentationCommentXmlNames.ElementEquals(element.Name.LocalName, name);
+
+ private static TNode Copy(TNode node, bool copyAttributeAnnotations)
+ where TNode : XNode
+ {
+ XNode copy;
+
+ // Documents can't be added to containers, so our usual copy trick won't work.
+ if (node.NodeType == XmlNodeType.Document)
+ {
+ copy = new XDocument((XDocument)(object)node);
+ }
+ else
+ {
+ XContainer temp = new XElement("temp");
+ temp.Add(node);
+ copy = temp.LastNode!;
+ temp.RemoveNodes();
+ }
+
+ AnalyzerDebug.Assert(copy != node);
+ AnalyzerDebug.Assert(copy.Parent == null); // Otherwise, when we give it one, it will be copied.
+
+ // Copy annotations, the above doesn't preserve them.
+ // We need to preserve Location annotations as well as line position annotations.
+ CopyAnnotations(node, copy);
+
+ // We also need to preserve line position annotations for all attributes
+ // since we report errors with attribute locations.
+ if (copyAttributeAnnotations && node.NodeType == XmlNodeType.Element)
+ {
+ var sourceElement = (XElement)(object)node;
+ var targetElement = (XElement)copy;
+
+ var sourceAttributes = sourceElement.Attributes().GetEnumerator();
+ var targetAttributes = targetElement.Attributes().GetEnumerator();
+ while (sourceAttributes.MoveNext() && targetAttributes.MoveNext())
+ {
+ AnalyzerDebug.Assert(sourceAttributes.Current.Name == targetAttributes.Current.Name);
+ CopyAnnotations(sourceAttributes.Current, targetAttributes.Current);
+ }
+ }
+
+ return (TNode)copy;
+ }
+
+ private static void CopyAnnotations(XObject source, XObject target)
+ {
+ foreach (var annotation in source.Annotations