¿Qué es Roslyn? ── Leer, corregir y generar código C# desde la perspectiva del compilador

· Actualizado el: · · .NET, CSharp, Roslyn, Analyzer, SourceGenerator, Compiler, StaticAnalysis, CodeGeneration, Aprovechamiento de activos existentes

1. Lo primero que hay que entender

Hay más situaciones de las que parece en las que conviene procesar código fuente C#. Por ejemplo, estas tareas.

Prohibir el uso de una API específica
Detectar mecánicamente estilos de código antiguos
Recopilar una lista de métodos y clases
Investigar las dependencias de todo un proyecto
Generar código repetitivo en tiempo de compilación
Advertir en tiempo de compilación sobre el uso incorrecto de una biblioteca propia
Llevar a cabo de forma segura una sustitución o migración a gran escala

En estos casos, es tentador abrir simplemente los archivos *.cs y procesarlos con búsqueda de texto o expresiones regulares.

Sin embargo, C# no es una cadena de texto. Aunque estas dos líneas se parezcan, en cuanto a significado son cosas distintas.

Console.WriteLine("Hello");
MyCompany.Logging.Console.WriteLine("Hello");

Además, el nombre Console puede referirse a un tipo distinto.

using Console = MyCompany.Logging.Console;

Console.WriteLine("Hello");

Vistas como texto, todas parecen Console.WriteLine. Pero, desde el punto de vista del compilador, no se puede saber si se trata de System.Console.WriteLine o de otro tipo sin resolver el nombre.

Aquí es donde entra Roslyn. Roslyn es la plataforma que hace posible que las aplicaciones y herramientas accedan, a través de una API, a la información que maneja internamente el compilador de C# y Visual Basic.

Dicho de forma sencilla, gracias a Roslyn es posible tratar el código C# de las siguientes maneras.

Leerlo como sintaxis, no como una cadena de texto
Leerlo por su significado, no por su apariencia
Leerlo como un proyecto o una solución, no como un único archivo
Emitir advertencias, propuestas de corrección y código generado a partir de lo leído

En este artículo se ordena la visión general de Roslyn, el Syntax Tree, el SemanticModel, el Workspace, los Analyzers, los Source Generators y sus aplicaciones prácticas.

El código que aparece en este artículo se publica en GitHub como un conjunto completo de ejemplos que se pueden compilar y ejecutar (una biblioteca que trabaja con Syntax Tree / SemanticModel, un Analyzer que advierte sobre DateTime.Now, un Source Generator, una demostración que analiza una solución completa y pruebas unitarias que comprueban falsos positivos y falsos negativos).

roslyn-dotnet-compiler-platform - komurasoft-blog-samples (GitHub)

Lectura según el objetivo

El artículo tiene 40 capítulos en total. Sin embargo, los capítulos que conviene leer son completamente distintos según se trate de un lector que solo quiere «activar los Analyzers existentes y aprovechar sus beneficios» o de uno que quiere «crear sus propios Analyzers». A continuación lo organizamos según el objetivo.

Objetivo Capítulos a leer Notas
Activar primero los Analyzers existentes y aprovechar solo sus beneficios Cap. 13 → Cap. 14 → Cap. 29 No hace falta crear ninguno. El eje central es .editorconfig
Entender qué es Roslyn Cap. 2 → Cap. 4 → Cap. 5 → Cap. 8 → Cap. 9 Parte conceptual. Apenas hace falta leer código
Escribir una herramienta de investigación de activos existentes Cap. 5 → Cap. 8 → Cap. 11 → Cap. 21 → Cap. 23 → Cap. 36 El Syntax Tree y el Workspace son los protagonistas
Crear un Analyzer con reglas propias Cap. 12 → Cap. 15 → Cap. 28 → Cap. 29 → Cap. 31 A partir de aquí se pasa al «lado de quien crea»
Llegar hasta distribuir un Code Fix Lo anterior más Cap. 16 → Cap. 25  
Plantearse usar Source Generator Cap. 17 → Cap. 18 → Cap. 19 → Cap. 20 → Cap. 30 Leer antes «para qué sirve / para qué no» de los capítulos 19 y 20 agiliza la decisión
Aplicarlo a activos de .NET Framework Cap. 32 → Cap. 33  
Decidir por dónde empezar Cap. 35 (orden de introducción) → Cap. 39 (checklist práctico)  

No es necesario leer seguidos la parte conceptual (capítulos 2 a 11) y la parte práctica (capítulos 13 a 37). Si no piensa crear nada, la forma más rentable de aprovechar este artículo es leer solo los capítulos 13 y 14 y poner en orden el .editorconfig.

El primer paso ── qué hay que preparar

Lo que se necesita depende de «qué se va a crear». Como dispersarlo por todo el texto dificultaría la lectura, aquí se resume primero en una tabla. Los detalles se tratan en cada capítulo correspondiente.

Qué se crea Forma del proyecto Paquetes NuGet principales
Herramienta de investigación de un solo archivo Aplicación de consola normal (.NET actual) Microsoft.CodeAnalysis.CSharp
Herramienta de investigación de toda una solución Aplicación de consola normal (.NET actual) Lo anterior más Microsoft.CodeAnalysis.Workspaces.MSBuild y Microsoft.Build.Locator
Analyzer / Code Fix Biblioteca de clases, netstandard2.0 Microsoft.CodeAnalysis.CSharp, Microsoft.CodeAnalysis.Analyzers
Source Generator Biblioteca de clases, netstandard2.0 Microsoft.CodeAnalysis.CSharp
Pruebas de Analyzer / Generator Proyecto de pruebas (.NET actual) Microsoft.CodeAnalysis.CSharp.CodeFix.Testing, Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing

Los que requieren atención en cuanto al framework de destino son el Analyzer y el Source Generator. Como estos no se ejecutan en la propia aplicación, sino que son cargados y ejecutados por el compilador del lado del usuario, lo habitual es dirigirlos a netstandard2.0. De hecho, la plantilla que ofrece Visual Studio se llama «Analyzer with Code Fix (.NET Standard)». El criterio para elegir la versión se trata en el capítulo 33.

Si va a empezar desde una plantilla en Visual Studio, instale antes el .NET Compiler Platform SDK. Puede marcarlo en la pestaña «Componentes individuales» del Visual Studio Installer, bajo «.NET Compiler Platform SDK» (dentro de la sección «Compilers, build tools, and runtimes»), o seleccionarlo como componente opcional de la carga de trabajo «Desarrollo de extensiones de Visual Studio». Aunque elija esa carga de trabajo, este componente no se instala automáticamente, así que márquelo de forma explícita.

Una vez instalado, si busca «Analyzer» en File > New > Project, encontrará «Analyzer with Code Fix (.NET Standard)». Esta plantilla crea de una sola vez cinco proyectos: el propio Analyzer, el Code Fix, uno para empaquetarlo como NuGet, uno de pruebas unitarias y un VSIX para comprobar el funcionamiento. El VSIX queda como proyecto de inicio predeterminado, y al ejecutarlo se abre una segunda instancia de Visual Studio con su Analyzer ya cargado.

2. Qué es Roslyn

El nombre oficial de Roslyn es .NET Compiler Platform. Es la implementación del compilador de C# y Visual Basic, y al mismo tiempo un conjunto de API para crear herramientas de análisis de código.

Tradicionalmente, el compilador solía tratarse como una «caja negra» de este tipo.

Se introduce el código fuente
El compilador lo procesa
Sale un DLL o un EXE

Normalmente, los desarrolladores no podían acceder a la información que el compilador generaba internamente.

Sin embargo, en realidad el compilador no se limita a convertir texto en código máquina o en IL: durante la compilación genera información como esta.

Esta cadena es una declaración de clase
Este identificador es una variable local
Esta llamada a método apunta a este método de este tipo
El tipo de retorno de esta expresión es string
Este código tiene un error de sintaxis
Esta referencia apunta a un tipo del ensamblado A
Este using en realidad no se usa

Roslyn pone esta información a disposición de los desarrolladores. Por eso Roslyn no es un simple compilador, sino una plataforma para comprender el código.

3. Qué se puede hacer con Roslyn

