| File: XmlComments\XmlComment.cs | Web Access |
| Project: src\aspnetcore\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); ResolveCrefLink(compilation, doc, DocumentationCommentXmlNames.SeeElementName); ResolveLangKeyword(doc, DocumentationCommentXmlNames.SeeElementName); // 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="elementName">The node type to process crefs for, can be `see` or `seealso`.</param> private static void ResolveCrefLink(Compilation compilation, XNode node, string elementName) { if (node == null || string.IsNullOrEmpty(elementName)) { return; } var attributeName = DocumentationCommentXmlNames.CrefAttributeName; var nodes = node.XPathSelectElements($"//{elementName}[@{attributeName}]").ToArray(); foreach (var item in nodes) { var cref = item.Attribute(attributeName).Value; if (string.IsNullOrEmpty(cref)) { continue; } var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(cref, compilation); if (symbol is not null) { var type = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); item.ReplaceWith(new XText(type)); } } } /// <summary> /// Resolves the links in the XML documentation into language keywords. /// </summary> /// <param name="node">The target node to process crefs in.</param> /// <param name="elementName">The node type to process langwords for, can be `see` or `seealso`.</param> private static void ResolveLangKeyword(XNode node, string elementName) { if (node == null || string.IsNullOrEmpty(elementName)) { return; } var attributeName = DocumentationCommentXmlNames.LangwordAttributeName; var nodes = node.XPathSelectElements($"//{elementName}[@{attributeName}]").ToArray(); foreach (var item in nodes) { var langword = item.Attribute(attributeName).Value; if (string.IsNullOrEmpty(langword)) { continue; } item.ReplaceWith(new XText($"`{langword}`")); } } 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(); } }