12
\$\begingroup\$

I converted a C# analyzer for removing empty argument lists from attributes to be a C# and VB.NET analyzer:

public override void Initialize(AnalysisContext context)
{
    context.RegisterSyntaxNodeAction(AnalyzeCSharpSymbol, CSharpSyntaxKind.Attribute);
    context.RegisterSyntaxNodeAction(AnalyzeVisualBasicSymbol, VisualBasicSyntaxKind.Attribute);
}

private void AnalyzeCSharpSymbol(SyntaxNodeAnalysisContext context)
{
    var attributeExpression = context.Node as CSharpAttributeSyntax;
    
    // attribute must have arguments
    // if there are no parenthesis, the ArgumentList is null
    // if there are empty parenthesis, the ArgumentList is empty
    if (attributeExpression?.ArgumentList == null || attributeExpression.ArgumentList.Arguments.Any())
    {
        return;
    }

    context.ReportDiagnostic(Diagnostic.Create(Rule, attributeExpression.GetLocation()));
}

private void AnalyzeVisualBasicSymbol(SyntaxNodeAnalysisContext context)
{
    var attributeExpression = context.Node as VisualBasicAttributeSyntax;

    // attribute must have arguments
    // if there are no parenthesis, the ArgumentList is null
    // if there are empty parenthesis, the ArgumentList is empty
    if (attributeExpression?.ArgumentList == null || attributeExpression.ArgumentList.Arguments.Any())
    {
        return;
    }

    context.ReportDiagnostic(Diagnostic.Create(Rule, attributeExpression.GetLocation()));
}

Here are my custom using definitions for this class:

using CSharpSyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind;
using VisualBasicSyntaxKind = Microsoft.CodeAnalysis.VisualBasic.SyntaxKind;
using CSharpAttributeSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.AttributeSyntax;
using VisualBasicAttributeSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeSyntax;

This seems very WET, but after thinking it over, this seems to be the best way because only what these methods do is the same, the details are just a coincidence.

This is the code fix. Again, I provided overloaded methods:

private Task<Solution> RemoveEmptyArgumentListAsync(Document document, SyntaxNode root, SyntaxNode statement)
{
    SyntaxNode newRoot = null;

    if (statement is Microsoft.CodeAnalysis.CSharp.Syntax.AttributeSyntax)
    {
        newRoot = RemoveEmptyArgumentList(root, (Microsoft.CodeAnalysis.CSharp.Syntax.AttributeSyntax)statement);
    }
    else if (statement is Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeSyntax)
    {
        newRoot = RemoveEmptyArgumentList(root, (Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeSyntax)statement);
    }

    var newDocument = document.WithSyntaxRoot(newRoot);
    return Task.FromResult(newDocument.Project.Solution);
}

private SyntaxNode RemoveEmptyArgumentList(SyntaxNode root, Microsoft.CodeAnalysis.CSharp.Syntax.AttributeSyntax attributeExpression)
{
    return root.RemoveNode(attributeExpression.ArgumentList, SyntaxRemoveOptions.KeepNoTrivia);
}

private SyntaxNode RemoveEmptyArgumentList(SyntaxNode root, Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeSyntax attributeExpression)
{
    return root.RemoveNode(attributeExpression.ArgumentList, SyntaxRemoveOptions.KeepNoTrivia);
}

And more custom usings:

using CSharpAttributeSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.AttributeSyntax;
using VisualBasicAttributeSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeSyntax;

And here are some sample unit tests:

C#

[TestMethod]
public void AttributeWithEmptyArgumentList_AttributeWithEmptyArgumentList()
{
    var original = @"
using System;

namespace ConsoleApplication1
{
    class MyClass
    {   
        [Obsolete()]
        void Method(string input)
        {
        }
    }
}";

    var result = @"
using System;

namespace ConsoleApplication1
{
    class MyClass
    {   
        [Obsolete]
        void Method(string input)
        {
        }
    }
}";

    VerifyDiagnostic(original, AttributeWithEmptyArgumentListAnalyzer.Rule.MessageFormat.ToString());
    VerifyFix(original, result);
}

Visual Basic

[TestMethod]
public void AttributeWithEmptyArgumentList_AttributeWithEmptyArgumentList()
{
    var original = @"
Module Module1

    <Obsolete()>
    Sub Foo()

    End Sub

End Module";

    var result = @"
Module Module1

    <Obsolete>
    Sub Foo()

    End Sub

End Module";

    VerifyDiagnostic(original, AttributeWithEmptyArgumentListAnalyzer.Rule.MessageFormat.ToString());
    VerifyFix(original, result);
}

For reference, these are the test cases that must pass:

Attributes with parameters (no results):

[Obsolete("test")] and <Obsolete("test")>

Attributes without parameters or parenthesis (no results):

[Obsolete] and <Obsolete>

Attributes without parameters and with parenthesis (results):

[Obsolete()] and <Obsolete()>

\$\endgroup\$
3
\$\begingroup\$

It seems you could combine the two methods into one by making it generic:

private void AnalyzeSymbol<TSyntax>(SyntaxNodeAnalysisContext context)
    where TSyntax : class
{
    var attributeExpression = context.Node as TSyntax;

    // attribute must have arguments
    // if there are no parenthesis, the ArgumentList is null
    // if there are empty parenthesis, the ArgumentList is empty
    if (attributeExpression?.ArgumentList == null || attributeExpression.ArgumentList.Arguments.Any())
    {
        return;
    }

    context.ReportDiagnostic(Diagnostic.Create(Rule, attributeExpression.GetLocation()));
}

If CSharpAttributeSyntax and VisualBasicAttributeSyntax share a common interface or base class (looks like it), use that to refine the generic type parameter constraint, as a class constraint merely ensures the type is a reference type. Otherwise you'd have to cast to dynamic for it to work.

Same here:

private SyntaxNode RemoveEmptyArgumentList<TSyntax>(SyntaxNode root, TSyntax attributeExpression)
     where TSyntax : WhateverTheInterfaceOrBaseTypeIs
{
    return root.RemoveNode(attributeExpression.ArgumentList, SyntaxRemoveOptions.KeepNoTrivia);
}

The call site already had the responsibility of knowing which method to call: now you can just change that to knowing which generic type parameter to use.


Side note, this assignment is redundant:

SyntaxNode newRoot = null;

A reference type is initialized to null anyway, so this suffices:

SyntaxNode newRoot;
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy