3

TL;DR: In Roslyn, I want to know how to test checking the type of an attribute against a known type.

I've followed Microsoft's tutorial for creating a C# code analyser, which basically involves using the Visual Studio template project called "Analyzer with code fix (.NET Standard)". That template gives you an analyser project (including a working analyser and a "code fix" for it), and a unit test project (with some classes to provide a framework for testing the analyser and "code fix") and a VSIX project. Very nice: thank you Microsoft.

I'm now trying to write my first analyser, and I'm falling at the first hurdle. I'm trying to create an analyser which checks that methods which are attributed using certain types are given the same name as the attribute type name. I'm planning to use this to ensure that the NUnit [Setup] method is called "SetUp", [TearDown] method is called "TearDown", etc... methods are given the same names as the attribute that is attached to the method.

I had looked at Roslyn Check Type of an Attribute; but the answer isn't actually phrased in the context of the question (it doesn't mention how to compare desiredSymbol with attr - I assume they wouldn't be 'ReferenceEquals'); and anyway, it turned out that my situation was being compounded by a secondary issue...

When I looked at x.AttributeClass with a debugger while running the unit test, the type name of the attribute was preceded by "ErrorType", so I assumed it hadn't been able to resolve the type; so I altered the References that are being used within the testing framework provided in the template project, by changing "DiagnosticVerifier.Helper.cs" "CreateProject" method by adding the following 2 lines in the relevant places:

    private static readonly MetadataReference NUnitReference = MetadataReference.CreateFromFile(typeof(NUnit.Framework.SetUpAttribute).Assembly.Location);

and

            .AddMetadataReference(projectId, NUnitReference);

That fixed the items in symbolsToCheckFor, which now resolve correctly rather than being null.

It's hard to provide a MCVE for something like this; but here's what I have for my DiagnosticAnalyser:

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MethodAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rules.CO1000_UseAttributeNameForSetUpAndTearDown);

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
    }

    private static void AnalyzeMethod(SymbolAnalysisContext context)
    {
        var symbol = (IMethodSymbol)context.Symbol;

        var symbolsToCheckFor = new[]
        {
            context.Compilation.GetTypeByMetadataName("NUnit.Framework.SetUpAttribute"),
            context.Compilation.GetTypeByMetadataName("NUnit.Framework.TearDownAttribute"),
            context.Compilation.GetTypeByMetadataName("NUnit.Framework.OneTimeSetUpAttribute"),
            context.Compilation.GetTypeByMetadataName("NUnit.Framework.OneTimeTearDownAttribute")
        };

        var first = symbol.GetAttributes().FirstOrDefault(x => symbolsToCheckFor.Contains(x.AttributeClass));
        if (first != null)
        {
            var expectedName = Regex.Replace(first.AttributeClass.Name, "Attribute$", string.Empty);
            if (symbol.Name != expectedName)
            {
                var diagnostic = Diagnostic.Create(Rules.CO1000_UseAttributeNameForSetUpAndTearDown, symbol.Locations[0], symbol.Name, expectedName);

                context.ReportDiagnostic(diagnostic);
            }
        }
    }

and here is the NUnit unit test that I would be hoping to pass when the above code is fixed:

    [TestCase("SetUp")]
    [TestCase("TearDown")]
    [TestCase("OneTimeSetUp")]
    [TestCase("OneTimeTearDown")]
    [TestCase("NUnit.Framework.SetUp")]
    [TestCase("NUnit.Framework.TearDown")]
    [TestCase("NUnit.Framework.OneTimeSetUp")]
    [TestCase("NUnit.Framework.OneTimeTearDown")]
    public void Diagnostic_triggered_correctly(string attributeName)
    {
        const string faultyMethodName = "MyMethod";
        var test = $@"
using NUnit.Framework;

namespace ConsoleApplication1
{{
    [TestFixture]
    public class MyTests
    {{
        [{attributeName}]
        public void {faultyMethodName}()
        {{
        }}
    }}
}}";
        string expectedMethodName = attributeName;
        int lastDot = expectedMethodName.LastIndexOf('.');
        if (lastDot != -1)
        {
            expectedMethodName = expectedMethodName.Substring(lastDot + 1);
        }

        var expected = new DiagnosticResult
        {
            Id = Rules.CO1000_UseAttributeNameForSetUpAndTearDown.Id,
            Message = $"Method name '{faultyMethodName}' should match the attribute name '{expectedMethodName}'",
            Severity = DiagnosticSeverity.Warning,
            Locations =
                new[] {
                    new DiagnosticResultLocation("Test0.cs", 11, 15)
                }
        };

        VerifyCSharpDiagnostic(test, expected);
    }

EDIT I've now discovered that the code I posted a month ago basically worked when used for real. The issue is totally with the testing... so I need help finding out how to make the test pass, given that the code actually works when it is actually used.

Any help gratefully received, thanks.

Richardissimo
  • 5,596
  • 2
  • 18
  • 36

0 Answers0