Code Analysis with Roslyn Compiler API in C#
Learn code analysis in C# using Roslyn Compiler API, including syntax trees, analyzers, and code generation scenarios.
On the .NET platform, Roslyn (Microsoft.CodeAnalysis) provides C# and VB compilers implemented entirely in managed code.
The Roslyn Compiler API allows you to read, analyze, modify, and even recompile source code programmatically.
This enables the creation of static analysis tools, automatic code fixes, linters, and custom IDE extensions.
What Is the Roslyn Compiler API?
Roslyn is the C# compiler exposed as an API for developers.
It allows you to perform all the operations that Visual Studio or the dotnet compiler does (parse, analyze, compile) directly in your own code.
It consists of the following main layers:
- Syntax API: Provides access to the syntactic structure of the code as a tree.
- Semantic API: Provides meaning-based information such as variable types, symbols, and compilation context.
- Compilation API: Handles code compilation, warning/error analysis, and IL (Intermediate Language) generation.
Installation
To use the Roslyn API, add the Microsoft.CodeAnalysis.CSharp NuGet package:
dotnet add package Microsoft.CodeAnalysis.CSharp
Creating a Syntax Tree from Source Code
The first step is to convert a C# code snippet into a Syntax Tree. This tree represents the structural components of the code as nodes.
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
class Program
{
static void Main()
{
string code = @"
using System;
class Hello
{
static void Main()
{
Console.WriteLine(""Hello Roslyn!"");
}
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
Console.WriteLine($"Root node: {root.Kind()}");
foreach (var node in root.DescendantNodes())
Console.WriteLine(node.Kind());
}
}
The output lists node types such as ClassDeclaration, MethodDeclaration, and InvocationExpression.
Semantic Model: Analyzing Code Semantics
The SemanticModel provides detailed information about variable types, method signatures, and symbols. For example, it can determine which class a method belongs to or the type of a variable.
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
class Program
{
static void Main()
{
string code = @"
using System;
class Test
{
static void Main()
{
string message = ""Hello!"";
Console.WriteLine(message);
}
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var compilation = CSharpCompilation.Create("TestCompilation")
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);
var root = tree.GetRoot();
var variable = root.DescendantNodes().OfType<VariableDeclaratorSyntax>().First();
var symbol = model.GetDeclaredSymbol(variable);
Console.WriteLine($"Variable name: {symbol.Name}, Type: {symbol.GetSymbolType()?.Name}");
}
}
This example retrieves the type (string) of the message variable.
Roslyn extracts this information from the symbol table — it’s not simple text parsing.
Syntax Rewriter: Modifying Code
Roslyn enables not only code analysis but also code transformation (refactoring).
You can use the CSharpSyntaxRewriter class to modify or remove specific syntax nodes.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
class UppercaseRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode VisitLiteralExpression(LiteralExpressionSyntax node)
{
if (node.IsKind(SyntaxKind.StringLiteralExpression))
{
string updated = node.Token.ValueText.ToUpperInvariant();
return SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(updated));
}
return base.VisitLiteralExpression(node);
}
}
class Program
{
static void Main()
{
string code = @"class A { void M() { System.Console.WriteLine(""hello""); } }";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var rewriter = new UppercaseRewriter();
var newRoot = rewriter.Visit(tree.GetRoot());
Console.WriteLine(newRoot.ToFullString());
}
}
This example converts all string literals to uppercase.
For instance, "hello" becomes "HELLO".
Diagnostics: Error and Warning Analysis
The Compilation object can return errors and warnings that occur during code compilation.
This allows you to define custom rules and build static analysis tools.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Linq;
class Program
{
static void Main()
{
var code = "class A { void M() { int x = \"wrong\"; } }";
var tree = CSharpSyntaxTree.ParseText(code);
var compilation = CSharpCompilation.Create("Analysis")
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(tree);
var diagnostics = compilation.GetDiagnostics();
foreach (var d in diagnostics)
Console.WriteLine($"{d.Id}: {d.GetMessage()}");
}
}
Output: CS0029: Cannot implicitly convert type 'string' to 'int'
This way, you can capture compiler errors or enforce your own analysis rules.
Example: “Unused Variable” Analysis
The following example uses the Roslyn API to detect variables that are declared but never used in a file. It works similarly to the green “unused variable” warning in Visual Studio.
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
class Program
{
static void Main()
{
string code = @"
class Test
{
void Method()
{
int x = 10;
int y = 20;
Console.WriteLine(x);
}
}";
var tree = CSharpSyntaxTree.ParseText(code);
var compilation = CSharpCompilation.Create("Analysis")
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(tree);
var model = compilation.GetSemanticModel(tree);
var root = tree.GetRoot();
var variables = root.DescendantNodes().OfType<VariableDeclaratorSyntax>();
foreach (var v in variables)
{
var symbol = model.GetDeclaredSymbol(v);
var usages = root.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.Count(i => i.Identifier.Text == v.Identifier.Text);
if (usages <= 1)
Console.WriteLine($"Warning: Variable '{v.Identifier.Text}' is not used.");
}
}
}
Output: Warning: Variable 'y' is not used.
This simple example demonstrates the logic behind static code analysis tools.
What Can Be Built with Roslyn?
- Static code analysis tools (e.g., style, security, performance rules)
- Code Analyzers & Code Fix Providers (Visual Studio extensions)
- Source Generators (compile-time code generation)
- Refactoring or automatic code formatting tools
- Code transformation and rewriting (e.g., replacing deprecated APIs)
Performance and Best Practices
- Roslyn analyses can be CPU and memory intensive; for large projects, prefer incremental analysis.
SyntaxTreeobjects are immutable; useWith...methods or a rewriter to apply changes.- To avoid unnecessary tree traversal in large codebases, use filtered queries (
OfType<T>). - If developing IDE extensions, run analyses asynchronously in the background.
TL;DR
- The Roslyn Compiler API provides a programmatic interface to analyze, transform, and compile C# code.
- Syntax API provides syntactic structure (Syntax Tree), while Semantic API provides meaning (type/symbol) information.
- Custom code analyzers or refactoring tools can be built using this API.
- Unlike Reflection, Roslyn focuses on compile-time analysis.
- Incremental analysis and performance optimization are crucial for large projects.
Related Articles
Reflection and Late Binding in C#
Learn Reflection and late binding in C# to inspect types at runtime and build flexible, dynamic applications.
SOLID Principles with C#
Applying SOLID principles in C# with examples: building flexible, maintainable, and testable code.
The Concept of Source Generators in C# (C# 9+)
Learn source generators in C# to generate code at compile time and improve performance with modern development techniques.