Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to remove all member attribute but leave an empty line?

Tags:

c#

roslyn

I've written a CSharpSyntaxRewriter that I am using to remove attributes from methods, but I am struggling to keep anything from before the attribute (upto the previous method) when I remove all attributes from a method.

This works perfectly for methods with more than one attribute, but not for just one.

Here's a minimal repro:

void Main()
{
    var code = @"namespace P
{
    class Program
    {
        public void NoAttributes() { }
        //???
        [TestCategory(""Atomic"")]
        public void OneAtt1() { }

        [TestCategory(""Atomic"")]
        public void OneAtt2() { }

        [TestMethod, TestCategory(""Atomic"")]
        public void TwoAtts() { }
    }
}";
    var tree = CSharpSyntaxTree.ParseText(code);
    var rewriter = new AttributeRemoverRewriter();

    var rewrittenRoot = rewriter.Visit(tree.GetRoot());

    Console.WriteLine(rewrittenRoot.GetText().ToString());
}

public class AttributeRemoverRewriter : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitAttributeList(AttributeListSyntax attributeList)
    {
        var nodesToRemove =
            attributeList
            .Attributes
            .Where(att => (att.Name as IdentifierNameSyntax).Identifier.Text.StartsWith("TestCategory"))
            .ToArray();

        if (nodesToRemove.Length == attributeList.Attributes.Count)
        {
            //Remove the entire attribute
            return
                attributeList
                .RemoveNode(attributeList, SyntaxRemoveOptions.KeepNoTrivia);
        }
        else
        {
            //Remove just the matching ones recursively
            foreach (var node in nodesToRemove)
                return
                    VisitAttributeList(attributeList.RemoveNode(node, SyntaxRemoveOptions.KeepNoTrivia));
        }

        return
            base.VisitAttributeList(attributeList);
    }
}

Full version is here on my gist (before anyone points out any other issues).

Expected output is:

namespace P
{
    class Program
    {
        public void NoAttributes() { }
        //???

        public void OneAtt1() { }

        public void OneAtt2() { }

        [TestMethod]
        public void TwoAtts() { }
    }
}

Actual output is:

namespace P
{
  class Program
  {
    public void NoAttributes() { }
    public void OneAtt1() { }
    public void OneAtt2() { }

    [TestMethod]
    public void TwoAtts() { }
  }
}

Any ideas on what I need to do to keep the whitespace (or even comments!!)?

I've messed around with every combination of Trivia I can think of. Changing the SyntaxRemoveOptions results in NullReferenceExceptions inside the Roslyn codebase and using the *Trivia extension methods results in the attributes not been removed anymore - just whitespace.

like image 435
DaveShaw Avatar asked Jul 31 '15 15:07

DaveShaw


1 Answers

Like I said in the comments, it looks like a bug in Roslyn to me. You can report it and use the following workaround if you'd like.

I just tried rewriting at the method level instead of at the attribute level. (You could use a similar approach on properties)

public class AttributeRemoverRewriter : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        var newAttributes = new SyntaxList<AttributeListSyntax>();

        foreach (var attributeList in node.AttributeLists)
        {
            var nodesToRemove =
            attributeList
           .Attributes
           .Where(att => (att.Name as IdentifierNameSyntax).Identifier.Text.StartsWith("TestCategory"))
           .ToArray();

            if (nodesToRemove.Length == attributeList.Attributes.Count)
            {
                //Do not add the attribute to the list. It's being removed completely.
            }
            else
            {
                //We want to remove only some of the attributes
                var newAttribute = (AttributeListSyntax)VisitAttributeList(attributeList.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia));
                newAttributes = newAttributes.Add(newAttribute);
            }
        }

        //Get the leading trivia (the newlines and comments)
        var leadTriv = node.GetLeadingTrivia();
        node = node.WithAttributeLists(newAttributes);

        //Append the leading trivia to the method
        node = node.WithLeadingTrivia(leadTriv);
        return node;
    }
}

Here's the output I get. You could further filter leadTriv if you'd like to remove comments.

My output

Note that this doesn't cover certain... perverse situations:

[TestCategory(""Atomic"")]
#if Debug
#endif
/*Trivia in unfortunate places*/
[TestCategory("test")]
public void OneAtt2() { }

You'll lose the trivia between the attributes. Trivia is one of the hardest things to get right when building rewriters.

like image 157
JoshVarty Avatar answered Sep 20 '22 14:09

JoshVarty