Last active
April 13, 2021 10:16
-
-
Save xdaDaveShaw/87643170e5fa97b7da3b to your computer and use it in GitHub Desktop.
Roslyn Based Attribute Remover
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
using Microsoft.CodeAnalysis; | |
void Main() | |
{ | |
var code = @"namespace P | |
{ | |
using NSAlias = Test; | |
class Program | |
{ | |
public void NoAttributes() { } | |
[TestCategory(""Atomic"")] | |
public void JustOneCategoryAttribute() { } | |
[TestMethod, TestCategory(""Atomic"")] | |
public void OnOneLine() { } | |
[TestMethod] | |
[TestCategory(""Atomic"")] | |
public void SeparateAttribute() { } | |
[TestCategory(""Atomic""), TestMethod] | |
public void WithCategoryBefore() { } | |
[TestMethod] | |
[TestCategoryAttribute(""Atomic"")] | |
public void FullAttributeName() { } | |
[TestMethod] | |
[Microsoft.VisualStudio.TestTools.UnitTesting.TestCategoryBaseAttribute(""Atomic"")] | |
public void FullyQualifiedName() { } | |
//[TestMethod] | |
//[TestCategory(""Atomic"")] | |
//public void Commented() { } | |
[TestMethod] | |
[TestCategory(""Database"")] | |
public void ADifferentValue() { } | |
[TestMethod] | |
[TestCategory(""Atomic"")] | |
[TestCategory(""Atomic"")] | |
public void TwoAttributes() { } | |
[TestMethod] | |
[TestCategory(""Atomic""), TestCategory(""Atomic"")] | |
public void TwoAttributesOneLine() { } | |
[TestMethod, TestCategory(""Atomic""), TestCategory(""Atomic"")] | |
public void TwoAttributesOneLineAndOneThatDoesntMatch() { } | |
[NSAlias.TestCategory(""Atomic"")] | |
public void QualifiedAlias() { } | |
[NSAlias::TestCategory(""Atomic"")] | |
public void AliasQualifiedAlias() { } | |
[NSAlias::TestCategory(""Atomic"")] | |
public void NSAlias2() { } | |
[TestMethod, | |
TestCategory(""Atomic"")] | |
public void SomeoneLikesNewLines() { } | |
} | |
}"; | |
var tree = CSharpSyntaxTree.ParseText(code); | |
var rewriter = new AttributeRemoverRewriter( | |
attributeName: "TestCategory", | |
attributeValue: "Atomic"); | |
var rewrittenRoot = rewriter.Visit(tree.GetRoot()); | |
rewrittenRoot.GetText().ToString().Dump(); | |
} | |
public class AttributeRemoverRewriter : CSharpSyntaxRewriter | |
{ | |
public AttributeRemoverRewriter( | |
String attributeName, | |
String attributeValue) | |
{ | |
_attributeName = attributeName; | |
_attributeValue = attributeValue; | |
} | |
private readonly String _attributeName; | |
private readonly String _attributeValue; | |
public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) | |
{ | |
var newAttributes = new SyntaxList<AttributeListSyntax>(); | |
foreach (var attributeList in node.AttributeLists) | |
{ | |
var nodesToRemove = | |
attributeList | |
.Attributes | |
.Where( | |
attribute => | |
AttributeNameMatches(attribute) | |
&& | |
HasMatchingAttributeValue(attribute)) | |
.ToArray(); | |
if (nodesToRemove.Length != attributeList.Attributes.Count) | |
{ | |
//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; | |
} | |
private static SimpleNameSyntax GetSimpleNameFromNode(AttributeSyntax node) | |
{ | |
var identifierNameSyntax = node.Name as IdentifierNameSyntax; | |
var qualifiedNameSyntax = node.Name as QualifiedNameSyntax; | |
return | |
identifierNameSyntax | |
?? | |
qualifiedNameSyntax?.Right | |
?? | |
(node.Name as AliasQualifiedNameSyntax).Name; | |
} | |
private Boolean AttributeNameMatches(AttributeSyntax attribute) | |
{ | |
return | |
GetSimpleNameFromNode(attribute) | |
.Identifier | |
.Text | |
.StartsWith(_attributeName); | |
} | |
private Boolean HasMatchingAttributeValue(AttributeSyntax attribute) | |
{ | |
return | |
attribute | |
.ArgumentList | |
.Arguments | |
.Select(argument => argument.Expression) | |
.OfType<LiteralExpressionSyntax>() | |
.Any(MatchesAttributeValue); | |
} | |
private Boolean MatchesAttributeValue(LiteralExpressionSyntax literalExpression) | |
{ | |
return | |
String.Equals(literalExpression.Token.ValueText, _attributeValue, StringComparison.OrdinalIgnoreCase); | |
} | |
} |
Updated to handle Aliased namespaces on attributes (e.g. NS::Type)
Added changed value comparison to be case insensitive as it matches the test runner.
Updated with help from Josh Varty for when completely removing attributes, or when there is a line break in the attribute declaration.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Part of my Roslyn Based Attribute Remover blog post.