Cargando...

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:


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?


Rendimiento y buenas prácticas


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