7
\$\begingroup\$

This is an analyzer and code fix for VSDiagnostics that detects when a doc comment on a method does not have all parameters listed. When the user tells it to provide the fix, it adds a /// <param name="{paramName}"></param> line for all missing parameter nodes.

Here is the analyzer:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
    var method = context.Node as MethodDeclarationSyntax;
    if (method == null)
    {
        return; 
    }

    var docNodes = method.GetLeadingTrivia()
            .Where(n => n.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
            .Select(t => t.GetStructure())
            .OfType<DocumentationCommentTriviaSyntax>()
            .ToList();

    if (!docNodes.Any())
    {
        return;
    }

    var paramNames = method.ParameterList.Parameters.Select(p => p.Identifier.Text).ToList();
    var xmlParamNodes = docNodes.SelectMany(n => n.Content.OfType<XmlElementSyntax>())
                .Where(e => e.StartTag.Name.LocalName.Text == "param")
                .ToList();

    var xmlParamNodeNames =
        xmlParamNodes.SelectMany(n =>
                n.StartTag.Attributes.OfType<XmlNameAttributeSyntax>()
                    .Where(a => a.Name.LocalName.Text == "name")
                    .Select(t => t.Identifier.Identifier.Text)).ToList();

    if (!paramNames.All(n => xmlParamNodeNames.Contains(n)))
    {
        context.ReportDiagnostic(Diagnostic.Create(Rule, method.Identifier.GetLocation()));
    }
}

First, I get the MethodDeclarationSyntax node, then get the doc nodes for that method. If a doc node exists, I get the list of method parameter names and the list of parameter names in the XML doc. If not all method parameter names are in the XML doc parameter list, I report a diagnostic.

What I am more bothered about is the code fix:

private Task<Solution> RemoveXmlParameterNode(Document document, SyntaxNode root, MethodDeclarationSyntax method)
{
    var docComment = method.GetLeadingTrivia()
                       .Where(n => n.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
                       .Select(t => t.GetStructure())
                       .OfType<DocumentationCommentTriviaSyntax>()
                       .First();

    var summaryBlock = docComment.Content.OfType<XmlElementSyntax>()
            .FirstOrDefault(node => node.StartTag.Name.LocalName.Text == "summary");

    var paramNames = method.ParameterList.Parameters.Select(param => param.Identifier.Text).ToList();
    var xmlParamNodes = docComment.Content.OfType<XmlElementSyntax>()
                                .Where(element => element.StartTag.Name.LocalName.Text == "param")
                                .ToList();

    var xmlParamNodeNames = xmlParamNodes.SelectMany(node =>
            node.StartTag.Attributes.OfType<XmlNameAttributeSyntax>()
                .Where(a => a.Name.LocalName.Text == "name")
                .Select(t => t.Identifier.Identifier.Text)).ToList();

    var missingNodeParamNames = paramNames.Where(name => !xmlParamNodeNames.Contains(name));

    foreach (var missingNodeParamName in missingNodeParamNames)
    {
        var attribute = SyntaxFactory.XmlNameAttribute(SyntaxFactory.XmlName("name"),
            SyntaxFactory.Token(SyntaxKind.DoubleQuoteToken), missingNodeParamName,
            SyntaxFactory.Token(SyntaxKind.DoubleQuoteToken))
            .WithLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "));

        var paramNode = SyntaxFactory.XmlElement(
            SyntaxFactory.XmlElementStartTag(SyntaxFactory.XmlName("param"),
                SyntaxFactory.List<XmlAttributeSyntax>()
                    .Add(attribute)), SyntaxFactory.XmlElementEndTag(SyntaxFactory.XmlName("param")))
            .WithLeadingTrivia(summaryBlock?.GetLeadingTrivia());

        xmlParamNodes.Insert(paramNames.IndexOf(missingNodeParamName), paramNode);
    }

    var newDocComment = SyntaxFactory.DocumentationCommentTrivia(
        SyntaxKind.SingleLineDocumentationCommentTrivia, GetNodes(docComment, xmlParamNodes, summaryBlock));
    return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(docComment, newDocComment)).Project.Solution);
}

