File: XmlComments\XmlComment.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;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.CodeAnalysis;
 
namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;
 
internal sealed partial class XmlComment
{
    public string? Summary { get; internal set; }
    public string? Description { get; internal set; }
    public string? Value { get; internal set; }
    public string? Remarks { get; internal set; }
    public string? Returns { get; internal set; }
    public bool? Deprecated { get; internal set; }
    public List<string?>? Examples { get; internal set; }
    public List<XmlParameterComment> Parameters { get; internal set; } = [];
    public List<XmlResponseComment> Responses { get; internal set; } = [];
 
    private XmlComment(Compilation compilation, string xml)
    {
        // Treat <doc> as <member>
        if (xml.StartsWith("<doc>", StringComparison.InvariantCulture) && xml.EndsWith("</doc>", StringComparison.InvariantCulture))
        {
            xml = xml.Substring(5, xml.Length - 11);
            xml = xml.Trim();
        }
 
        // Transform triple slash comment
        var doc = XDocument.Parse(xml, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
 
        ResolveCrefLink(compilation, doc, $"//{DocumentationCommentXmlNames.SeeAlsoElementName}[@cref]");
        ResolveCrefLink(compilation, doc, $"//{DocumentationCommentXmlNames.SeeElementName}[@cref]");
        // Resolve <list> and <item> tags into bullets
        ResolveListTags(doc);
        // Resolve <code> tags into code blocks
        ResolveCodeTags(doc, DocumentationCommentXmlNames.CodeElementName, "```");
        // Resolve <paramref> and typeparamref tags into parameter names
        ResolveParamRefTags(doc);
        // Resolve <para> tags into underlying content
        ResolveParaTags(doc);
        // Resolve <c> tags into inline code blocks
        ResolveCodeTags(doc, DocumentationCommentXmlNames.CElementName, "`");
 
        var nav = doc.CreateNavigator();
        Summary = GetSingleNodeValue(nav, "/member/summary");
        Description = GetSingleNodeValue(nav, "/member/description");
        Remarks = GetSingleNodeValue(nav, "/member/remarks");
        Returns = GetSingleNodeValue(nav, "/member/returns");
        Value = GetSingleNodeValue(nav, "/member/value");
        Deprecated = GetSingleNodeValue(nav, "/member/deprecated") == "true";
 
        Examples = [.. GetMultipleExampleNodes(nav, "/member/example")];
        Parameters = XmlParameterComment.GetXmlParameterListComment(nav, "/member/param");
        Responses = XmlResponseComment.GetXmlResponseCommentList(nav, "/member/response");
    }
 
    private static void ResolveListTags(XDocument document)
    {
        var listElements = document.Descendants(DocumentationCommentXmlNames.ListElementName).ToArray();
        foreach (var element in listElements)
        {
            if (element is null)
            {
                continue;
            }
            var rawListType = element.Attribute(DocumentationCommentXmlNames.TypeAttributeName)?.Value;
            var listPrefix = rawListType switch
            {
                "table" => "* ",
                "number" => "1. ",
                "bullet" => "* ",
                _ => "* ",
            };
            var items = element.Elements(DocumentationCommentXmlNames.ItemElementName);
            if (items == null)
            {
                continue;
            }
            var bulletPoints = items
                .Select(item => listPrefix + item?.Value?.TrimEachLine() ?? string.Empty)
                .ToList();
 
            var bulletText = string.Join("\n", bulletPoints);
            element.ReplaceWith(new XText(bulletText));
        }
    }
 
    private static void ResolveCodeTags(XDocument document, string tagName, string codeBlockDelimiter)
    {
        var codeElements = document.Descendants(tagName).ToArray();
        foreach (var element in codeElements)
        {
            if (element is null)
            {
                continue;
            }
            var codeText = element.Value.TrimEachLine();
            element.ReplaceWith(new XText(codeBlockDelimiter + codeText + codeBlockDelimiter));
        }
    }
 
    private static void ResolveParamRefTags(XDocument document)
    {
        var paramRefElements = document.Descendants(DocumentationCommentXmlNames.ParameterReferenceElementName).ToArray();
        foreach (var element in paramRefElements)
        {
            if (element is null)
            {
                continue;
            }
            var paramName = element.Attribute(DocumentationCommentXmlNames.NameAttributeName)?.Value;
            if (paramName is null)
            {
                continue;
            }
            element.ReplaceWith(new XText(paramName));
        }
 
        var typeParamRefElements = document.Descendants(DocumentationCommentXmlNames.TypeParameterReferenceElementName).ToArray();
        foreach (var element in typeParamRefElements)
        {
            if (element is null)
            {
                continue;
            }
            var paramName = element.Attribute(DocumentationCommentXmlNames.NameAttributeName)?.Value;
            if (paramName is null)
            {
                continue;
            }
            element.ReplaceWith(new XText(paramName));
        }
    }
 
    private static void ResolveParaTags(XDocument document)
    {
        var paraElements = document.Descendants(DocumentationCommentXmlNames.ParaElementName).ToArray();
        foreach (var element in paraElements)
        {
            if (element is null)
            {
                continue;
            }
            var paraText = element.Value.TrimEachLine();
            element.ReplaceWith(new XText(paraText));
        }
    }
 
    public static XmlComment? Parse(ISymbol symbol, Compilation compilation, string xmlText, CancellationToken cancellationToken)
    {
        // Avoid processing empty or malformed XML comments.
        if (string.IsNullOrEmpty(xmlText) ||
            xmlText.StartsWith("<!-- Badly formed XML comment ignored for member ", StringComparison.Ordinal))
        {
            return null;
        }
 
        var resolvedComment = GetDocumentationComment(symbol, xmlText, [], compilation, cancellationToken);
        return !string.IsNullOrEmpty(resolvedComment) ? new XmlComment(compilation, resolvedComment!) : null;
    }
 
    /// <summary>
    /// Resolves the cref links in the XML documentation into type names.
    /// </summary>
    /// <param name="compilation">The compilation to resolve type symbol declarations from.</param>
    /// <param name="node">The target node to process crefs in.</param>
    /// <param name="nodeSelector">The node type to process crefs for, can be `see` or `seealso`.</param>
    private static void ResolveCrefLink(Compilation compilation, XNode node, string nodeSelector)
    {
        if (node == null || string.IsNullOrEmpty(nodeSelector))
        {
            return;
        }
 
        var nodes = node.XPathSelectElements(nodeSelector + "[@cref]").ToArray();
        foreach (var item in nodes)
        {
            var cref = item.Attribute(DocumentationCommentXmlNames.CrefAttributeName).Value;
            if (string.IsNullOrEmpty(cref))
            {
                continue;
            }
 
            var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(cref, compilation);
            if (symbol is not null)
            {
                var type = symbol.ToDisplayString();
                item.ReplaceWith(new XText(type));
            }
        }
    }
 
    private static IEnumerable<string?> GetMultipleExampleNodes(XPathNavigator navigator, string selector)
    {
        var iterator = navigator.Select(selector);
        if (iterator == null)
        {
            yield break;
        }
        foreach (XPathNavigator nav in iterator)
        {
            yield return nav.InnerXml.TrimEachLine();
        }
    }
 
    private static string? GetSingleNodeValue(XPathNavigator nav, string selector)
    {
        var node = nav.Clone().SelectSingleNode(selector);
        return node?.InnerXml.TrimEachLine();
    }
}