Con Roslyn se pueden hacer principalmente estas cosas.

Análisis sintáctico de C# / VB
Análisis semántico de tipos y métodos
Obtención de información de compilación
Análisis de un proyecto o de una solución completa
Creación de Analyzers propios
Creación de Code Fixes
Creación de Source Generators
Creación de herramientas de refactorización
Generación de código
Transformación de código

Dicho de forma algo más práctica, estos son los usos habituales.

Convertir en advertencia de compilación el uso de una API prohibida
Listar los puntos donde se usa una API antigua
Comprobar la regla de nombres de los métodos async
Detectar un manejo incompleto de IDisposable
Guiar hacia el uso correcto del framework propio
Generar en tiempo de compilación el código de un DTO o de mapeo
Generar código repetitivo a partir de un archivo de configuración o de atributos
Apoyar la investigación de una migración de .NET Framework a .NET

Lo importante de Roslyn es que permite escribir el «procesamiento del código fuente de C#» sobre la misma base que usa el propio compilador.

Leer C# con expresiones regulares o con un analizador propio llega enseguida a sus límites. Por ejemplo, no es sencillo tratar correctamente elementos como estos.

using alias
Métodos de extensión
partial class
partial method
global using
Anotaciones nullable
Tipos genéricos
Resolución de sobrecargas
Compilación condicional
Directivas de preprocesador
Reescritura que conserva comentarios y espacios en blanco

Roslyn ofrece una API para tratar todos estos elementos conforme a la especificación del lenguaje C#.

4. Roslyn separa la «sintaxis» del «significado»

Para entender Roslyn, lo primero que conviene separar son estos dos conceptos.

Sintaxis: cómo está escrito el código
Significado: a qué se refiere ese código

Veamos, por ejemplo, este código.

Console.WriteLine(message);

Visto como sintaxis, tiene esta forma.

Sentencia de expresión
  Expresión de llamada
    Expresión de acceso a miembro
      Identificador Console
      Identificador WriteLine
    Argumento message

Pero con esto solo no se conoce el significado. No se puede determinar únicamente a partir de la sintaxis a qué tipo pertenece Console, qué sobrecarga es WriteLine ni cuál es el tipo de message.

Para verlo como significado, se necesita toda esta información.

El estado de los using
Los ensamblados referenciados
Las definiciones de tipos dentro del mismo proyecto
Las referencias a otros proyectos
La inferencia de tipos
La resolución de sobrecargas
La versión del lenguaje
El contexto nullable

En Roslyn, esta diferencia también se refleja en la propia API.

Syntax Tree     : representa la sintaxis del código
SemanticModel   : representa el significado de la sintaxis
Compilation     : representa la información global necesaria para compilar
Workspace       : gestiona la solución, los proyectos y los documentos

Tener clara esta distinción hace que Roslyn resulte mucho más comprensible.

5. Qué es el Syntax Tree

El Syntax Tree es un árbol que representa la estructura sintáctica del código fuente. Supongamos, por ejemplo, que tenemos este código.

class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}

Visto desde Roslyn, este código tiene, a grandes rasgos, esta estructura.

CompilationUnit
  ClassDeclaration: User
    PropertyDeclaration: Name
    MethodDeclaration: Rename
      Parameter: name
      Block
        ExpressionStatement
          AssignmentExpression

El Syntax Tree no es simplemente el texto dividido línea por línea, sino una estructura organizada en elementos sintácticos de C#: clases, métodos, propiedades, expresiones, sentencias, argumentos, operadores, etc.

Veamos un ejemplo sencillo.

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);
var root = tree.GetCompilationUnitRoot();

var methods = root
    .DescendantNodes()
    .OfType<MethodDeclarationSyntax>();

foreach (var method in methods)
{
    Console.WriteLine(method.Identifier.Text);
}

Este código busca declaraciones de métodos dentro del código fuente y muestra el nombre de cada método. En este ejemplo se obtiene Rename.

Lo importante es que no se busca void mediante una búsqueda de texto, sino que se busca la «declaración de método» como elemento sintáctico de C#.

6. Node, Token y Trivia

Al trabajar con el Syntax Tree, aparecen con frecuencia estos tres términos.

SyntaxNode
SyntaxToken
SyntaxTrivia

SyntaxNode

SyntaxNode es una unidad sintáctica. Por ejemplo, estas.

Declaración de clase
Declaración de método
Declaración de propiedad
Sentencia if
Sentencia for
Expresión de asignación
Expresión de llamada
Expresión lambda

En la sintaxis de C#, un Node es un elemento que además puede tener elementos hijos.

SyntaxToken

SyntaxToken es la unidad mínima que compone la sintaxis. Por ejemplo, estos elementos.

La palabra clave class
La palabra clave public
El identificador User
El identificador Rename
{ o }
; o ,
Un literal de cadena
Un literal numérico

El Token es el elemento que se encuentra en los extremos del árbol de sintaxis.

SyntaxTrivia

SyntaxTrivia es información que no interviene directamente en el análisis semántico habitual, pero que es necesaria para reproducir el código fuente. Por ejemplo, esto.

Espacios en blanco
Saltos de línea
Comentarios
Directivas de preprocesador

Gracias a este Trivia, Roslyn puede tratar el código fuente con alta fidelidad, incluyendo comentarios y espacios en blanco.

El Trivia es muy importante al realizar formateo de código, refactorización o reescrituras mecánicas.

Si solo se tratara de construir un AST, podría parecer razonable descartar los comentarios. Sin embargo, en la transformación de código en la práctica, es importante no romper los comentarios ni los saltos de línea.

Formas de ver el árbol

Hasta aquí se ha explicado Node, Token y Trivia mediante texto, pero el Syntax Tree es de esos temas que se entienden más rápido viéndolos con código real. Hay dos formas de hacerlo.

1. El Syntax Visualizer de Visual Studio

Se puede usar una vez instalado el .NET Compiler Platform SDK mencionado en el capítulo 1.

View > Other Windows > Syntax Visualizer

Al colocar el cursor sobre el código en el editor, el nodo correspondiente se resalta dentro del árbol. Y a la inversa, al seleccionar un nodo del árbol, se selecciona el fragmento correspondiente en el editor. La visualización distingue por colores: el SyntaxNode en azul, el SyntaxToken en verde y el SyntaxTrivia en rojo. Los tres tipos explicados en este capítulo se ven directamente representados por esos colores.

Si además quiere usar la vista en forma de grafo, instale también el DGML editor desde «Componentes individuales» del Visual Studio Installer (dentro de la sección «Code tools»).

2. sharplab.io

sharplab.io es un patio de juegos para C# / VB / F# que se usa solo desde el navegador. Al escribir código en el panel izquierdo, en el derecho puede alternar entre el resultado de la descompilación a C#, el IL y el código nativo generado por el JIT.

Para ver el propio Syntax Tree, el Syntax Visualizer es más directo, pero sharplab es útil para comprobar «en qué se expande realmente este azúcar sintáctico». Como se puede ver a simple vista, por ejemplo, cómo una sentencia using se convierte en try / finally, resulta más fácil sentir en la práctica la idea del capítulo 4 de que Roslyn trata el «significado» y no la «apariencia».

Vale la pena mencionar un consejo práctico para cuando escriba un Analyzer. Comprobar antes, en el Syntax Visualizer, el nombre del tipo del nodo objetivo (por ejemplo, MemberAccessExpressionSyntax) y su SyntaxKind, y solo después escribir el código, reduce bastante el trabajo repetido. El hecho de que el Analyzer del capítulo 15 especifique SyntaxKind.SimpleMemberAccessExpression es precisamente información que se puede confirmar siguiendo este procedimiento.

7. El Syntax Tree es inmutable

El Syntax Tree de Roslyn es inmutable. Es decir, en lugar de modificar directamente el árbol de sintaxis obtenido, se crea un nuevo árbol de sintaxis con los cambios aplicados.

Por ejemplo, incluso cuando se quiere cambiar el nombre de un método, no se modifica in situ el MethodDeclarationSyntax existente.

var newMethod = oldMethod.WithIdentifier(
    SyntaxFactory.Identifier("NewName"));

