File: src\Analyzers\Core\CodeFixes\DocumentationComments\AbstractAddDocCommentNodesCodeFixProvider.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.DocumentationComments;
 
internal abstract class AbstractAddDocCommentNodesCodeFixProvider
    <TXmlElementSyntax, TXmlNameAttributeSyntax, TXmlTextSyntax, TMemberDeclarationSyntax> : CodeFixProvider
    where TXmlElementSyntax : SyntaxNode
    where TXmlNameAttributeSyntax : SyntaxNode
    where TXmlTextSyntax : SyntaxNode
    where TMemberDeclarationSyntax : SyntaxNode
{
    public override FixAllProvider GetFixAllProvider()
        => WellKnownFixAllProviders.BatchFixer;
 
    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
        var parentMethod = root.FindNode(context.Span).FirstAncestorOrSelf<TMemberDeclarationSyntax>();
        if (parentMethod is null)
            return;
 
        var docCommentNode = TryGetDocCommentNode(parentMethod.GetLeadingTrivia());
        if (docCommentNode is null)
            return;
 
        context.RegisterCodeFix(
            CodeAction.Create(
                CodeFixesResources.Add_missing_param_nodes,
                cancellationToken => AddParamTagAsync(context.Document, parentMethod, docCommentNode, cancellationToken),
                nameof(CodeFixesResources.Add_missing_param_nodes)),
            context.Diagnostics);
    }
 
    protected abstract string NodeName { get; }
 
    protected abstract List<TXmlNameAttributeSyntax> GetNameAttributes(TXmlElementSyntax node);
    protected abstract string GetValueFromNameAttribute(TXmlNameAttributeSyntax attribute);
    protected abstract SyntaxNode? TryGetDocCommentNode(SyntaxTriviaList parameter);
    protected abstract string GetXmlElementLocalName(TXmlElementSyntax element);
    protected abstract ImmutableArray<string> GetParameterNames(TMemberDeclarationSyntax method);
    protected abstract TXmlElementSyntax GetNewNode(string parameterName, bool isFirstNodeInComment);
 
    protected async Task<Document> AddParamTagAsync(
        Document document, TMemberDeclarationSyntax parentMethod, SyntaxNode docCommentNode, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        var newDocComment = docCommentNode;
        var parameterNames = GetParameterNames(parentMethod);
 
        for (var index = 0; index < parameterNames.Length; index++)
        {
            var parameterName = parameterNames[index];
            var paramNodes = GetElementNodes(newDocComment, NodeName);
            if (NodeExists(paramNodes, parameterName))
            {
                continue;
            }
 
            var paramsBeforeCurrentParam = parameterNames.TakeWhile(t => t != parameterName).ToList();
            var paramsAfterCurrentParam = parameterNames.Except(paramsBeforeCurrentParam).ToList();
            paramsAfterCurrentParam.Remove(parameterName);
 
            // If the index is not `0`, there is a node before the current one for sure
            // If the index is `0`, try to add the node after the `summary` node,
            // only if any existing nodes are at the top level--this route will not
            // be taken if the existing node is nested in another node
            var summaryNode = GetElementNodes(newDocComment, "summary").FirstOrDefault();
            if (index != 0 || (!paramNodes.Any() && summaryNode != null))
            {
                // First, try to get the node before the param node so we know where to insert the new node
                TXmlElementSyntax? nodeBeforeNewParamNode = null;
                if (index > 0)
                {
                    nodeBeforeNewParamNode = GetParamNodeForParamName(paramNodes, parameterNames[index - 1]);
                }
 
                // This will be hit in the index is `0`, in which case the previous node is the summary node
                nodeBeforeNewParamNode ??= summaryNode;
 
                newDocComment = newDocComment.InsertNodesAfter(nodeBeforeNewParamNode!,
                    [GetNewNode(parameterName, isFirstNodeInComment: false)]);
 
                continue;
            }
 
            // At this point, the node has to go at the beginning of the comment
            var nodeAfterNewParamNode = paramNodes.FirstOrDefault() ?? newDocComment.ChildNodes().First();
 
            // Adjust for doc comment marker before the node
            var paramNodeSiblings = nodeAfterNewParamNode.GetRequiredParent().ChildNodes().ToList();
            var indexOfNode = paramNodeSiblings.IndexOf(nodeAfterNewParamNode);
 
            // set insert node to be the doc comment signifier of the closest param before the new node
            if (indexOfNode > 0 && paramNodeSiblings[indexOfNode - 1] is TXmlTextSyntax previousSibling)
                nodeAfterNewParamNode = previousSibling;
 
            var newNodeList = new[]
            {
                // the last value will almost always be true, unless the node is embedded in another doc comment node
                GetNewNode(parameterName, nodeAfterNewParamNode == newDocComment.ChildNodes().First())
            };
            newDocComment = newDocComment.InsertNodesBefore(nodeAfterNewParamNode, newNodeList);
        }
 
        var newRoot = root.ReplaceNode(docCommentNode, newDocComment.WithAdditionalAnnotations(Formatter.Annotation));
        return document.WithSyntaxRoot(newRoot);
    }
 
    private List<TXmlElementSyntax> GetElementNodes(SyntaxNode docComment, string nodeName)
    {
        var nodes = docComment.ChildNodes().OfType<TXmlElementSyntax>()
                                           .Where(w => GetXmlElementLocalName(w) == nodeName)
                                           .ToList();
 
        // Prefer to return element nodes that are the top-level children of the DocComment.
        // If we don't find any, then fallback to the first element node at any depth with the requested name.
        if (!nodes.Any())
        {
            nodes = docComment.DescendantNodes(descendIntoChildren: _ => true)
                              .OfType<TXmlElementSyntax>()
                              .Where(w => GetXmlElementLocalName(w) == nodeName)
                              .ToList();
        }
 
        return nodes;
    }
 
    private bool NodeExists(IEnumerable<TXmlElementSyntax> paramNodes, string name)
    {
        return paramNodes.Select(GetNameAttributes)
                         .Where(nameAttributes => nameAttributes.Count == 1)
                         .Any(nameAttributes => nameAttributes.Select(GetValueFromNameAttribute).Contains(name));
    }
 
    protected TXmlElementSyntax? GetParamNodeForParamName(
        IEnumerable<TXmlElementSyntax> paramNodeList,
        string name)
    {
        foreach (var paramNode in paramNodeList)
        {
            var paramNameAttributesForNode = GetNameAttributes(paramNode);
 
            // param node is missing `name` attribute or there are multiple `name` attributes
            if (paramNameAttributesForNode.Count != 1)
            {
                continue;
            }
 
            if (GetValueFromNameAttribute(paramNameAttributesForNode.Single()) == name)
            {
                return paramNode;
            }
        }
 
        return null;
    }
}