Roslyn analyzers: How to
This post is part of the series 'Roslyn Analyzers'. Be sure to check out the rest of the blog posts of the series!
- Writing a Roslyn analyzer
- Writing language-agnostic Roslyn Analyzers using IOperation
- Working with types in a Roslyn analyzer
- Referencing an analyzer from a project
- Packaging a Roslyn Analyzer with NuGet package references
- Multi-targeting a Roslyn analyzer
- Roslyn analyzers: How to (this post)
- How to test a Roslyn analyzer
- Useful resources to write Roslyn Analyzers
This post contains a collection of samples for writing Roslyn analyzers. It's a living document that I will update over time.
#Find an IMethodSymbol from a compilation
var type = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")
var methods = type.GetMembers("GetAwaiter").OfType<IMethodSymbol>();
If you want a specific method when there are multiple overloads, you can use the DocumentationCommentId
to get it directly. This is sometimes easier than filtering the methods manually. For example, to get the StringBuilder.Append(char)
method, you can use the following code:
DocumentationCommentId.GetFirstSymbolForDeclarationId("M:System.Text.StringBuilder.Append(System.Char)", context.Compilation) as IMethodSymbol
The documentation comment ID syntax is documented on GitHub. If you are unsure about the documentation comment id, you can use JetBrains Rider to find it. Place the cursor on the method name, and use the "Copy Reference" feature. Then, select "XML-doc ID":
#Check if a symbol is accessible from an assembly
You can check if a symbol is accessible from a specific assembly using the IsSymbolAccessibleWithin
method. For example, the following code checks if the System.Threading.Tasks.Task
type is accessible from the assembly represented by the compilation object:
var assembly = compilation.Assembly;
var symbol = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task");
var isAccessible = compilation.IsSymbolAccessibleWithin(symbol, assembly);
#Access a configuration from the .editorconfig file
Analyzers can read configuration data from the .editorconfig
file. This is useful to pass configuration data to the analyzer. For example, you could exclude a specific type from the analysis. The configuration data is read from the .editorconfig
file using the AnalyzerConfigOptionsProvider
class. The following code shows how to read the configuration data in an analyzer:
public override void Initialize(AnalysisContext context)
{
// context.Options is available in all RegisterXyzAction methods
context.RegisterOperationAction(context =>
{
var syntaxTree = context.Operation.Syntax.SyntaxTree;
// Get options for the current syntax tree
// .editorconfig files are read from the directory of the syntax tree
var options = context.Options.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree);
// Get the value of the my_analyzer.excludedTypes option
if(options.TryGetValue("my_analyzer.configuration", out string configurationValue))
{
// todo
}
});
}
#Access additional files
Analyzers can read additional files that are passed to the compilation. This is useful to pass configuration data to the analyzer. For example, you can pass a file with a list of types that should be ignored by the analyzer. The file can be a text file, a JSON file, or any other file format. It's up to the analyzer to read the file and interpret its content. Additional files are often added from the .csproj
file using the AdditionalFiles
item group. For example, the following code adds a file named ConfigurationFile.txt
to the compilation:
<Project>
<ItemGroup>
<AdditionalFiles Include="ConfigurationFile.txt" />
</ItemGroup>
</Project>
Here's an example of how to read the content of the file in an analyzer:
public override void Initialize(AnalysisContext context)
{
// context.Options is available in all RegisterXyzAction methods
context.RegisterCompilationStartAction(context =>
{
var files = context.Options.AdditionalFiles;
foreach(var file in files)
{
_ = file.Path;
var sourceText = file.GetText(context.CancellationToken);
if (sourceText is null)
continue;
foreach (var line in sourceText.Lines)
{
if (line.Span.IsEmpty)
continue;
var lineText = line.ToString();
// Do whatever you need to do with the line
}
}
});
}
#Run an analyzer only if a specific symbol available
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(context =>
{
var symbol = context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task");
if (symbol is null)
return;
context.RegisterOperationAction(...);
});
}
#Check if a method is an operator
public static bool IsOperator(this IMethodSymbol methodSymbol)
{
return methodSymbol.MethodKind is MethodKind.UserDefinedOperator or MethodKind.Conversion;
}
#Get the namespace of a type
To get the namespace, you can recursively get the containing namespace of the type. Depending on the context, you can also use the SymbolDisplayFormat
class to format the namespace. For example, to get the fully qualified namespace of a type, you can use the following code:
ITypeSymbol type = ...;
var format = SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted);
type.ContainingNamespace?.ToDisplayString(format)
#Detect which version of .NET is used
Roslyn doesn't provide a direct way to detect which version of .NET is used in a project. However, you can use the Compilation
object to check the references and determine the version of .NET. For example, you can check if the System.Object
type is available in the compilation:
var type = compilation.GetSpecialType(SpecialType.System_Object);
var version = type.ContainingAssembly.Identity.Version;
var isNet6OrLater = version.Major >= 6;
#Analyze the body of methods decorated with an attribute
context.RegisterOperationBlockStartAction(context =>
{
var attribute = context.Compilation.GetTypeByMetadataName("TestAttribute");
if (attribute == null)
return;
var symbol = context.OwningSymbol;
if (symbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attribute)))
{
// Register a scoped action to analyze the method body
context.RegisterOperationAction(context => { }, OperationKind.Invocation);
}
});
#Analyze documentation comments (XML comments)
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration);
}
private void AnalyzeMethod(SyntaxNodeAnalysisContext context)
{
var syntax = (MethodDeclarationSyntax)context.Node;
if (!syntax.HasStructuredTrivia)
return;
foreach (var trivia in syntax.GetLeadingTrivia())
{
var structure = trivia.GetStructure();
if (structure is null)
continue;
if (structure is not DocumentationCommentTriviaSyntax documentation)
continue;
foreach (var child in documentation.ChildNodes())
{
if (child is XmlElementSyntax elementSyntax)
{
if (elementSyntax.StartTag.Name.LocalName.Text == "summary")
{
// Do something with the summary
}
}
}
}
}
#Get data flow
The AnalyzeDataFlow
method can be used to get data flow information for a specific syntax node. The data flow result contains the symbols that are read, written, declared, and captured inside or outside the syntax node.
var semanticModel = context.SemanticModel;
var dataFlow = semanticModel.AnalyzeDataFlow(syntax);
if (dataFlow.Succeeded)
{
// dataFlow.ReadInside
// dataFlow.WrittenInside
// dataFlow.VariablesDeclared
// dataFlow.CapturedInside
// dataFlow.UsedLocalFunctions
// etc.
}
#Get control flow
The control flow result contains the basic blocks, entry points, exit points, and control flow edges.
var semanticModel = context.SemanticModel;
var controlFlow = semanticModel.AnalyzeControlFlow(statementSyntax);
if(controlFlow.Succeeded)
{
// controlFlow.EntryPoints
// controlFlow.ExitPoints
// controlFlow.StartPointIsReachable
// controlFlow.EndPointIsReachable
// controlFlow.ReturnStatements
}
#Get C# language version
You can get the C# language version from the compilation options:
SyntaxNode syntaxNode = ...;
if (syntaxNode.SyntaxTree.Options is CSharpParseOptions options)
{
_ = options.LanguageVersion;
}
#Check if a symbol is accessible from outside the assembly
static bool IsVisibleOutsideOfAssembly([NotNullWhen(true)] this ISymbol? symbol)
{
if (symbol is null)
return false;
if (symbol.DeclaredAccessibility != Accessibility.Public &&
symbol.DeclaredAccessibility != Accessibility.Protected &&
symbol.DeclaredAccessibility != Accessibility.ProtectedOrInternal)
{
return false;
}
if (symbol.ContainingType is null)
return true;
return IsVisibleOutsideOfAssembly(symbol.ContainingType);
}
#Check if a member implements an interface member
static bool IsInterfaceImplementation(this IMethodSymbol symbol)
{
if (symbol.ExplicitInterfaceImplementations.Length > 0)
return true;
return IsInterfaceImplementation((ISymbol)symbol);
}
static bool IsInterfaceImplementation(this IPropertySymbol symbol)
{
if (symbol.ExplicitInterfaceImplementations.Length > 0)
return true;
return IsInterfaceImplementation((ISymbol)symbol);
}
static bool IsInterfaceImplementation(this IEventSymbol symbol)
{
if (symbol.ExplicitInterfaceImplementations.Length > 0)
return true;
return IsInterfaceImplementation((ISymbol)symbol);
}
static bool IsInterfaceImplementation(ISymbol symbol)
{
var interfaces = symbol.ContainingType.AllInterfaces;
foreach (var interfaceSymbol in interfaces)
{
foreach (var interfaceMember in interfaceSymbol.GetMembers())
{
var impl = symbol.ContainingType.FindImplementationForInterfaceMember(interfaceMember);
var areEqual = SymbolEqualityComparer.Default.Equals(symbol, impl);
if (areEqual)
{
return true;
}
}
}
return false;
}
#Remove trailing whitespace from a syntax node
public static T WithoutTrailingSpacesTrivia<T>(this T syntaxNode) where T : SyntaxNode
{
if (!syntaxNode.HasTrailingTrivia)
return syntaxNode;
var newTrivia = syntaxNode
.GetTrailingTrivia()
.Reverse()
.SkipWhile(t => t.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.WhitespaceTrivia))
.Reverse();
return syntaxNode.WithTrailingTrivia(newTrivia);
}
#Support multiple versions of Roslyn
When writing an analyzer or a source generator, you might want to support multiple versions of Roslyn. This can be challenging because the APIs might change between versions. To support multiple versions of Roslyn, you can compile the analyzer or source generator against multiple versions of the Roslyn SDK. Then, you can create a NuGet package with a folder per Roslyn version. Roslyn will load the highest compatible version of the analyzer.
Documentation: https://github.com/dotnet/sdk/issues/20355
Do you have a question or a suggestion about this post? Contact me!