De esta manera se crea un nuevo nodo.

Ser inmutable tiene varias ventajas.

Es fácil de manejar desde varios hilos
Permite tratar de forma segura las instantáneas mientras se edita en el IDE
Es fácil generar diferencias
Es fácil comparar el estado antes y después del cambio

Al principio puede resultar algo incómodo. Sin embargo, en un entorno donde varios procesos —el IDE, la compilación, un Analyzer, un Source Generator— hacen referencia al mismo código de forma simultánea, la inmutabilidad se convierte en una gran ventaja.

8. Qué es el SemanticModel

Con el Syntax Tree solo se conoce la apariencia del código. Para conocer el significado se usa el SemanticModel.

Consideremos, por ejemplo, este código.

Console.WriteLine("Hello");

A partir del Syntax Tree se sabe que existen los identificadores Console y WriteLine. Pero no se sabe si eso se refiere a System.Console.WriteLine(string?) o a un método de otro tipo.

Con el SemanticModel se puede obtener «a qué símbolo se resolvió este nodo sintáctico».

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello");
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);

var compilation = CSharpCompilation.Create(
    assemblyName: "Sample",
    syntaxTrees: new[] { tree },
    references: new[]
    {
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
    });

var semanticModel = compilation.GetSemanticModel(tree);
var root = tree.GetCompilationUnitRoot();

var invocation = root
    .DescendantNodes()
    .OfType<InvocationExpressionSyntax>()
    .First();

var symbolInfo = semanticModel.GetSymbolInfo(invocation);
var method = (IMethodSymbol?)symbolInfo.Symbol;

Console.WriteLine(method?.ContainingType.ToDisplayString());
Console.WriteLine(method?.Name);

De esta manera se puede obtener a qué método se resolvió realmente Console.WriteLine.

La fortaleza de Roslyn está en poder usar, además de la sintaxis, el resultado de la resolución de nombres realizada por el compilador.

9. Qué es un Symbol

En Roslyn, los tipos, métodos, propiedades, campos, argumentos y variables locales, entre otros, se tratan como Symbol.

A continuación se listan las interfaces más representativas.

INamedTypeSymbol  : clases, estructuras, interfaces, etc.
IMethodSymbol     : métodos
IPropertySymbol   : propiedades
IFieldSymbol      : campos
IParameterSymbol  : argumentos
ILocalSymbol      : variables locales
INamespaceSymbol  : espacios de nombres

El Symbol no representa la apariencia en el código fuente, sino el significado resuelto por el compilador.

Por ejemplo, estos dos fragmentos de código tienen una apariencia distinta.

System.Console.WriteLine("Hello");
using System;

Console.WriteLine("Hello");

Pero si ambos se refieren al mismo System.Console.WriteLine, el análisis semántico de Roslyn los trata como el mismo símbolo de método.

Gracias a esta propiedad, se pueden hacer comprobaciones como estas.

Si esta llamada corresponde realmente a una API prohibida por la empresa
Si este tipo implementa una interfaz determinada
Si este método es async
Si este valor de retorno es nullable
Si este atributo está realmente aplicado
Si esta clase hereda de una clase base determinada

No se trata de una simple búsqueda de texto, sino de un análisis basado en el juicio del propio compilador.

10. Qué es Compilation

Compilation reúne la información necesaria para compilar un programa en C# o Visual Basic.

En concreto, contiene información como esta.

El conjunto de SyntaxTree
Los ensamblados referenciados
Las opciones de compilación
La versión del lenguaje
Los símbolos definidos
La información de tipos y miembros
La información de diagnóstico

Si solo se trata de leer un único archivo como sintaxis, basta con SyntaxTree. Pero para resolver tipos o referencias hace falta Compilation.

Por ejemplo, cuando se quiere hacer algo como esto.

Saber a qué tipo pertenece el método de esta llamada
Saber si esta clase implementa IDisposable
Saber a qué tipo de atributo corresponde realmente este atributo
Saber el tipo de retorno de esta expresión
Obtener los errores o advertencias de compilación

Nada de esto se puede determinar solo con la sintaxis. Es necesario tener en cuenta también las referencias del proyecto y las opciones de compilación.

11. Qué es Workspace

Cuando se quiere trabajar con una solución o un proyecto completo, y no con un único archivo, se usa Workspace.

Workspace maneja estas unidades.

Solution
Project
Document

Por ejemplo, cuando se quiere cargar todos los proyectos de una solución completa y analizar todos sus documentos.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync("Sample.sln");

foreach (var project in solution.Projects)
{
    Console.WriteLine(project.Name);

    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        Console.WriteLine($"  {document.Name}: {root?.DescendantNodes().Count()} nodes");
    }
}

Al crear una herramienta así, se puede usar para investigar una base de código existente o para apoyar una migración.

Por ejemplo, para usos como estos.

Convertir a CSV la lista de llamadas a una API específica
Listar los puntos donde se usa un espacio de nombres antiguo
Crear una lista de la API pública
Investigar las dependencias entre proyectos
Investigar infracciones de convenciones de código en una solución enorme
Realizar una transformación mecánica de código

El Analyzer es un mecanismo que funciona integrado con el IDE y la compilación. En cambio, una herramienta de consola que use Workspace es adecuada para investigaciones o migraciones masivas.

Ambos se parecen, pero conviene distinguir cuándo usar cada uno.

12. Las formas típicas de usar Roslyn

Las formas de usar Roslyn se dividen a grandes rasgos en cuatro.

1. Usarlo como biblioteca
2. Crear un Analyzer
3. Crear un Code Fix
4. Crear un Source Generator

Cada una tiene un propósito distinto.

Usarlo como biblioteca

Se llama a la API de Roslyn desde una aplicación de consola propia o una herramienta interna.

Los usos adecuados son estos.

Investigación de la base de código
Transformación masiva
Recopilación de métricas
Apoyo a la migración
Elaboración de informes

En esta forma, la herramienta se puede ejecutar en el momento que se desee. Como no necesita funcionar mientras se escribe en el IDE, resulta más fácil admitir procesos relativamente pesados.

Crear un Analyzer

El Analyzer es un mecanismo que analiza el código y emite advertencias o errores.

Por ejemplo, se pueden crear reglas como estas.

No usar DateTime.Now, sino DateTimeOffset.UtcNow
El nombre de un método async debe terminar en Async
No llamar a la API de inicialización de una biblioteca en el orden incorrecto
No usar un espacio de nombres determinado en código nuevo
Prohibir el uso de Task.Result / Wait

El Analyzer puede ejecutarse en Visual Studio o durante la compilación. Permite detectar de forma mecánica las convenciones del equipo o el uso correcto de una biblioteca, sin depender de la memoria de quien revisa el código.

Crear un Code Fix

El Code Fix es un mecanismo que propone una corrección para el problema detectado por el Analyzer.

Es fácil de imaginar como la corrección que se puede aplicar desde el icono de la bombilla de Visual Studio.

Supongamos, por ejemplo, que el Analyzer detecta este código.

DateTime.Now

El Code Fix puede proponer una corrección como esta.

DateTimeOffset.UtcNow

La fortaleza del Code Fix es que no se limita a «emitir una advertencia», sino que automatiza también «una forma segura de corregirla».

Crear un Source Generator

El Source Generator es un mecanismo que genera código en tiempo de compilación y lo añade a esa misma compilación.

Por ejemplo, tiene usos como estos.

Generar código repetitivo a partir de una clase con un atributo aplicado
Generar accesores con seguridad de tipos a partir de un archivo de configuración
Generar el código de mapeo de un DTO
Generar código para un serializador
Generar código de conversión entre un enum y una cadena
Generar código de enrutamiento o de registro en el contenedor de DI

En algunos casos, un proceso que recopilaba información en tiempo de ejecución mediante Reflection se puede sustituir por código generado en tiempo de compilación. Esto puede reducir el costo de arranque y mejorar la compatibilidad con AOT.

13. El Analyzer se puede usar como «revisión de código automática»

En la práctica, resulta fácil de entender pensar en el Analyzer como una «revisión de código automática».

En las revisiones de código, a veces se repiten las mismas observaciones una y otra vez.

