Creating a Stand-Alone Code Analyzer
Many of my past posts have covered the process of creating an analyzer that is part of a Visual Studio addin or nuget package. However, there are times that you might want to just create a command line utility to do some analysis of a project. This type of project is very useful in prototyping out ideas or writing a one off utility to get an idea of the state of a solution. It can also be very useful for consultants wanting to run a custom set of tools against customer solutions.
To create a stand alone code analyzer, you can select the Stand-Alone Code Analysis Tool project template from the Extensibility sections of the new project window.
At this point, you just have a basic console application that is referencing the code analysis DLLs. To start analyzing a project, you need to load it up. To do this, we will create a MSBuildWorkSpace and load the solution file:
var ws = Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace.Create();
var soln = ws.OpenSolutionAsync(@"Z:\Dev\Temp\SimpleWinformsTestApp\SimpleWinformsTestApp.sln").Result;
Next we will get the first project from the solution and get the Compilation
object, so that we can access things like the SyntaxTree
.
var proj = soln.Projects.Single();
var compilation = proj.GetCompilationAsync().Result;
Now that we have the Compliation
object we have a myriad of options available to us and we can start doing analysis. In this example, I want to find all classes that inherit from System.Windows.Forms.Form
. So, as I described in the Working With Types in Your Analyzer post, we can get the type by its metadata name form the Compilation
object:
string TEST_ATTRIBUTE_METADATA_NAME = "System.Windows.Forms.Form";
var testAttributeType = compilation.GetTypeByMetadataName(TEST_ATTRIBUTE_METADATA_NAME);
To get at the classes declared in the project, we need to loop over all of the SyntaxTrees
in the Compilation
and then find all of the ClassDeclarationSyntax
nodes declared in those trees:
foreach (var tree in compilation.SyntaxTrees)
{
var classes = tree.GetRoot().DescendantNodesAndSelf().Where(x => x.IsKind(SyntaxKind.ClassDeclaration));
foreach (var c in classes)
{
// ...
}
// ...
}
Once we have all of the classes, we need to determine if the class inherits from the testAttributeType
we declared earlier in the analysis. The ClassDeclarationSyntax
has a BaseList
property which defines all of the base classes for the class. So from these base classes, we can get the type information (from the SemanticModel
) and compare it to the testAttributeType
to see if it is a Windows Form:
var classDec = (ClassDeclarationSyntax)c;
var bases = classDec.BaseList;
if (bases?.Types != null)
{
foreach (var b in bases.Types)
{
var nodeType = compilation.GetSemanticModel(tree).GetTypeInfo(b.Type);
// Is the node a System.Windows.Forms.Form?
if (nodeType.Type.Equals(testAttributeType))
{
Console.WriteLine(classDec.Identifier.Text);
}
}
}
The full program is as follows:
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace StandAloneCodeAnalysis
{
class Program
{
static void Main(string[] args)
{
var ws = Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace.Create();
var soln = ws.OpenSolutionAsync(@"Z:\Dev\Temp\SimpleWinformsTestApp\SimpleWinformsTestApp.sln").Result;
var proj = soln.Projects.Single();
var compilation = proj.GetCompilationAsync().Result;
string TEST_ATTRIBUTE_METADATA_NAME = "System.Windows.Forms.Form";
var testAttributeType = compilation.GetTypeByMetadataName(TEST_ATTRIBUTE_METADATA_NAME);
foreach (var tree in compilation.SyntaxTrees)
{
var classes = tree.GetRoot().DescendantNodesAndSelf().Where(x => x.IsKind(SyntaxKind.ClassDeclaration));
foreach (var c in classes)
{
var classDec = (ClassDeclarationSyntax)c;
var bases = classDec.BaseList;
if (bases?.Types != null)
{
foreach (var b in bases.Types)
{
var nodeType = compilation.GetSemanticModel(tree).GetTypeInfo(b.Type);
// Is the node a System.Windows.Forms.Form?
if (nodeType.Type.Equals(testAttributeType))
{
Console.WriteLine(classDec.Identifier.Text);
}
}
}
}
}
Console.ReadKey();
}
}
}
As you can see, creating an analyzer that works as a standalone project is not too different from creating an analyzer that runs inside of Visual Studio. There are some basic differences and you are required to setup a little more yourself, but the underlying logic does not change much.