Análisis de código con Roslyn Compiler API en C#
Aprende análisis de código en C# con Roslyn Compiler API, incluyendo árboles de sintaxis y generación de código.
En la plataforma .NET, Roslyn (Microsoft.CodeAnalysis) proporciona los compiladores de C# y VB totalmente implementados en código administrado.
La API del compilador Roslyn permite leer, analizar, modificar e incluso recompilar el código fuente de forma programática.
Esto permite crear herramientas de análisis estático, corrección automática de código (code fix), linters o extensiones personalizadas del IDE.
¿Qué es la API del compilador Roslyn?
Roslyn es el compilador de C# expuesto como una API para los desarrolladores.
Permite realizar mediante código todas las operaciones que normalmente ejecuta el compilador de Visual Studio o dotnet (analizar, compilar, etc.).
Se compone de las siguientes capas principales:
- Syntax API: Proporciona acceso a la estructura sintáctica del código fuente como un árbol.
- Semantic API: Ofrece información semántica como tipos de variables, símbolos y contexto de compilación.
- Compilation API: Gestiona la compilación del código, el análisis de errores/advertencias y la generación del IL (lenguaje intermedio).
Instalación
Para usar la API de Roslyn, agrega el paquete NuGet Microsoft.CodeAnalysis.CSharp:
dotnet add package Microsoft.CodeAnalysis.CSharp
Crear un árbol de sintaxis desde el código fuente
El primer paso consiste en convertir un fragmento de código C# en un árbol de sintaxis (Syntax Tree). Este árbol representa la estructura del código como una jerarquía de nodos.
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
class Program
{
static void Main()
{
string codigo = @"
using System;
class Hola
{
static void Main()
{
Console.WriteLine(""¡Hola Roslyn!"");
}
}";
SyntaxTree arbol = CSharpSyntaxTree.ParseText(codigo);
var raiz = arbol.GetRoot();
Console.WriteLine($"Nodo raíz: {raiz.Kind()}");
foreach (var nodo in raiz.DescendantNodes())
Console.WriteLine(nodo.Kind());
}
}
La salida mostrará tipos de nodos como ClassDeclaration, MethodDeclaration e InvocationExpression.
Modelo semántico: análisis del significado del código
El SemanticModel proporciona información detallada sobre los tipos de variables, firmas de métodos y símbolos. Por ejemplo, puede determinar a qué clase pertenece un método o el tipo de una variable.
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
class Program
{
static void Main()
{
string codigo = @"
using System;
class Test
{
static void Main()
{
string mensaje = ""¡Hola!"";
Console.WriteLine(mensaje);
}
}";
SyntaxTree arbol = CSharpSyntaxTree.ParseText(codigo);
var compilacion = CSharpCompilation.Create("CompilacionPrueba")
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(arbol);
var modelo = compilacion.GetSemanticModel(arbol);
var raiz = arbol.GetRoot();
var variable = raiz.DescendantNodes().OfType<VariableDeclaratorSyntax>().First();
var simbolo = modelo.GetDeclaredSymbol(variable);
Console.WriteLine($"Nombre de la variable: {simbolo.Name}, Tipo: {simbolo.GetSymbolType()?.Name}");
}
}
Este ejemplo obtiene el tipo (string) de la variable mensaje.
Roslyn obtiene esta información de la tabla de símbolos; no es un simple análisis de texto.
Syntax Rewriter: modificar el código
Con Roslyn no solo se puede analizar el código, sino también transformarlo (refactorización).
Puedes usar la clase CSharpSyntaxRewriter para modificar o eliminar nodos específicos.
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 nuevo = node.Token.ValueText.ToUpperInvariant();
return SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(nuevo));
}
return base.VisitLiteralExpression(node);
}
}
class Program
{
static void Main()
{
string codigo = @"class A { void M() { System.Console.WriteLine(""hola""); } }";
SyntaxTree arbol = CSharpSyntaxTree.ParseText(codigo);
var rewriter = new UppercaseRewriter();
var nuevaRaiz = rewriter.Visit(arbol.GetRoot());
Console.WriteLine(nuevaRaiz.ToFullString());
}
}
Este ejemplo convierte todas las cadenas literales a mayúsculas.
Por ejemplo, "hola" se convierte en "HOLA".
Diagnósticos: análisis de errores y advertencias
El objeto Compilation puede devolver los errores y advertencias que ocurren durante la compilación del código.
Esto permite definir reglas personalizadas y crear herramientas de análisis estático.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Linq;
class Program
{
static void Main()
{
var codigo = "class A { void M() { int x = \"incorrecto\"; } }";
var arbol = CSharpSyntaxTree.ParseText(codigo);
var compilacion = CSharpCompilation.Create("Analisis")
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(arbol);
var diagnosticos = compilacion.GetDiagnostics();
foreach (var d in diagnosticos)
Console.WriteLine($"{d.Id}: {d.GetMessage()}");
}
}
Salida: CS0029: No se puede convertir implícitamente el tipo 'string' a 'int'
De esta manera, puedes detectar errores del compilador o aplicar tus propias reglas de análisis.
Ejemplo: Análisis de “Variable no utilizada”
El siguiente ejemplo usa la API de Roslyn para detectar variables que se declaran pero nunca se utilizan en un archivo. Funciona de manera similar a la advertencia verde de “no utilizada” que aparece en 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 codigo = @"
class Test
{
void Metodo()
{
int x = 10;
int y = 20;
Console.WriteLine(x);
}
}";
var arbol = CSharpSyntaxTree.ParseText(codigo);
var compilacion = CSharpCompilation.Create("Analisis")
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(arbol);
var modelo = compilacion.GetSemanticModel(arbol);
var raiz = arbol.GetRoot();
var variables = raiz.DescendantNodes().OfType<VariableDeclaratorSyntax>();
foreach (var v in variables)
{
var simbolo = modelo.GetDeclaredSymbol(v);
var usos = raiz.DescendantNodes()
.OfType<IdentifierNameSyntax>()
.Count(i => i.Identifier.Text == v.Identifier.Text);
if (usos <= 1)
Console.WriteLine($"Advertencia: la variable '{v.Identifier.Text}' no se utiliza.");
}
}
}
Salida: Advertencia: la variable 'y' no se utiliza.
Este ejemplo simple muestra cómo funcionan las herramientas de análisis estático de código.
¿Qué se puede desarrollar con Roslyn?
- Herramientas de análisis de código estático (por ejemplo, reglas de estilo, seguridad o rendimiento)
- Code Analyzer y Code Fix Provider (extensiones de Visual Studio)
- Source Generators (generación de código en tiempo de compilación)
- Herramientas de refactorización o formateo automático de código
- Transformación y reescritura de código (por ejemplo, reemplazar APIs obsoletas)
Rendimiento y buenas prácticas
- Los análisis de Roslyn pueden ser costosos en CPU y memoria; en proyectos grandes se recomienda el análisis incremental.
- Los objetos
SyntaxTreeson inmutables; utiliza los métodosWith...o un rewriter para aplicar cambios. - Evita recorridos innecesarios del árbol en grandes bases de código usando consultas filtradas (
OfType<T>). - Si desarrollas extensiones para IDE, ejecuta los análisis de forma asíncrona en segundo plano.
TL;DR
- La API del compilador Roslyn proporciona una interfaz programática para analizar, transformar y compilar código C#.
- La Syntax API ofrece la estructura sintáctica (árbol de sintaxis), mientras que la Semantic API proporciona la información semántica (tipo/símbolo).
- Con esta API se pueden desarrollar herramientas personalizadas de análisis de código o de refactorización.
- A diferencia de la reflexión (Reflection), Roslyn se centra en el análisis en tiempo de compilación.
- En proyectos grandes, el análisis incremental y la optimización del rendimiento son esenciales.
Artículos relacionados
El concepto de los generadores de código en C# (C# 9+)
Aprende generadores de código en C# para generar código en tiempo de compilación y mejorar el rendimiento.
Principios SOLID en C#
Aplicación de los principios SOLID en C# con ejemplos: código más flexible, mantenible y comprobable.
Reflexión y enlace tardío en C#
Aprende Reflection y late binding en C# para inspeccionar tipos en tiempo de ejecución y crear aplicaciones dinámicas.