No use esta API
Este nombre de método no cumple la convención
Este catch está tragando la excepción
Esta comprobación de null es innecesaria
Esta llamada tiene un problema de rendimiento

Si una persona lo señala cada vez, parte de ese trabajo se puede convertir en un Analyzer.

Las reglas especialmente adecuadas para un Analyzer son estas.

Se puede juzgar con claridad si algo está bien o mal
Hay pocas excepciones
El criterio de corrección está definido
Se quiere que todo el equipo la respete
Aparece con frecuencia en las revisiones
Se puede permitir que detenga la compilación

Por el contrario, también hay cosas para las que el Analyzer no es adecuado.

El juicio depende del contexto y es difícil
Requiere una decisión de diseño
Hay demasiadas excepciones
La opinión varía según la persona
Hay tantas advertencias que nadie las revisa

El Analyzer es una herramienta poderosa. Precisamente por serlo, si se introduce en exceso, empeora la experiencia de desarrollo. Es recomendable empezar con un número reducido de reglas importantes.

14. Empezar por los Analyzers incluidos en el .NET SDK

Antes de escribir un Analyzer propio, lo más realista es revisar primero los Analyzers incluidos en el .NET SDK.

En los proyectos de .NET 5 en adelante, el análisis de código de .NET está habilitado de forma predeterminada.

Entre los ID de diagnóstico más habituales hay estas dos familias.

CAxxxx : calidad de código, fiabilidad, rendimiento, seguridad, etc.
IDExxxx: estilo de código, asistencia del IDE, etc.

La severidad del Analyzer se puede ajustar en .editorconfig.

# Ejemplo: convertir un using no utilizado en advertencia
dotnet_diagnostic.IDE0005.severity = warning

# Ejemplo: convertir CA2000 en error
dotnet_diagnostic.CA2000.severity = error

También se puede activar o endurecer desde el archivo de proyecto.

<PropertyGroup>
  <EnableNETAnalyzers>true</EnableNETAnalyzers>
  <AnalysisLevel>latest</AnalysisLevel>
  <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>

Al introducirlo en un proyecto existente, al principio pueden aparecer muchísimas advertencias. En ese caso, es mejor avanzar de forma gradual en lugar de convertirlas todas en errores de golpe.

Primero, conocer el número total de advertencias
Adoptar la política de no aumentarlas en el código nuevo
Convertir en warning solo las reglas importantes
Convertir en error solo las reglas que realmente se quieren hacer cumplir
Reducir las infracciones existentes de forma planificada

Conviene pensar en el Analyzer propio como un complemento que, sobre esta base, añade «las reglas específicas de la propia empresa».

15. Una imagen mínima de un Analyzer

El Analyzer busca una sintaxis o un símbolo específico y reporta un Diagnostic.

Consideremos, por ejemplo, un Analyzer que advierte sobre el uso de DateTime.Now.

En código de producción real habría que tratar con cuidado la resolución de tipos y los casos excepcionales, pero la imagen mínima sería la siguiente.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoDateTimeNowAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id: "CMP001",
        title: "No usar DateTime.Now directamente",
        messageFormat: "En lugar de DateTime.Now, considere usar DateTimeOffset.UtcNow u otra alternativa según el uso",
        category: "Usage",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        => ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();

        context.RegisterSyntaxNodeAction(
            AnalyzeMemberAccess,
            SyntaxKind.SimpleMemberAccessExpression);
    }

    private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
    {
        var memberAccess = (MemberAccessExpressionSyntax)context.Node;

        if (memberAccess.Name.Identifier.Text != "Now")
        {
            return;
        }

        var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
        if (symbol is not IPropertySymbol propertySymbol)
        {
            return;
        }

        if (propertySymbol.Name == "Now" &&
            propertySymbol.ContainingType.ToDisplayString() == "System.DateTime")
        {
            var diagnostic = Diagnostic.Create(Rule, memberAccess.GetLocation());
            context.ReportDiagnostic(diagnostic);
        }
    }
}

Lo importante de este ejemplo es que no se busca DateTime.Now simplemente como una cadena de texto.

Se usa el SemanticModel para comprobar si realmente se refiere a System.DateTime.Now.

Por eso resulta más difícil generar falsos positivos con algo distinto como esto.

MyCompany.DateTime.Now

En los Analyzers se suele usar este flujo: reducir los candidatos por sintaxis y luego confirmar con el análisis semántico si realmente son el objetivo.

Buscar candidatos rápidamente por Syntax
Determinar con precisión mediante SemanticModel
Reportar la ubicación y el mensaje mediante Diagnostic

16. El Code Fix es un mecanismo que también distribuye «cómo corregir»

El Analyzer encuentra el problema y el Code Fix propone cómo corregirlo.

Por ejemplo, al detectar DateTime.Now, se puede proponer una corrección como esta.

Sustituirlo por DateTimeOffset.UtcNow
Sustituirlo por una abstracción como IClock.Now

Sin embargo, el Code Fix debe diseñarse con cuidado. No siempre basta con cambiar DateTime.Now por DateTimeOffset.UtcNow. Cuando se quiere mostrar la hora local y cuando se quiere manejar una hora para guardar o comparar, el tipo adecuado y el tratamiento de la zona horaria son distintos.

Por eso, el Code Fix es adecuado cuando se cumplen condiciones como estas.

El significado después de la corrección es claro
El efecto secundario es pequeño
Se puede corregir de forma segura con una transformación mecánica
Es fácil de verificar para una persona

Por ejemplo, este tipo de correcciones encaja bien con un Code Fix.

Sustituir el nombre de una API antigua por el de la nueva
Añadir un using que falta
Cambiar un nombre para que siga la convención
Añadir un atributo
Eliminar un argumento innecesario

Por otro lado, en las correcciones que requieren una decisión de diseño, a veces es mejor limitarse a la advertencia en lugar de aplicar una corrección automática.

17. El Source Generator es «generación de código en tiempo de compilación»

El Source Generator se ejecuta en tiempo de compilación y añade el código C# generado a esa misma compilación.

El flujo, a grandes rasgos, es este.

Leer el código fuente del usuario
Examinar los atributos o las definiciones de tipos
Generar el código C# necesario
Añadir el código generado al conjunto que se va a compilar

Este es un ejemplo sencillo de Source Generator.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public sealed class BuildInfoGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(static ctx =>
        {
            var source = """
namespace Generated;

public static class BuildInfo
{
    public static string Tool => "Roslyn Source Generator";
}
""";

            ctx.AddSource(
                "BuildInfo.g.cs",
                SourceText.From(source, Encoding.UTF8));
        });
    }
}

En un proyecto que referencia este Generator, se puede usar este tipo aunque no se haya escrito ningún archivo fuente para él.

Console.WriteLine(Generated.BuildInfo.Tool);

El Source Generator no reescribe el código existente del usuario. Lo que puede hacer es generar código fuente adicional y hacerlo participar en la compilación.

Por eso, es más fácil de entender pensándolo así.

No transforma el código existente
Examina el código existente y crea código adicional

Si lo que se quiere es reescribir en bloque el código existente, en lugar de un Source Generator conviene considerar una herramienta de migración basada en Roslyn o un Code Fix.

18. Cómo referenciar un Source Generator

Cuando durante el desarrollo se referencia un proyecto de Generator desde otro proyecto, el tratamiento es distinto al de una referencia de biblioteca normal.

Esto se debe a que el generador no es una biblioteca a la que se hace referencia en tiempo de ejecución, sino algo que se carga como Analyzer en tiempo de compilación.

En la referencia de proyecto se especifica así.

<ItemGroup>
  <ProjectReference Include="..\BuildInfoGenerator\BuildInfoGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

Al poner ReferenceOutputAssembly="false", se evita que el DLL del Generator se trate como un ensamblado de referencia normal.

También al distribuirlo como paquete NuGet, hay que colocarlo de forma que se cargue como Analyzer / Source Generator.

El Source Generator es útil, pero su ciclo de vida es distinto al de una biblioteca normal.

Biblioteca normal: la usa la aplicación en tiempo de ejecución
Source Generator: lo usa el compilador en tiempo de compilación