private SyntaxList<SyntaxNode> GetNodes(DocumentationCommentTriviaSyntax docComment, List<XmlElementSyntax> xmlParamNodes, XmlElementSyntax summaryBlock)
{
    var nodes = SyntaxFactory.List<SyntaxNode>().Add(docComment.Content.First());

    var newLineToken = SyntaxFactory.Token(default(SyntaxTriviaList),
        SyntaxKind.XmlTextLiteralNewLineToken,
        Environment.NewLine, Environment.NewLine, default(SyntaxTriviaList));

    var docCommentToken = SyntaxFactory.Token(SyntaxFactory.TriviaList(
        SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, "///")),
        SyntaxKind.XmlTextLiteralToken, " ", " ", default(SyntaxTriviaList));

    var xmlTextElement = SyntaxFactory.XmlText(SyntaxFactory.TokenList(newLineToken, docCommentToken));

    var paramListInserted = false;
    // We already added the first one.
    for (var i = 1; i < docComment.Content.Count; i++)
    {
        var syntax = docComment.Content[i] as XmlTextSyntax;

        // Skip XmlTextSyntax nodes - we will re-add them
        if (syntax != null &&
            syntax.TextTokens.All(t => t.Text.Trim() == string.Empty))
        {
            continue;
        }

        if (!xmlParamNodes.Contains(docComment.Content[i]))
        {
            nodes = i == 1 || docComment.Content[i] is XmlTextSyntax
                ? nodes.Add(docComment.Content[i])
                : nodes.AddRange(new SyntaxNode[] {xmlTextElement, docComment.Content[i]});
        }

        if (docComment.Content[i] != summaryBlock && (i != docComment.Content.Count - 1 || paramListInserted))
        {
            continue;
        }

        nodes = xmlParamNodes.Aggregate(nodes, (current, paramNode) => current.AddRange(new SyntaxNode[] { xmlTextElement, paramNode }));
        paramListInserted = true;
    }

    if (!paramListInserted)
    {
        nodes = xmlParamNodes.Aggregate(nodes, (current, paramNode) => current.AddRange(new SyntaxNode[] { xmlTextElement, paramNode }));
    }

    var lastXmlTextSyntax = nodes.Last() as XmlTextSyntax;
    if (lastXmlTextSyntax == null || !lastXmlTextSyntax.TextTokens.Last().IsKind(SyntaxKind.XmlTextLiteralNewLineToken))
    {
        var endNode = SyntaxFactory.XmlText(
                SyntaxFactory.TokenList(SyntaxFactory.Token(default(SyntaxTriviaList), SyntaxKind.XmlTextLiteralNewLineToken,
                    Environment.NewLine, Environment.NewLine, default(SyntaxTriviaList))));

        return nodes.Add(endNode);
    }

    return nodes;
}

Here, I get the doc comment, its summary block, and the param lists. Then, I build the full parameter list, build a list of nodes, and replace the old doc comment with the new one.

When I build the list of nodes, instead of checking which items are separated with a newline, etc, I just remove all XmlTextSyntax nodes that only contain whitespace (which contain the beginning /// and the newlines), and insert my own between each item. I also leave the beginning node to prevent messing with the whitespace before the doc comment. All other items, except the current param nodes, go into the list. The param nodes are all entered together from the list to keep the items in the same order as they are in the method. If the last item is not an XmlTextSyntax node that ends with a newline token, I add a newline to prevent the beginning of the parameter definition from being added to the doc comment. This seems awful long and complex compared to some Roslyn code fixes, and I am wondering if I am missing an easier way to do this.

Example change (full test suite can be found here):

namespace ConsoleApplication1
{
    class MyClass
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="myInt"></param>
        /// <param name="myDouble"></param>
        /// <returns></returns>
        public int Fizz(int myInt, string myString, char myChar, double myDouble)
        {
            return 0;
        }
    }
}
namespace ConsoleApplication1
{
    class MyClass
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="myInt"></param>
        /// <param name="myString"></param>
        /// <param name="myChar"></param>
        /// <param name="myDouble"></param>
        /// <returns></returns>
        public int Fizz(int myInt, string myString, char myChar, double myDouble)
        {
            return 0;
        }
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I would recommend first to extract the duplicate code in methods. This way you have smaller methods and it's easier to change implementation and test.

This code is a duplicate, extract it in a method and send the token as parameter.

var newLineToken = SyntaxFactory.Token(default(SyntaxTriviaList),
    SyntaxKind.XmlTextLiteralNewLineToken,
    Environment.NewLine, Environment.NewLine, default(SyntaxTriviaList));

var docCommentToken = SyntaxFactory.Token(SyntaxFactory.TriviaList(
    SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, "///")),
    SyntaxKind.XmlTextLiteralToken, " ", " ", default(SyntaxTriviaList));

 var endNode = SyntaxFactory.XmlText(
            SyntaxFactory.TokenList(SyntaxFactory.Token(default(SyntaxTriviaList), SyntaxKind.XmlTextLiteralNewLineToken,
                Environment.NewLine, Environment.NewLine, default(SyntaxTriviaList))));

Also this part is a duplicate:

nodes = xmlParamNodes.Aggregate(nodes, (current, paramNode) => current.AddRange(new SyntaxNode[] { xmlTextElement, paramNode }));
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.