Skip to content

TDD friendly test helpers for Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer

License

Notifications You must be signed in to change notification settings

DeNA/Dena.CodeAnalysis.Testing

Repository files navigation

Dena.CodeAnalysis.Testing

NuGet version CI

This library provides TDD friendly DiagnosticAnalyzer test helpers:

  • DiagnosticAnalyzerRunner

    A runner for Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer. The purpose of the runner is providing another runner instead of Microsoft.CodeAnalysis.Analyzer.Testing.AnalyzerVerifier.VerifyAnalyzerAsync.

    Because of the AnalyzerVerifier has several problems:

    1. Using AnalyzerVerifier, it is hard to instantiate analyzer with custom arguments (the custom args may be needed if your analyzer is composed by several smaller analyzer-like components)
    2. AnalyzerVerifier may throw some exceptions because it test Diagnostics. But it should be optional because analyzer-like smaller components may not need it. If it is not optional the tests for the components become to need to wrap try-catch statements for each call of VerifyAnalyzerAsync
  • Test Doubles for DiagnosticAnalyzer

    • NullAnalyzer: it do nothing
    • StubAnalyzer: it analyze codes with a Dena.CodeAnalysis.Testing.AnalyzerActions
    • SpyAnalyzer: it analyze codes and do not report any Diagnostics, but instead it records all actions that registered via Microsoft.CodeAnalysis.Dignostics.AnalysisContext

Requirements

  • .NET Standard 2.1 or later

Usage

Run DiagnosticAnalyzer

var analyzer = new YourAnalyzer();