Es importante tener presente esta diferencia.

19. Para qué es adecuado el Source Generator

El Source Generator no es algo que sirva para generar cualquier cosa sin más.

Es adecuado para código como este.

Que resulta tedioso y propenso a errores si lo escribe una persona
Que se determina mecánicamente a partir de la información de entrada
Cuyo resultado generado es fácil de leer
Que permite reducir el uso de Reflection en tiempo de ejecución
Que puede mejorar la compatibilidad con AOT y con el trimming
Que puede aumentar la seguridad de tipos

Algunos ejemplos.

Metadatos para la serialización JSON
Código de registro en el contenedor de DI
Accesores de valores de configuración
Clientes de API
Código de conversión de enum
Generación de tipos a partir de definiciones SQL o CSV
Código auxiliar de INotifyPropertyChanged

Sin embargo, si el código generado es demasiado complejo, resulta difícil seguirlo cuando surge un problema.

Al usar un Source Generator, conviene tener presente lo siguiente.

Hacer que el código generado se pueda revisar
Mantener estables los nombres del código generado
Hacer que el resultado generado sea determinista
Hacer que el Diagnostic sea claro en caso de error
Evitar que un pequeño cambio en la entrada provoque una gran cantidad de diferencias

El código generado no debe parecer magia. Es importante que produzca código que quien lo mantenga en el futuro pueda leer.

20. Para qué no es adecuado el Source Generator

También hay procesos para los que el Source Generator no es adecuado.

Procesos que dependen del estado en tiempo de ejecución
Procesos que necesitan acceso a la red
Procesos que dependen del valor actual de un servicio externo
Procesos cuyo resultado cambia cada vez
Reescritura del código fuente existente
Análisis de una solución completa y enorme

Como el Generator se ejecuta en tiempo de compilación, un Generator lento empeora el tiempo de compilación y la experiencia en el IDE.

Además, un Generator que depende del entorno provoca problemas como estos.

Funciona en el PC del desarrollador pero falla en la CI
Funciona en la CI pero falla en otro sistema operativo
El resultado cambia según el estado de la caché
La compilación falla por un problema de red

Es recomendable que el Source Generator se acerque, en la medida de lo posible, a un proceso puro.

Entrada: código fuente, AdditionalFiles, AnalyzerConfigOptions
Salida: código C# generado, Diagnostic

Cuanto más clara sea esta relación, más estable será el Generator.

21. Herramientas de investigación de código con Roslyn

El uso de Roslyn no se limita a los Analyzers o los Source Generators. Usarlo desde una herramienta de consola propia también es una opción válida en la práctica.

Por ejemplo, existen necesidades como estas.

Listar los puntos donde se usa una API antigua
Contar el número de clases public por proyecto
Convertir a CSV las clases que tienen un atributo determinado
Investigar los espacios de nombres de los que depende una solución enorme
Identificar las API dependientes de Windows antes de migrar desde .NET Framework

En estos casos, a veces resulta más manejable crear una herramienta de investigación de ejecución puntual o periódica que crear un Analyzer.

Como ejemplo, esta es una imagen sencilla de cómo enumerar las clases public de una solución.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        if (root is null)
        {
            continue;
        }

        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.Modifiers.Any(m => m.Text == "public"));

        foreach (var cls in classes)
        {
            Console.WriteLine($"{project.Name},{document.FilePath},{cls.Identifier.Text}");
        }
    }
}

Este ejemplo solo observa la sintaxis. Si se quisiera investigar «las clases public que heredan de una clase base determinada», habría que usar SemanticModel para examinar la relación de herencia entre los tipos.

Si basta con el nombre o la forma, Syntax
Si hace falta llegar hasta el tipo o la referencia, SemanticModel
Si se trabaja con todo el proyecto, Workspace

Esta distinción es la base.

22. Diferencias con las expresiones regulares

Las expresiones regulares son útiles, pero no son adecuadas para tratar el significado del código C#.

Consideremos, por ejemplo, este código.

// Console.WriteLine("debug");

Si se busca Console.WriteLine con una expresión regular, es posible que también se capture el texto dentro de un comentario.

También existen literales de cadena como este.

var text = "Console.WriteLine";

O bien, puede haber un salto de línea de por medio.

Console
    .WriteLine("Hello");

Además, también se puede usar un alias.

using C = System.Console;

C.WriteLine("Hello");

Es difícil tratar correctamente todos estos casos con expresiones regulares.

Con Roslyn se pueden distinguir los comentarios, los literales de cadena, las llamadas a métodos a nivel sintáctico y el método realmente resuelto.

Por supuesto, para una investigación sencilla a veces basta con grep o ripgrep. Pero si se van a tomar decisiones de diseño o a aplicar correcciones automáticas a partir del resultado, es más seguro usar Roslyn.

Para una búsqueda aproximada, la búsqueda de texto
Para determinar algo correctamente como C#, Roslyn

23. Usar Roslyn para investigar activos existentes

Al migrar de .NET Framework al .NET actual, lo primero que se necesita es «conocer la situación actual». Y en esto Roslyn resulta útil.

Por ejemplo, investigaciones como estas.

Lista de dependencias de System.Web
Lista de código que presupone App.config / Web.config
Puntos donde se usan API específicas de Windows Forms / WPF
Puntos donde se usan Remoting / BinaryFormatter
Presencia de referencias COM
Lista de P/Invoke
Procesos de E/S que no se han hecho asíncronos
Puntos donde se usan API de cifrado antiguas

Incluso una simple búsqueda de texto puede arrojar candidatos. Pero con Roslyn se puede elaborar la lista a partir del resultado resuelto como tipo o método.

Por ejemplo, si solo se busca la cadena BinaryFormatter, también se capturan comentarios y documentación.

Si se investiga con Roslyn el uso del tipo System.Runtime.Serialization.Formatters.Binary.BinaryFormatter, se pueden obtener candidatos más precisos.

En el trabajo de migración no hace falta crear desde el principio un Analyzer perfecto. Ya tiene valor, para empezar, con una herramienta de consola de investigación que genere un CSV como este.

Project,File,Line,Symbol,Kind
Legacy.Web,Controllers/HomeController.cs,42,System.Web.HttpContext.Current,Property
Legacy.Core,Serialization/OldStore.cs,18,System.Runtime.Serialization.Formatters.Binary.BinaryFormatter,Type

Con una lista así, resulta más fácil elaborar el plan de migración.

24. Roslyn desde el punto de vista de quien desarrolla bibliotecas

Roslyn no solo es útil para quien desarrolla aplicaciones, sino también para quien desarrolla bibliotecas. Toda biblioteca tiene una forma correcta de usarse.

Por ejemplo, reglas como estas.

Hay que llamar primero al método de inicialización
Hay que aplicar un atributo determinado
Hay que llamar a Dispose
Una opción determinada es peligrosa
No se quiere que se use una API obsoleta en código nuevo

Si esto se comunica solo mediante la documentación, es posible que la persona usuaria se lo salte.

Si se incluye el Analyzer en el paquete NuGet de la biblioteca, se pueden mostrar advertencias directamente en el código de quien la use.

Por ejemplo, supongamos una biblioteca propia Company.Messaging; se podría detectar un uso incorrecto como este.

var client = new MessageClient();
client.Send(message); // Se llama a Send antes de llamar a Configure

El Analyzer puede emitir una advertencia como esta.

CMP1001: Llame a Configure antes de llamar a MessageClient.Send

Además, con un Code Fix se pueden proponer candidatos de corrección o código de ejemplo. Esto mejora la experiencia de uso de la biblioteca.

Comunicar en el editor de quien usa la biblioteca lo que está escrito en la documentación

Esta idea es uno de los grandes valores de Roslyn.

25. Consideraciones al distribuir un Analyzer por NuGet

El Analyzer se puede distribuir como paquete NuGet. Sin embargo, hay que pensarlo por separado de una biblioteca de ejecución normal, ya que el Analyzer no es algo necesario en tiempo de ejecución de la aplicación, sino algo que se usa en tiempo de compilación o en el IDE.

Por eso, al diseñar el paquete hay que considerar puntos como estos.

