Working with Namespace Names in a Code Analyzer
Let's imagine we wanted to create an analyzer that compared the name of the namespace to the path of the folder in which the file it resides. If the namespace doesn't match the folder structure, then we should raise a diagnostic. As we have seen in a previous post, you can get at the file name for the code you are analyzing from a SyntaxTreeAction
. To start, we'll register a new SyntaxTreeAction
and get the file path. We can also take that path and turn it into something that resembles a namespace declaration:
#!cs
compilationSyntax.RegisterSyntaxTreeAction((syntaxTreeContext) =>
{
var filePath = syntaxTreeContext.Tree.FilePath;
if (filePath == null)
return;
var parentDirectory = System.IO.Path.GetDirectoryName(filePath);
// This will only work on windows and is not very robust.
var parentDirectoryWithDots = parentDirectory.Replace("\\", ".");
}
Now that we have the path to use for comparison, we can get the namespaces in the file and then perform the comparison. We can get all of the namespaces in a SyntaxTree
by using the OfType
extension method and find all NamespaceDeclarationSyntax
node types. Note: Make sure you use DescendantNodes
and not ChildNodes
to ensure you get all nested namespace declarations.
#!cs
var namespaceNodes = syntaxTreeContext.Tree.GetRoot().DescendantNodes().OfType<NamespaceDeclarationSyntax>();
We can loop over those nodes and compare the name to the folder path:
#!cs
foreach (var ns in namespaceNodes)
{
var name = ????
if (!parentDirectoryWithDots.EndsWith(name, StringComparison.OrdinalIgnoreCase))
{
syntaxTreeContext.ReportDiagnostic(Diagnostic.Create(
Rule, ns.Name.GetLocation(), parentDirectoryWithDots));
}
}
You might be tempted to just use the ns.ToFullString()
method to get the name of the namespace, however, this does not work for nested namespaces. For example, if we have the code:
#!cs
namespace Foo
{
namespace Bar
{
}
}
The full namespace for Bar
is Foo.Bar
, but the ns.ToFullString()
method will only return Bar
. To properly get the full name, we need to ask our friend the SemanticModel
for the Symbol
for this SyntaxNode
and then we can get the DisplayString
from there. Getting the semantic model requires a little massaging of our registration code.
The SemanticModel
is available off of the Compilation
object. As we discussed in the "Working with Types in Your Analyzer" post, you can access the Compilation
object by registering for a CompilationStartAction
and then registering your other actions from that context.
So our registration code, can be modified to:
#!cs
context.RegisterCompilationStartAction((compilationContext) =>
{
compilationContext.RegisterSyntaxTreeAction((syntaxTreeContext) =>
{
var semModel = compilationContext.Compilation.GetSemanticModel(syntaxTreeContext.Tree);
//...
});
});
Then the code to check the namespace name can use the semantic model to get the INamespaceSymbol
and get the display string from that.
#!cs
foreach (var ns in namespaceNodes)
{
var symbolInfo = semModel.GetDeclaredSymbol(ns) as INamespaceSymbol;
var name = symbolInfo.ToDisplayString();
if (!parentDirectoryWithDots.EndsWith(name, StringComparison.OrdinalIgnoreCase))
{
syntaxTreeContext.ReportDiagnostic(Diagnostic.Create(
Rule, ns.Name.GetLocation(), parentDirectoryWithDots));
}
}
So, in the end, the full analyzer method looks like this:
#!cs
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction((compilationContext) =>
{
compilationContext.RegisterSyntaxTreeAction((syntaxTreeContext) =>
{
var semModel = compilationContext.Compilation.GetSemanticModel(syntaxTreeContext.Tree);
var filePath = syntaxTreeContext.Tree.FilePath;
if (filePath == null)
return;
var parentDirectory = System.IO.Path.GetDirectoryName(filePath);
// This will only work on windows and is not very robust.
var parentDirectoryWithDots = parentDirectory.Replace("\\", ".");
var namespaceNodes = syntaxTreeContext.Tree.GetRoot().DescendantNodes().OfType<NamespaceDeclarationSyntax>();
foreach (var ns in namespaceNodes)
{
var symbolInfo = semModel.GetDeclaredSymbol(ns) as INamespaceSymbol;
var name = symbolInfo.ToDisplayString();
if (!parentDirectoryWithDots.EndsWith(name, StringComparison.OrdinalIgnoreCase))
{
syntaxTreeContext.ReportDiagnostic(Diagnostic.Create(
Rule, ns.Name.GetLocation(), parentDirectoryWithDots));
}
}
});
});
}
And that is everything. I think this post really shows some of the power of using the SyntaxTree
and the SemanticModel
together to put together an analyzer that can process real world code. This post was inspired by a question I answered on StackOverflow and took a few iterations to get right. I thought I would share the process of how I got there so others can learn from it. If you have done something written a similar analyzer or have great examples of using the SemanticModel
and SyntaxTree
together, let's talk about it in the comments below.