// The analyzer get intialized and get to call registered actions.
await DiagnosticAnalyzerRunner.Run(
    analyzer,
    @"public static class Foo
{
    public static void Bar()
    {
        System.Console.WriteLine(""Hello, World!"");
    }
}");

Get Diagnostics

var analyzer = new YourAnalyzer();

var diagnostics = await DiagnosticAnalyzerRunner.Run(
    analyzer,
    @"public static class Foo
{
    public static void Bar()
    {
        System.Console.WriteLine(""Hello, World!"");
    }
}");

Assert.AreEqual(0, diagnostics.Length);

Assert Locations

var location = diagnostic.Location;

LocationAssert.HaveTheSpan(
    "/0/Test0.",             // Optional. Skip path assertion if the path not specified,  
    new LinePosition(1, 0),
    new LinePosition(8, 5),
    location
);

Print Diagnostics

var diagnostics = await DiagnosticAnalyzerRunner.Run(
    anyAnalyzer,
    @"
internal static class Foo
{
    internal static void Bar()
    {
        System.Console.WriteLine(""Hello, World!"");
    }
}
ERROR");

Assert.AreEqual(0, diagnostics.Length, DiagnosticsFormatter.Format(diagnostics));
// This message is like:
//
//   // /0/Test0.cs(9,1): error CS0116: A namespace cannot directly contain members such as fields or methods
//   DiagnosticResult.CompilerError(""CS0116"").WithSpan(""/0/Test0.cs"", 9, 1, 9, 6),

Check whether the DiagnosticAnalyzer.Initialize have been called

var spyAnalyzer = new SpyAnalyzer();

var diagnostics = await DiagnosticAnalyzerRunner.Run(
    spyAnalyzer,
    @"public static class Foo
{
    public static void Bar()
    {
        System.Console.WriteLine(""Hello, World!"");
    }
}");

Assert.IsTrue(spyAnalyzer.IsInitialized);

Check recorded actions

var spyAnalyzer = new SpyAnalyzer();

var diagnostics = await DiagnosticAnalyzerRunner.Run(
    spyAnalyzer,
    @"public static class Foo
{
    public static void Bar()
    {
        System.Console.WriteLine(""Hello, World!"");
    }
}");

// CompilationActionHistory hold the Compilation object that given
// to the action registered by AnalysisContext.RegisterCompilationAction.
Assert.AreEqual(1, spyAnalyzer.CompilationActionHistory.Count);

// Other available histories are:
//
//   - spyAnalyzer.CodeBlockActionHistory
//   - spyAnalyzer.CodeBlockStartActionHistory
//   - spyAnalyzer.CompilationActionHistory
//   - spyAnalyzer.CompilationStartActionHistory
//   - spyAnalyzer.OperationActionHistory
//   - spyAnalyzer.OperationBlockActionHistory
//   - spyAnalyzer.OperationBlockStartAction
//   - spyAnalyzer.OperationBlockStartActionHistory
//   - spyAnalyzer.SemanticModelActionHistory
//   - spyAnalyzer.SymbolActionHistory
//   - spyAnalyzer.SymbolStartActionHistory
//   - spyAnalyzer.SyntaxNodeActionHistory
//   - spyAnalyzer.SyntaxTreeActionHistory

Do something in action

var stubAnalyzer = new StubAnalyzer(
    new AnalyzerActions
    {
        CodeBlockStartAction = context => DoSomething()
    }
);

await DiagnosticAnalyzerRunner.Run(
    stubAnalyzer,
    @"public static class Foo
{
    public static void Bar()
    {
        System.Console.WriteLine(""Hello, World!"");
    }
}");

// Other available actions are:
//
//   - stubAnalyzer.CodeBlockAction
//   - stubAnalyzer.CodeBlockStartAction
//   - stubAnalyzer.CompilationAction
//   - stubAnalyzer.CompilationStartAction
//   - stubAnalyzer.OperationAction
//   - stubAnalyzer.OperationBlockAction
//   - stubAnalyzer.OperationBlockStartAction
//   - stubAnalyzer.OperationBlockStartAction
//   - stubAnalyzer.SemanticModelAction
//   - stubAnalyzer.SymbolAction
//   - stubAnalyzer.SymbolStartAction
//   - stubAnalyzer.SyntaxNodeAction
//   - stubAnalyzer.SyntaxTreeAction

DiagnosticAssert Class

AreEqual

DiagnosticAssert.AreEqual assert that collections of Diagnostics for equality.

Throw an assert exception if given collections satisfy the following condition:

Elements that are only contained on one side. The equivalence is based on following properties

  • File path (e.g., path/to/file.cs)
  • Location of the Diagnostic (starting line number, starting character position)-(finishing line number, finishing character position)
  • Identifier of the DiagnosticDescriptor (DDID) (e.g., CS0494)
  • DiagnosticMessage (e.g., The field 'C.hoge' is assigned but its value is never used)

Otherwise, do nothing.

[Test]
public async Task M()
{
    var analyzer = new YourAnalyzer();
    const string testData = @"
class C
{
    string {|hoge|CS0414|The field 'C.hoge' is assigned but its value is never used|} = ""Forgot semicolon string""
}";

    var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnosticFromFile(testData);
    var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source);
    DiagnosticsAssert.AreEqual(expected, actual);
}

Output example

Missing 0 diagnostics, extra 1 diagnostics of all 2 diagnostics:
    extra	/0/Test0.cs: (3,43)-(3,43), CS1002, ; expected

IsEmpty

DiagnosticAssert.IsEmpty assert that the Diagnositc is no exist.

Throw an assert exception if given collections exist any Diagnostic.

The output format and equivalence is the same as DiagnosticAssert.AreEqual.

Otherwise, do nothing.

[Test]
public async Task M()
{
    var analyzer = new YourAnalyzer();
    var source = @"
class C
{
}"; 
    var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source);
    DiagnosticsAssert.IsEmpty(actual);
}

TestDataParser Class

CreateSourceAndExpectedDiagnostics

Create source and expected diagnostic from formatting embedded.

[Test]
public async Task M()
{
    var analyzer = new YourAnalyzer();
    const string testData = @"
class C
{
    string {|hoge|CS0414|The field 'C.hoge' is assigned but its value is never used|} = ""Forgot semicolon string""
}";

    var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnosticFromFile(testData);
}

The testData variable has formatting embedded in the source.

You can parse this format using CreateSourceAndExpectedDiagnosticFromFile and get the source and expected Diagnostics.

  • source
class C
{
    string hoge = "Forgot semicolon string"
}
  • expected Diagnostics

    • Location
      • (3,11)-(3,15)
    • DDID
      • CS0414
    • DiagnosticMessage
      • The field 'C.hoge' is assigned but its value is never used

Specify the part to be reported in the following format.

The format is enclosed in { } and separated by |.

{|source|DDID|DiagnosticMessage|}

License

MIT license

About

TDD friendly test helpers for Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer

Topics

Resources

License

Stars

Watchers

Forks

Languages