Si se incluyen la biblioteca de ejecución y el Analyzer en el mismo paquete
Si el Analyzer se distribuye en un paquete aparte
Si se emite una advertencia de forma predeterminada
Qué severidad se le asigna
Si se puede controlar mediante .editorconfig
Si no se genera de repente una gran cantidad de advertencias a los usuarios existentes

Si se usa solo dentro de la propia empresa, puede ser más fácil aceptar reglas relativamente estrictas.

Si se distribuye como biblioteca pública, hay que procurar no romper de repente la compilación de quienes la usan.

En muchos casos resulta más manejable empezar con Info o Warning y permitir que, si es necesario, cada usuario lo eleve a Error por su cuenta.

26. Roslyn y las funciones del IDE

En Visual Studio y en otros entornos de desarrollo de .NET, el enfoque de Roslyn está profundamente relacionado también con las funciones del IDE.

Por ejemplo, funciones como estas.

IntelliSense
Go to Definition
Find All References
Rename
Extract Method
Quick Actions
Advertencias de estilo de código
Detección de using no utilizados

Nada de esto se puede lograr con una simple búsqueda de texto. Por ejemplo, en Rename no se debe modificar por error otro símbolo distinto que tenga el mismo nombre.

class User
{
    public string Name { get; set; }
}

class Product
{
    public string Name { get; set; }
}

Cuando se quiere cambiar User.Name, no se debe cambiar también Product.Name. Esto requiere distinguirlos no solo por sintaxis, sino como símbolos.

La API de Roslyn constituye la base que permite aplicar este tipo de funciones propias del IDE también en herramientas propias.

27. Consideraciones de rendimiento

Roslyn es potente, pero si se escribe un proceso pesado, se vuelve lento, como es lógico. En particular, el Analyzer y el Source Generator pueden ejecutarse mientras el desarrollador está escribiendo o durante la compilación.

Por eso conviene prestar atención a lo siguiente.

Evitar obtener el SemanticModel innecesariamente
Reducir los candidatos por Syntax antes de hacer el análisis semántico
Evitar la E/S de archivos
No acceder a la red
Evitar un uso pesado de Reflection
Respetar las solicitudes de cancelación
Tener en cuenta la ejecución en paralelo
No llevar el análisis de toda la solución al Analyzer

En el Analyzer conviene reducir todo lo posible los objetos que se registran en Initialize.

Un mal ejemplo.

Examinar todos los SyntaxNode y luego decidir internamente con una gran cantidad de sentencias if

Una buena orientación.

Registrar solo el SyntaxKind necesario
Reducir primero, de forma ligera, por el nombre o la forma
Confirmar con SemanticModel solo cuando sea necesario

El Analyzer puede permanecer activo de forma constante en el entorno de desarrollo del usuario. Por eso, además de la precisión, la ligereza también forma parte de su calidad.

28. Diseño del Diagnostic

El Diagnostic que emite un Analyzer no consiste simplemente en mostrar una advertencia. Cuando lo vea el desarrollador, debe poder entender lo siguiente.

Qué es lo que está mal
Por qué es un problema
Qué parte hay que corregir
Cómo hay que corregirlo
Si existen excepciones

Un ejemplo de mensaje deficiente.

CMP001: Está prohibido

Así no se entiende qué es lo que está mal.

Un ejemplo con una buena orientación.

CMP001: DateTime.Now depende de la hora local del entorno de ejecución. Para la hora que se vaya a guardar o comparar, use DateTimeOffset.UtcNow o un proveedor de hora.

También conviene diseñar de antemano el ID del Diagnostic.

CMP0001-CMP0999: reglas comunes
CMP1000-CMP1999: reglas de la biblioteca A
CMP2000-CMP2999: reglas de apoyo a la migración

Si se puede preparar una página de documentación, también es útil configurar HelpLinkUri en el DiagnosticDescriptor.

Una advertencia es una comunicación dirigida al desarrollador. Si el mensaje es descuidado, la propia regla pierde credibilidad.

29. Diseño de la severidad

La severidad de un Analyzer debe decidirse con cuidado. Los niveles habituales son estos.

Hidden / Silent
Info
Suggestion
Warning
Error

En la práctica, en muchos casos es mejor no ponerlo directamente en Error. En particular, cuando hay mucho código existente, empezar con Error detiene la adopción.

En la práctica, resulta más fácil una introducción por etapas como esta.

1. Introducirlo primero como Warning
2. Visualizar el número de advertencias en la CI
3. No aumentar las infracciones nuevas
4. Convertir en Error solo las reglas importantes
5. Elaborar un plan para reducir las infracciones existentes

El objetivo del Analyzer no es incomodar al desarrollador, sino elevar la calidad de la base de código sin forzar la situación.

30. Depuración de un Source Generator

Como el Source Generator se ejecuta en un lugar distinto al de una aplicación normal, su depuración tiene ciertas particularidades.

Básicamente, se investiga con métodos como estos.

Revisar el código fuente generado
Emitir un Diagnostic
Escribir pruebas
Adjuntar un depurador si es necesario

En un proyecto de estilo SDK, resulta más fácil comprobarlo si se usa la configuración que emite los archivos generados.

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Con esto resulta más fácil revisar los .g.cs generados.

$(BaseIntermediateOutputPath) normalmente apunta dentro de obj/.

Si se especifica directamente bajo el proyecto, como en Generated, dado que en un proyecto de estilo SDK **/*.cs se incluye de forma predeterminada en la compilación, en la siguiente compilación el .g.cs ya generado puede volver a incorporarse como código fuente normal y provocar un error de tipo o miembro duplicado.

Si de todos modos se necesita emitir directamente bajo el proyecto, hay que excluirlo explícitamente de la compilación con algo como <Compile Remove="Generated/**/*.cs" />.

En las pruebas de un Generator se suele usar la forma de comparar el código de entrada con el resultado generado.

Preparar el código fuente de entrada
Ejecutar el Generator
Comprobar el código fuente generado
Comprobar el Diagnostic esperado

Un Source Generator que solo se verifica manualmente se rompe enseguida. Cuanto más compleja sea la lógica de generación, más importantes son las pruebas.

Orden de comprobación cuando no se ve lo generado

Cuando parece que el Source Generator «no está funcionando», la causa suele estar, más que en la propia lógica de generación, en la forma en que se referencia o en la caché del IDE. A continuación se indica, según el síntoma, qué es lo primero que hay que sospechar.

Síntoma Qué sospechar primero
La compilación pasa, pero no se encuentra el tipo que debería haberse generado La forma de referenciar el proyecto Generator. Si tiene OutputItemType="Analyzer" y ReferenceOutputAssembly="false" (capítulo 18)
No aparece ningún .g.cs dentro de obj/ Si se ha habilitado EmitCompilerGeneratedFiles. Si aun habilitándolo no aparece, es que el propio Generator no se está invocando
La compilación por línea de comandos pasa, pero solo el IDE muestra errores en rojo La caché del IDE. Reinicie Visual Studio, o cierre la solución y elimine obj/ y bin/
Se corrigió el Generator pero el cambio no se refleja Lo mismo que arriba. El DLL del Generator puede quedar cargado en el proceso del IDE
Aparece un error de tipo o miembro duplicado El destino generado está incluido en la compilación. Excluirlo con el <Compile Remove> mencionado antes
No aparece ningún error, pero tampoco se genera nada Es posible que se produzca una excepción dentro del Generator. Las excepciones a veces no se manifiestan de forma clara

Para aislar el problema, este orden es el más rápido.

1. Compruebe si aparece el mismo síntoma ejecutando dotnet build por línea de comandos
   → Si no aparece, el problema está en la caché del IDE
2. Habilite EmitCompilerGeneratedFiles y compruebe si aparece el .g.cs
   → Si no aparece, el Generator no se está invocando (sospeche de la forma de referenciarlo)
   → Si aparece, lea su contenido (el problema está en lo generado)
3. Reporte un Diagnostic en el punto de entrada del Generator y compruebe si se llega hasta ahí
4. Si aun así no queda claro, escriba una prueba

Si en el lado del Generator no se traga la excepción, sino que se captura con try / catch y se reporta explícitamente con ReportDiagnostic, el último síntoma de la tabla (que no aparece nada y no se genera nada) se reduce bastante.

«Adjuntar un depurador» es el último recurso. Casi siempre es más rápido aislar el problema con los pasos 1 a 3 de arriba, y como el Generator se invoca tanto desde el IDE como desde la compilación, confundir a qué proceso se está observando hace perder mucho tiempo.

31. Pruebas con Roslyn

Los Analyzers y los Source Generators deben desarrollarse escribiendo pruebas. En particular, en el Analyzer tanto los falsos positivos como los falsos negativos son un problema.

En las pruebas conviene preparar patrones como estos.

Código que debe detectarse
Código que no debe detectarse
Código que usa un using alias
Código que usa un nombre completamente calificado
Código que usa otro tipo con un nombre parecido
Código tratado como generated code
Código con nullable habilitado

Por ejemplo, en el caso de un Analyzer que prohíbe System.DateTime.Now, se comprueban casos como estos.

// Debe detectarse
var x = System.DateTime.Now;
// También debe detectarse cuando hay un using
using System;
var x = DateTime.Now;
// No debe detectarse si es un tipo distinto
namespace MyCompany;

public static class DateTime
{
    public static string Now => "now";
}

var x = DateTime.Now;

Este último caso es un ejemplo fácil de confundir con una búsqueda de texto. En un Analyzer de Roslyn, esto se evita comprobando el símbolo objetivo mediante el SemanticModel.

32. ¿Se puede usar también en proyectos de .NET Framework?

Roslyn no es exclusivo del .NET actual. Sin embargo, los puntos a tener en cuenta cambian según «cómo se use».

Cuando se usa como herramienta de investigación

Crear una herramienta de Roslyn como aplicación de consola en .NET 8 o .NET 10 para cargar y analizar una solución de .NET Framework es una opción realista.

En este caso, la propia herramienta se ejecuta en el .NET actual, mientras que el objeto de análisis puede ser código de .NET Framework.

Sin embargo, para cargar la solución con MSBuildWorkspace se necesita el MSBuild, el SDK, los ensamblados de referencia y el entorno de restauración de NuGet capaces de compilar el proyecto en cuestión.

Es decir, Roslyn por sí solo no puede leerlo todo: para resolver la configuración real del proyecto hace falta un entorno de compilación.

Cuando se usa como Analyzer

El Analyzer se ejecuta cargado por el compilador o el IDE.

Aunque el proyecto de destino sea .NET Framework, se puede usar si el entorno permite que el compilador cargue el Analyzer.

Sin embargo, en una configuración basada en un csproj antiguo, una versión antigua de Visual Studio, un MSBuild antiguo o packages.config, la introducción y el mantenimiento pueden no ser tan sencillos como en el estilo SDK actual.

Al introducirlo en un proyecto de .NET Framework existente, conviene comprobar primero lo siguiente.

La versión de Visual Studio / MSBuild
Si se puede usar PackageReference
Si el mismo Analyzer funciona en la CI
Si las advertencias aparecen en el registro de compilación
Si el .editorconfig surte efecto

Cuando se usa como Source Generator

El Source Generator es un mecanismo que el compilador carga en tiempo de compilación.

Por eso, más que el framework de ejecución del proyecto de destino, lo que importa es el nivel de compatibilidad del compilador y el SDK usados para compilar.

Mientras que en un proyecto de estilo SDK del .NET actual resulta fácil de manejar, en un proyecto antiguo de .NET Framework hay que prestar atención según el formato del proyecto y el entorno de compilación.

Para los activos existentes en .NET Framework, en muchos casos es más seguro empezar primero con una herramienta de investigación o un Analyzer basados en Roslyn, en lugar de incorporar un Source Generator desde el principio.

33. Consideraciones sobre la elección de versión

Entre los paquetes NuGet relacionados con Roslyn se encuentran los Microsoft.CodeAnalysis.*.

Los más representativos son estos.

Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
Microsoft.CodeAnalysis.Workspaces.MSBuild
Microsoft.CodeAnalysis.Analyzers
Microsoft.CodeAnalysis.CSharp.CodeFix.Testing
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing

Lo que conviene tener presente aquí es que el Analyzer y el Source Generator son cargados por el compilador del lado del usuario.

Es decir, si el SDK / Visual Studio del desarrollador o de la CI es antiguo, un Analyzer o Generator que use una API de Roslyn demasiado reciente puede no funcionar.

Si es de uso exclusivamente interno y se puede unificar el entorno de compilación, resulta más fácil usar una API relativamente reciente.

En cambio, en una biblioteca de distribución externa hay que elegir con cuidado la versión de Microsoft.CodeAnalysis de la que se depende, teniendo en cuenta la amplia variedad de entornos de los usuarios.

Como criterio general, conviene pensarlo así.

Uso interno: unificar la CI y el entorno de desarrollo y usar una API relativamente reciente
Distribución externa: elegir de forma conservadora teniendo en cuenta el rango de SDK/VS de los usuarios
Generator: diseñarlo como Incremental Generator siempre que sea posible
Analyzer: dar prioridad a la ligereza para no romper la experiencia del IDE

Como Roslyn pertenece a un área cercana al compilador, es especialmente sensible a las diferencias de versión.

34. No intentar resolverlo todo con Roslyn

Roslyn es potente, pero no es una herramienta que resuelva todos los problemas. Por ejemplo, problemas como estos no se pueden resolver solo con Roslyn.

Qué rama se ejecuta en tiempo de ejecución
Qué valor llega con los datos de producción
Un método invocado dinámicamente mediante reflection
El resultado del registro en tiempo de ejecución del contenedor de DI
Un proceso que cambia según el archivo de configuración
Un valor devuelto por un servicio externo

Roslyn es principalmente una herramienta para tratar el código fuente y la información de compilación. Si se quiere conocer el comportamiento en tiempo de ejecución, hacen falta otros medios, como pruebas, registros, trazas, perfilado o análisis de volcados.

Por lo tanto, conviene entender el papel de Roslyn así.

Tratar con alta precisión lo que se puede saber de forma estática

Si se intenta forzar a Roslyn a resolver también lo que solo se puede saber de forma dinámica, el resultado es un mecanismo complejo e impreciso.

35. Orden de introducción

Si va a empezar a usar Roslyn en la práctica, se recomienda este orden.

1. Poner en orden los Analyzers de .NET existentes y el .editorconfig
2. Escribir una pequeña herramienta de investigación con Syntax Tree
3. Probar la resolución de tipos con SemanticModel
4. Leer una solución con MSBuildWorkspace
5. Crear un pequeño Analyzer específico del equipo
6. Añadir un Code Fix si es necesario
7. Considerar un Source Generator en los lugares con mucho código repetitivo

No es necesario empezar directamente por Source Generator. En muchos proyectos, el Analyzer y las herramientas de investigación dan resultados antes.

Especialmente cuando los activos existentes son grandes, un flujo como este es realista.

Conocer la situación actual con una herramienta de investigación
Convertir en Analyzer los problemas frecuentes
Convertir en Code Fix solo lo que se pueda corregir con seguridad
Convertir en Generator el código repetitivo que se escribe una y otra vez

Roslyn es una herramienta que se puede usar de forma gradual.

36. Un pequeño ejemplo: listar llamadas a métodos

Por último, veamos el uso de Roslyn en una forma algo más cercana a la práctica. Aquí se muestra la idea de listar las llamadas a métodos dentro de una solución.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    var compilation = await project.GetCompilationAsync();
    if (compilation is null)
    {
        continue;
    }

    foreach (var document in project.Documents)
    {
        var tree = await document.GetSyntaxTreeAsync();
        if (tree is null)
        {
            continue;
        }

        var root = await tree.GetRootAsync();
        var semanticModel = compilation.GetSemanticModel(tree);

        var invocations = root
            .DescendantNodes()
            .OfType<InvocationExpressionSyntax>();

        foreach (var invocation in invocations)
        {
            var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
            if (symbol is null)
            {
                continue;
            }

            var lineSpan = invocation.GetLocation().GetLineSpan();
            var line = lineSpan.StartLinePosition.Line + 1;

            Console.WriteLine(string.Join(",", new[]
            {
                project.Name,
                document.FilePath ?? document.Name,
                line.ToString(),
                symbol.ContainingType.ToDisplayString(),
                symbol.Name
            }));
        }
    }
}

Si se amplía un poco una herramienta como esta, se pueden realizar investigaciones como estas.

Extraer solo las llamadas a un método específico
Mostrar los puntos donde se usa una API obsoleta
Mostrar la frecuencia de uso por proyecto
Crear una lista de las API objeto de migración

Poder leer el código fuente desde la perspectiva del compilador facilita bastante la investigación del código existente.

37. Consideraciones al reescribir código con Roslyn

Con Roslyn también se puede reescribir código usando el árbol de sintaxis.

Por ejemplo, se puede cambiar el nombre de un método determinado, añadir un atributo o añadir un using.

Sin embargo, la reescritura de código debe hacerse con cuidado. Los puntos a tener en cuenta son estos.

Confirmar que el significado no cambia
No romper los comentarios ni los espacios en blanco
Que la diferencia no sea excesivamente grande
Unificar el formato
No hacer demasiadas transformaciones a la vez
Hacer que la diferencia en Git sea fácil de revisar

Como el Syntax Tree de Roslyn conserva el Trivia, es posible transformar el código manteniendo los comentarios y los espacios en blanco. Sin embargo, si se crean los nodos de forma descuidada, el formato del código resultante puede quedar deshecho.

Al crear una herramienta de reescritura, este criterio resulta seguro.

Realizar primero solo la detección
Comprobar la diferencia antes y después de la transformación
Empezar por transformaciones pequeñas
Escribir pruebas para la propia herramienta de transformación
En la CI, empezar por el modo de detección

En una transformación mecánica a gran escala, Roslyn es potente, pero al final siempre hace falta la revisión de una persona.

38. Roslyn y la asistencia de codificación con IA

En los últimos años se ha generalizado también la generación de código y el apoyo a la revisión mediante IA, pero el valor de Roslyn no disminuye por ello. La IA es hábil manejando el lenguaje natural y el contexto circundante, mientras que Roslyn, como compilador, es hábil manejando información sintáctica y semántica precisa. Más que competir, ambos se complementan.

Por ejemplo, se puede pensar en una distinción de uso como esta.

Extraer con Roslyn, de forma precisa, los puntos objetivo
Generar con IA el criterio de corrección o el texto explicativo
Comprobar con Roslyn si la propuesta de corrección compila
Prevenir la reincidencia con un Analyzer

En algunos casos es más seguro extraer con Roslyn los puntos objetivo de forma precisa que pedirle a la IA que «corrija todas las API antiguas de esta base de código».

Y usar la IA para estudiar el criterio de corrección o como apoyo en la revisión resulta más práctico.

Lo que el compilador puede determinar, se deja en manos del compilador. Las personas y la IA se concentran en el juicio que va más allá de eso.

Este reparto es lo importante.

39. Checklist práctico

Antes de usar Roslyn, conviene comprobar lo siguiente.

El objetivo es investigar, advertir, corregir o generar
Basta con la sintaxis, o hace falta el análisis semántico
Basta con un único archivo, o hace falta todo el proyecto
Hace falta que funcione en el IDE, o basta con una herramienta puntual
Se puede permitir que afecte al tiempo de compilación
Se va a ejecutar en la CI
No va a generar una gran cantidad de advertencias en el código existente
Qué severidad se le da al Analyzer
Si el Code Fix se puede aplicar de forma segura
Si se puede revisar el código generado por el Source Generator
Si la versión del SDK / Visual Studio de los usuarios está unificada

Si duda a la hora de decidir, conviene dividirlo así.

Quiero investigar                     -> Una herramienta de consola con Roslyn
Quiero que siempre se respete         -> Analyzer
La forma de corregirlo está decidida  -> Code Fix
Quiero crear código repetitivo        -> Source Generator

Con esta división resulta más difícil equivocarse en dónde aplicar Roslyn.

40. Resumen

Roslyn es lo que abre el compilador de C# y Visual Basic como una API que los desarrolladores pueden utilizar.

Con Roslyn, el código fuente se puede tratar de estas formas, y no como una simple cadena de texto.

Leer la sintaxis como Syntax Tree
Leer el significado como SemanticModel
Tratar la compilación completa como Compilation
Tratar la solución o el proyecto como Workspace
Emitir advertencias como Analyzer
Proponer correcciones como Code Fix
Generar código como Source Generator

En la práctica, resulta especialmente útil en situaciones como estas.

Investigación de una base de código existente
Apoyo a la migración de .NET Framework a .NET
Comprobación automática de las convenciones del equipo
Guía para quienes usan una biblioteca
Generación de código repetitivo
Garantía de calidad en el IDE o en la CI

Lo importante es no adoptar frente a Roslyn una actitud demasiado rígida, como si fuera una «tecnología de compiladores difícil».

Al principio basta con leer un archivo con CSharpSyntaxTree.ParseText y enumerar los nombres de los métodos. A partir de ahí, basta con ir ampliando hacia SemanticModel, Workspace, Analyzer y Source Generator.

Si hubiera que resumir Roslyn en una frase, sería esta.

Permite tratar el código C#, no como una cadena de texto, sino como la estructura que el compilador ha comprendido.

Con esta perspectiva, la revisión de código, la migración, la investigación y la automatización de la generación resultan un poco más sencillas.

Referencias

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué es Roslyn?
Roslyn, cuyo nombre oficial es .NET Compiler Platform, es la implementación del compilador de C# y Visual Basic y, al mismo tiempo, un conjunto de API para crear herramientas de análisis de código. Permite que las aplicaciones y herramientas accedan a la información que el compilador genera internamente (por ejemplo, a qué tipo pertenece este identificador o qué método referencia esta llamada), información que antes quedaba encerrada dentro de la «caja negra» del compilador. Gracias a esto, es posible leer el código C# como sintaxis en lugar de como texto, interpretarlo por su significado en lugar de por su apariencia, y emitir advertencias, propuestas de corrección y código generado.
¿Qué se puede hacer con Roslyn?
Con Roslyn puede realizar análisis sintáctico de C# / VB, análisis semántico de tipos y métodos, análisis de proyectos o soluciones completas, creación de Analyzers, Code Fixes y Source Generators propios, así como generación y transformación de código. En términos más prácticos, se usa para convertir el uso de una API prohibida en una advertencia de compilación, listar los puntos donde se usa una API obsoleta, generar en tiempo de compilación el código de mapeo de un DTO, o apoyar la investigación de una migración de .NET Framework a .NET. Las formas de uso se dividen a grandes rasgos en cuatro: como biblioteca, como Analyzer, como Code Fix y como Source Generator.
¿En qué se diferencia de la búsqueda de código con expresiones regulares o grep?
Con expresiones regulares es difícil tratar correctamente las cadenas dentro de comentarios, los literales de cadena, las llamadas que se dividen en varias líneas o las llamadas con un alias definido mediante using alias. Con Roslyn, en cambio, es posible distinguir entre comentarios, literales de cadena, llamadas a métodos a nivel sintáctico y el método realmente resuelto. Para una búsqueda aproximada, a veces basta con la búsqueda de texto, pero si va a tomar decisiones de diseño o aplicar correcciones automáticas a partir del resultado, es más seguro usar Roslyn, que permite decidir en función de la resolución de nombres realizada por el compilador.
¿Por dónde conviene empezar a aprender Roslyn?
No es necesario empezar directamente por Source Generator. Se recomienda primero poner en orden los Analyzers incluidos en el .NET SDK y el archivo .editorconfig, después escribir una pequeña herramienta de investigación que lea un archivo con CSharpSyntaxTree.ParseText y enumere los nombres de los métodos, y a partir de ahí ampliar el alcance hacia la resolución de tipos con SemanticModel, la carga de soluciones con MSBuildWorkspace y, finalmente, la creación de pequeños Analyzers específicos del equipo. En muchos proyectos, los Analyzers y las herramientas de investigación dan resultados antes que los Source Generators.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog