File: XmlCommentGenerator.Parser.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.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);
            }
        }
    }
}