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;
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.