How it works...

In this recipe, we wrote a C# console application based on the Roslyn Compiler API to load and execute diagnostic analyzers from an analyzer assembly and report the diagnostics reported by the analyzers. These operations are very similar to what the C# compiler would do when you compile source files with /analyzer:<%analyzer_file_path%> command line switch. Let's walk through the code and understand how we implemented these operations:

public static void Main(string[] args)
{
// Parse arguments to get analyzer assembly file and source file.
var files = ParseArguments(args);
if (files.analyzerFile == null || files.sourceFile == null)
{
return;
}

// Parse source file and create a compilation.
var compilation = CreateCompilation(files.sourceFile);

// Create compilation with analyzers.
var compilationWithAnalyzers = CreateCompilationWithAnalyzers(files.analyzerFile, compilation);

// Display analyzer diagnostics in the compilation.
DisplayAnalyzerDiagnostics(compilationWithAnalyzers);
}

The Main method invokes individual methods to perform the following operations:

  • ParseArguments to scan for:
    • Analyzer assembly file.
    • Input source file on which the analyzers will be executed.
  • CreateCompilation to create a C# compilation from parsed syntax tree created with the text from the input source file.
  • CreateCompilationWithAnalyzers to create a compilation instance with diagnostic analyzers from the given analyzer assembly file attached to it.
  • DisplayAnalyzerDiagnostics to execute the analyzers to compute the analyzer diagnostics and then display them.

Implementation of ParseArguments and CreateCompilation is identical to the one in the previous recipe, Writing an application based on Compiler Syntax API to parse and transform source files. Refer to the How it works..., section of that recipe for further explanation on these methods.

The CreateCompilationWithAnalyzers method takes the compilation and the analyzer assembly as the parameters:

private static CompilationWithAnalyzers CreateCompilationWithAnalyzers(string analyzerFilePath, Compilation compilation)
{
var analyzerFileReference = new AnalyzerFileReference(analyzerFilePath, new AnalyzerAssemblyLoader());
var analyzers = analyzerFileReference.GetAnalyzers(LanguageNames.CSharp);
var options = new CompilationWithAnalyzersOptions(
new AnalyzerOptions(ImmutableArray<AdditionalText>.Empty),
onAnalyzerException: (exception, analyzer, diagnostic) => throw exception,
concurrentAnalysis: false,
logAnalyzerExecutionTime: false);
return new CompilationWithAnalyzers(compilation, analyzers, options);
}

First, it creates AnalyzerFileReference (http://source.roslyn.io/#q=AnalyzerFileReference) with the analyzer file path and an instance of the analyzer assembly loader (IAnalyzerAssemblyLoader - details later in the section (http://source.roslyn.io/#q=IAnalyzerAssemblyLoader)). It invokes the AnalyzerReference.GetAnalyzers API (http://source.roslyn.io/#q=AnalyzerReference.GetAnalyzers) on this analyzer file reference to load the analyzer assembly with the given analyzer assembly loader, and then create instances of the diagnostic analyzers defined in this assembly.

It creates a default set of CompilationWithAnalyzersOptions (http://source.roslyn.io/#q=CompilationWithAnalyzersOptions) for configuring the analyzer execution. The possible options include:

  • AnalyzerOptions: Analyzer options contain the set of additional non-source text files that are passed to the analyzers. In this recipe, we use an empty set. You can read more about additional files at https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md.
  • onAnalyzerException delegate: Delegate to be invoked when an analyzer throws an exception. In this recipe, we just re-throw this exception.
  • concurrentAnalysis: Flag to control whether the analyzers should be run concurrently or not. In this recipe, we default this to false.
  • logAnalyzerExecutionTime: Flag to control whether the relative execution times for each analyzer should be tracked. If set to true, then this data can be requested for each analyzer using the public API CompilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync (http://source.roslyn.io/#q=CompilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync). This returns an AnalyzerTelemetryInfo, which has a property named ExecutionTime (http://source.roslyn.io/#q=AnalyzerTelemetryInfo.ExecutionTime). In this recipe, we default this to false.

Finally, the method creates and returns a CompilationWithAnalyzers (http://source.roslyn.io/#Microsoft.CodeAnalysis/DiagnosticAnalyzer/CompilationWithAnalyzers.cs,7efdf3edc21e904a) instance with the given compilation, analyzer file reference, and options.

We briefly mentioned our custom AnalyzerAssemblyLoader passed into the AnalyzerFileReference constructor above. It is implemented in our code as follows:

private class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
{
void IAnalyzerAssemblyLoader.AddDependencyLocation(string fullPath)
{
}

Assembly IAnalyzerAssemblyLoader.LoadFromPath(string fullPath)
{
return Assembly.LoadFrom(fullPath);
}
}

This analyzer assembly loader handles loading the analyzer assembly using the .NET APIs for assembly loading on the executing platform. In this recipe, we use .NET Framework API Assembly.LoadFrom (https://msdn.microsoft.com/en-us/library/system.reflection.assembly.loadfrom(v=vs.110).aspx) to load the assembly from the given path.

We ignore the callbacks to add analyzer dependency locations in our custom AnalyzerAssemblyLoader, as our test analyzer assembly has no dependencies. We can enhance this assembly loader to track these locations and handle assembly loading for dependencies.

DisplayAnalyzerDiagnostics takes the CompilationWithAnalyzers instance created earlier and executes the analyzers on the underlying compilation using GetAnalyzerDiagnosticsAsync API. It then walks through all the analyzer diagnostics and outputs the message for each diagnostic, with the line and column information:

private static void DisplayAnalyzerDiagnostics(CompilationWithAnalyzers compilationWithAnalyzers)
{
var diagnostics = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(CancellationToken.None).Result;
Console.WriteLine($"Number of diagnostics: {diagnostics.Length}");
foreach (var diagnostic in diagnostics)
{
Console.WriteLine(diagnostic.ToString());
}

Console.WriteLine();
}

return newSolution;
}
You can invoke the CompilationWithAnalyzers.GetAnalysisResultAsync (http://source.roslyn.io/#q=CompilationWithAnalyzers.GetAnalysisResultAsync) public API to get a more fine-grained view of the analysis results. The returned AnalysisResult (http://source.roslyn.io/#Microsoft.CodeAnalysis/DiagnosticAnalyzer/AnalysisResult.cs,86a401660972cfb8) allows you to get separate syntax, semantic, and compilations diagnostics reported by each diagnostic analyzer and also allows you to get the analyzer telemetry info for each analyzer.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset