Tutorial: Crear el primer analizador y la corrección de código

El SDK de .NET Compiler Platform proporciona las herramientas que necesita para crear diagnósticos personalizados (analizadores), correcciones de código, refactorización de código y supresores de diagnóstico que tengan como destino código de C# o Visual Basic. Un analizador contiene código que reconoce las infracciones de la regla. La corrección del código contiene el código que corrige la infracción. Las reglas implementadas pueden ser cualquier elemento de la estructura de código para codificar el estilo de las convenciones de nomenclatura y mucho más. .NET Compiler Platform proporciona el marco para ejecutar el análisis a medida que los desarrolladores escriben código y todas las características de la interfaz de usuario de Visual Studio para corregir código: mostrar líneas de subrayado en el editor, rellenar la lista de errores de Visual Studio, crear las sugerencias con "bombillas" y mostrar la vista previa enriquecida de las correcciones sugeridas.

En este tutorial, explorará la creación de un analizador y una corrección de código complementaria con el uso de las API de Roslyn. Un analizador es una manera de realizar análisis de código fuente y notificar un problema al usuario. Opcionalmente, se puede asociar una corrección de código al analizador para representar una modificación en el código fuente del usuario. Este tutorial crea un analizador que busca declaraciones de variable local que podrían declararse mediante el modificador const, aunque no están. La corrección de código complementaria modifica esas declaraciones para agregar el modificador const.

Requisitos previos

Debe instalar el SDK de .NET Compiler Platform a través del Instalador de Visual Studio:

Instrucciones de instalación: Instalador de Visual Studio

Hay dos maneras distintas de buscar el SDK de .NET Compiler Platform en el Instalador de Visual Studio:

Instalación con el Instalador de Visual Studio: visualización de cargas de trabajo

El SDK de .NET Compiler Platform no se selecciona automáticamente como parte de la carga de trabajo de desarrollo de extensiones de Visual Studio. Se debe seleccionar como un componente opcional.

  1. Ejecute el Instalador de Visual Studio.
  2. Selección de Modificar
  3. Active la carga de trabajo Desarrollo de extensiones de Visual Studio.
  4. Abra el nodo Desarrollo de extensiones de Visual Studio en el árbol de resumen.
  5. Active la casilla SDK de .NET Compiler Platform. La encontrará en última posición bajo los componentes opcionales.

Opcionalmente, también le interesará que el Editor de DGML muestre los gráficos en el visualizador:

  1. Abra el nodo Componentes individuales en el árbol de resumen.
  2. Active la casilla Editor de DGML.

Instalación con el Instalador de Visual Studio: pestaña Componentes individuales

  1. Ejecute el Instalador de Visual Studio.
  2. Selección de Modificar
  3. Haga clic en la pestaña Componentes individuales.
  4. Active la casilla SDK de .NET Compiler Platform. La encontrará en la parte superior bajo la sección Compiladores, herramientas de compilación y tiempos de ejecución.

Opcionalmente, también le interesará que el Editor de DGML muestre los gráficos en el visualizador:

  1. Active la casilla Editor de DGML. La encontrará en la sección Herramientas de código.

Hay varios pasos para crear y validar su analizador:

  1. Crear la solución.
  2. Registrar el nombre del analizador y la descripción.
  3. Notificar las recomendaciones y las advertencias del analizador.
  4. Implementar la corrección de código para aceptar las recomendaciones.
  5. Mejorar el análisis mediante las pruebas unitarias.

Creación de la solución

  • En Visual Studio, elija Archivo > Nuevo > Proyecto... para mostrar el cuadro de diálogo Nuevo proyecto.
  • En Visual C# > Extensibilidad, elija Analizador con corrección de código (.NET Standard).
  • Asigne al proyecto el nombre "MakeConst" y haga clic en Aceptar.

Nota

Puede obtener un error de compilación (MSB4062: No se pudo cargar la tarea "CompareBuildTaskVersion"). Para corregirlo, actualice los paquetes NuGet de la solución con el Administrador de paquetes NuGet o use Update-Package en la ventana Consola del administrador de paquetes.

Exploración de la plantilla del analizador

La plantilla de analizador con corrección de código crea cinco proyectos:

  • MakeConst, que contiene el analizador.
  • MakeConst.CodeFixes, que contiene la corrección del código.
  • MakeConst.Package, que se usa a fin de generar el paquete NuGet para el analizador y la corrección de código.
  • MakeConst.Test, que es un proyecto de prueba unitaria.
  • MakeConst.Vsix, que es el proyecto de inicio predeterminado que inicia una segunda instancia de Visual Studio que ha cargado el nuevo analizador. Presione F5 para iniciar el proyecto de VSIX.

Nota

Los analizadores deben tener como destino .NET Standard 2.0 porque se pueden ejecutar en el entorno de .NET Core (compilaciones de línea de comandos) y el de .NET Framework (Visual Studio).

Sugerencia

Al ejecutar el analizador, inicie una segunda copia de Visual Studio. Esta segunda copia usa un subárbol del registro diferente para almacenar la configuración. Le permite diferenciar la configuración visual en las dos copias de Visual Studio. Puede elegir un tema diferente para la ejecución experimental de Visual Studio. Además, no mueva la configuración o el inicio de sesión a la cuenta de Visual Studio con la ejecución experimental de Visual Studio. Así se mantiene una configuración diferente.

El subárbol incluye no solo el analizador en desarrollo, sino también los analizadores anteriores abiertos. Para restablecer el subárbol Roslyn, debe eliminarlo manualmente de %LocalAppData%\Microsoft\VisualStudio. El nombre de carpeta del subárbol Roslyn terminará en Roslyn; por ejemplo, 16.0_9ae182f9Roslyn. Tenga en cuenta que es posible que tenga que limpiar la solución y volver a generarla después de eliminar el subárbol.

En la segunda instancia de Visual Studio que acaba de iniciar, cree un proyecto de aplicación de consola de C# (servirá cualquier marco; los analizadores funcionan en el nivel de origen). Mantenga el mouse sobre el token con un subrayado ondulado y aparecerá el texto de advertencia proporcionado por un analizador.

La plantilla crea un analizador que notifica una advertencia en cada declaración de tipo, donde el nombre de tipo contiene letras minúsculas, tal como se muestra en la ilustración siguiente:

Advertencia notificada por el analizador

La plantilla también proporciona una corrección de código que cambia cualquier nombre de tipo que contiene caracteres en minúsculas a mayúsculas. Puede hacer clic en la bombilla mostrada con la advertencia para ver los cambios sugeridos. Al aceptar los cambios sugeridos, se actualizan el nombre de tipo y todas las referencias a dicho tipo en la solución. Ahora que ha visto el analizador inicial en acción, cierre la segunda instancia de Visual Studio y vuelva a su proyecto de analizador.

No tiene que iniciar una segunda copia de Visual Studio, y cree código para probar todos los cambios en el analizador. La plantilla también crea un proyecto de prueba unitaria de forma automática. Este proyecto contiene dos pruebas. TestMethod1 muestra el formato típico de una prueba que analiza el código sin que se desencadene un diagnóstico. TestMethod2 muestra el formato de una prueba que desencadena un diagnóstico y, a continuación, se aplica una corrección de código sugerida. Al crear el analizador y la corrección de código, deberá escribir pruebas para diferentes estructuras de código para verificar el trabajo. Las pruebas unitarias de los analizadores son mucho más rápidas que las pruebas interactivas con Visual Studio.

Sugerencia

Las pruebas unitarias del analizador son una herramienta magnífica si se sabe qué construcciones de código deben y no deben desencadenar el analizador. Cargar el analizador en otra copia de Visual Studio es una herramienta magnífica para explorar y buscar construcciones en las que puede no haber pensado todavía.

En este tutorial, se escribe un analizador que notifica al usuario las declaraciones de variables locales que se pueden convertir en constantes locales. Por ejemplo, considere el siguiente código:

int x = 0;
Console.WriteLine(x);

En el código anterior, a x se le asigna un valor constante y nunca se modifica. Se puede declarar con el modificador const:

const int x = 0;
Console.WriteLine(x);

El análisis para determinar si una variable se puede convertir en constante, que requiere un análisis sintáctico, un análisis constante de la expresión del inicializador y un análisis del flujo de datos para garantizar que no se escriba nunca en la variable. .NET Compiler Platform proporciona las API que facilita la realización de este análisis.

Creación de registros del analizador

La plantilla crea la clase DiagnosticAnalyzer inicial en el archivo MakeConstAnalyzer.cs. Este analizador inicial muestra dos propiedades importantes de cada analizador.

  • Cada analizador de diagnóstico debe proporcionar un atributo [DiagnosticAnalyzer] que describe el lenguaje en el que opera.
  • Cada analizador de diagnóstico debe derivar (directa o indirectamente) de la clase DiagnosticAnalyzer.

La plantilla también muestra las características básicas que forman parte de cualquier analizador:

  1. Registre acciones. Las acciones representan los cambios de código que deben desencadenar el analizador para examinar el código para las infracciones. Cuando Visual Studio detecta las modificaciones del código que coinciden con una acción registrada, llama al método registrado del analizador.
  2. Cree diagnósticos. Cuando el analizador detecta una infracción, crea un objeto de diagnóstico que Visual Studio usa para notificar la infracción al usuario.

Registre acciones en la invalidación del método DiagnosticAnalyzer.Initialize(AnalysisContext). En este tutorial, repasará nodos de sintaxis que buscan declaraciones locales y verá cuáles de ellos tienen valores constantes. Si una declaración puede ser constante, el analizador creará y notificará un diagnóstico.

El primer paso es actualizar las constantes de registro y el método Initialize, por lo que estas constantes indican su analizador "Make Const". La mayoría de las constantes de cadena se definen en el archivo de recursos de cadena. Debe seguir dicha práctica para una localización más sencilla. Abra el archivo Resources.resx para el proyecto de analizador MakeConst. Muestra el editor de recursos. Actualice los recursos de cadena como sigue:

  • Cambie AnalyzerDescription a "Variables that are not modified should be made constants.".
  • Cambie AnalyzerMessageFormat a "Variable '{0}' can be made constant".
  • Cambie AnalyzerTitle a "Variable can be made constant".

Cuando haya terminado, el editor de recursos debe aparecer tal y como se muestra en la ilustración siguiente:

Actualización de los recursos de cadena

Los cambios restantes están en el archivo del analizador. Abra MakeConstAnalyzer.cs en Visual Studio. Cambie la acción registrada de una que actúa en los símbolos a una que actúa en la sintaxis. En el método MakeConstAnalyzerAnalyzer.Initialize, busque la línea que registra la acción en los símbolos:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Reemplácela por la línea siguiente:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Después de este cambio, puede eliminar el método AnalyzeSymbol. Este analizador examina SyntaxKind.LocalDeclarationStatement, no las instrucciones SymbolKind.NamedType. Tenga en cuenta que AnalyzeNode tiene un subrayado ondulado rojo debajo. El código recién agregado hace referencia a un método AnalyzeNode que no se ha declarado. Declare dicho método con el siguiente código:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Cambie Category a "Usage" en MakeConstAnalyzer.cs, como se muestra en el código siguiente:

private const string Category = "Usage";

Búsqueda de las declaraciones locales que podrían ser constantes

Es el momento de escribir la primera versión del método AnalyzeNode. Debe buscar una sola declaración local que podría ser const pero no lo es, al igual que el código siguiente:

int x = 0;
Console.WriteLine(x);

El primer paso es encontrar las declaraciones locales. Agregue el código siguiente a AnalyzeNode en MakeConstAnalyzer.cs:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Esta conversión siempre se realiza correctamente porque el analizador registró los cambios de las declaraciones locales, y solo las declaraciones locales. Ningún otro tipo de nodo desencadena una llamada al método AnalyzeNode. A continuación, compruebe la declaración de cualquier modificador const. Si la encuentra, devuélvala de inmediato. El código siguiente busca cualquier modificador const en la declaración local:

// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
    return;
}

Por último, deberá comprobar que la variable podría ser const. Esto significa asegurarse de que nunca se asigne después de inicializarse.

Realizará algún análisis semántico con SyntaxNodeAnalysisContext. Use el argumento context para determinar si la declaración de variable local puede convertirse en const. Una clase Microsoft.CodeAnalysis.SemanticModel representa toda la información semántica en un solo archivo de origen. Puede obtener más información en el artículo que trata los modelos semánticos. Deberá usar Microsoft.CodeAnalysis.SemanticModel para realizar análisis de flujo de datos en la instrucción de declaración local. A continuación, use los resultados de este análisis de flujo de datos para garantizar que la variable local no se escriba con un valor nuevo en cualquier otro lugar. Llame al método de extensión GetDeclaredSymbol para recuperar ILocalSymbol para la variable y compruebe si no está incluido en la colección DataFlowAnalysis.WrittenOutside del análisis de flujo de datos. Agregue el código siguiente al final del método AnalyzeNode:

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

El código recién agregado garantiza que no se modifique la variable y que se pueda convertir por tanto en const. Es el momento de generar el diagnóstico. Agregue el código siguiente a la última línea en AnalyzeNode:

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));

Puede comprobar el progreso presionando F5 para ejecutar el analizador. Puede cargar la aplicación de consola que creó anteriormente y después agregar el siguiente código de prueba:

int x = 0;
Console.WriteLine(x);

Debe aparecer la bombilla, y el analizador debe informar de un diagnóstico. Sin embargo, en función de la versión de Visual Studio, aparecerá una de estas opciones:

  • La bombilla, que todavía usa la corrección de código generada en plantilla, indica que se puede convertir en mayúsculas.
  • Un mensaje de banner en la parte superior del editor, que indica que "MakeConstCodeFixProvider" ha encontrado un error y se ha deshabilitado. Esto se debe a que el proveedor de corrección de código aún no se ha cambiado y todavía espera encontrar los elementos TypeDeclarationSyntax en vez de los elementos LocalDeclarationStatementSyntax.

En la sección siguiente se explica cómo escribir la corrección de código.

Escritura de la corrección de código

Un analizador puede proporcionar una o varias correcciones de código. Una corrección de código define una edición que soluciona el problema notificado. Para el analizador que ha creado, puede proporcionar una corrección de código que inserta la palabra clave const:

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

El usuario la elige en la interfaz de usuario de la bombilla del editor, y Visual Studio cambia el código.

Abra el archivo CodeFixResources.resx y cambie CodeFixTitle a "Make constant".

Abra el archivo MakeConstCodeFixProvider.cs agregado por la plantilla. Esta corrección de código ya está conectada con el identificador de diagnóstico generado por el analizador de diagnóstico, pero aún no implementa la transformación de código correcta.

Después, elimine el método MakeUppercaseAsync. Ya no se aplica.

Todos los proveedores de corrección de código se derivan de CodeFixProvider. Todas invalidan CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) para notificar las correcciones de código disponibles. En RegisterCodeFixesAsync, cambie el tipo de nodo antecesor que está buscando por LocalDeclarationStatementSyntax para que coincida con el diagnóstico:

var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();

A continuación, cambie la última línea para registrar una corrección de código. La corrección creará un documento que resulta de agregar el modificador const a una declaración existente:

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
    CodeAction.Create(
        title: CodeFixResources.CodeFixTitle,
        createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
        equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
    diagnostic);

Observará un subrayado ondulado rojo en el código que acaba de agregar en el símbolo MakeConstAsync. Agregue una declaración para MakeConstAsync como el código siguiente:

private static async Task<Document> MakeConstAsync(Document document,
    LocalDeclarationStatementSyntax localDeclaration,
    CancellationToken cancellationToken)
{
}

El nuevo método MakeConstAsync transformará la clase Document que representa el archivo de origen del usuario en una nueva clase Document que ahora contiene una declaración const.

Se crea un token de palabra clave const para insertarlo en la parte delantera de la instrucción de declaración. Tenga cuidado de quitar primero cualquier curiosidad inicial del primer token de la instrucción de declaración y adjúntela al token const. Agregue el código siguiente al método MakeConstAsync:

// Remove the leading trivia from the local declaration.
SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
    firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));

// Create a const token with the leading trivia.
SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

A continuación, agregue el token const a la declaración con el siguiente código:

// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
    .WithModifiers(newModifiers)
    .WithDeclaration(localDeclaration.Declaration);

Después aplique formato a la nueva declaración para que coincida con las reglas de formato de C#. Aplicar formato a los cambios para que coincidan con el código existente mejora la experiencia. Agregue la instrucción siguiente inmediatamente después del código existente:

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

Se requiere un nuevo espacio de nombres para este código. Agregue la siguiente directiva using al principio del archivo:

using Microsoft.CodeAnalysis.Formatting;

El último paso es realizar la edición. Hay tres pasos para este proceso:

  1. Obtenga un identificador para el documento existente.
  2. Cree un documento mediante el reemplazo de la declaración existente con la nueva declaración.
  3. Devuelva el nuevo documento.

Agregue el código siguiente al final del método MakeConstAsync:

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);

La corrección de código está lista para probarla. Presione F5 para ejecutar el proyecto del analizador en una segunda instancia de Visual Studio. En la segunda instancia de Visual Studio, cree un proyecto de aplicación de consola de C# y agregue algunas declaraciones de variable local inicializadas con valores de constante para el método Main. Observará que se notifican como advertencias de la siguiente forma.

Puede convertir las advertencias en constantes

Ha progresado bastante. Hay subrayados ondulados debajo de las declaraciones que pueden convertirse en const. Pero aún queda trabajo por hacer. Esto funciona bien si agrega const a las declaraciones a partir de i, luego j y, por último, k. Sin embargo, si agrega el modificador const en un orden diferente, a partir de k, el analizador crea errores: k no puede declararse como const, a menos que i y j ya sean const. Tiene que realizar más análisis para asegurarse de que controla la forma en que las variables pueden declararse e inicializarse.

Compilación de pruebas unitarias

El analizador y la corrección de código funcionan en un caso sencillo de una única declaración que puede convertirse en const. Hay varias instrucciones de declaración posibles donde esta implementación comete errores. Abordará estos casos al trabajar con la biblioteca de pruebas unitarias escrita por la plantilla. Es mucho más rápido que abrir repetidamente una segunda copia de Visual Studio.

Abra el archivo MakeConstUnitTests.cs en el proyecto de prueba unitaria. La plantilla creó dos pruebas que siguen los dos patrones comunes para una prueba unitaria de la corrección de código y del analizador. TestMethod1 muestra el patrón para una prueba que garantiza que el analizador no notifique un diagnóstico cuando no debe. TestMethod2 muestra el patrón de notificación de un diagnóstico y de ejecución de la corrección de código.

La plantilla usa los paquetes Microsoft.CodeAnalysis.Testing para las pruebas unitarias.

Sugerencia

La biblioteca de pruebas admite una sintaxis de marcado especial, que incluye lo siguiente:

  • [|text|]: indica que se ha notificado un diagnóstico para text. De forma predeterminada, este formulario solo se puede usar para probar analizadores con exactamente una instancia de DiagnosticDescriptor proporcionada por DiagnosticAnalyzer.SupportedDiagnostics.
  • {|ExpectedDiagnosticId:text|}: indica que se ha notificado un diagnóstico para text con IdExpectedDiagnosticId.

Reemplace las pruebas de plantilla de la clase MakeConstUnitTest por el método de prueba siguiente:

        [TestMethod]
        public async Task LocalIntCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|int i = 0;|]
        Console.WriteLine(i);
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }

Ejecute esta prueba para asegurarse de que se supera. En Visual Studio, abra el Explorador de pruebas; para ello, seleccione Prueba>Windows>Explorador de pruebas. Luego, seleccione Ejecutar todo.

Creación de pruebas para declaraciones válidas

Por norma general, los analizadores deben existir lo más rápido posible, pero haciendo el mínimo trabajo. Visual Studio llama a los analizadores registrados a medida que el usuario edita el código. La capacidad de respuesta es un requisito clave. Hay varios casos de pruebas del código que no deben realizar un diagnóstico. El analizador ya controla una de esas pruebas, el caso en que una variable se asigna después de inicializarse. Agregue el siguiente método de prueba para representar ese caso:

        [TestMethod]
        public async Task VariableIsAssigned_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0;
        Console.WriteLine(i++);
    }
}
");
        }

Esta prueba también pasa. Después, agregue métodos de prueba para las condiciones que todavía no ha controlado:

  • Declaraciones que ya son const, porque ya son constantes:

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Declaraciones que no tienen inicializador, porque no hay ningún valor para usar:

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Declaraciones donde el inicializador no es una constante, porque no pueden ser constantes en tiempo de compilación:

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

Puede ser incluso más complicado, porque C# admite varias declaraciones como una instrucción. Considere la siguiente constante de cadena de caso de prueba:

        [TestMethod]
        public async Task MultipleInitializers_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0, j = DateTime.Now.DayOfYear;
        Console.WriteLine(i);
        Console.WriteLine(j);
    }
}
");
        }

La variable i puede convertirse en constante, pero la variable j no puede. Por tanto, esta instrucción no puede convertirse en una declaración de constante.

Vuelva a ejecutar las pruebas y, después, observará que estos nuevos casos de prueba generarán errores.

Actualización del analizador para ignorar declaraciones correctas

Necesita algunas mejoras en el método AnalyzeNode del analizador para filtrar el código que cumple estas condiciones. Son todas condiciones relacionadas, por lo que los cambios similares corregirán todas estas condiciones. Realice los siguientes cambios en AnalyzeNode:

  • El análisis semántico analizó una única declaración de variable. Este código necesita estar en un bucle foreach que examina todas las variables declaradas en la misma instrucción.
  • Cada variable declarada necesita tener un inicializador.
  • El inicializador de cada variable declarada debe ser una constante de tiempo de compilación.

En el método AnalyzeNode, reemplace el análisis semántico original:

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

por el siguiente fragmento de código:

// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    EqualsValueClauseSyntax initializer = variable.Initializer;
    if (initializer == null)
    {
        return;
    }

    Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
    if (!constantValue.HasValue)
    {
        return;
    }
}

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    // Retrieve the local symbol for each variable in the local declaration
    // and ensure that it is not written outside of the data flow analysis region.
    ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
    if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
    {
        return;
    }
}

El primer bucle foreach examina cada declaración de variable con análisis sintácticos La primera comprobación garantiza que la variable tiene un inicializador. La segunda comprobación garantiza que el inicializador es una constante. El segundo bucle tiene el análisis semántico original. Las comprobaciones semánticas se encuentran en un bucle independiente porque afectan más al rendimiento. Vuelva a ejecutar las pruebas y observará que todas pasan.

Adición de un retoque final

Casi ha terminado. Hay algunas condiciones más que el analizador tiene que cumplir. Visual Studio llama a los analizadores mientras el usuario escribe el código. Suele darse el caso de que se llama al analizador para código que no compila. El método AnalyzeNode del analizador de diagnóstico no comprueba si el valor de constante se puede convertir al tipo de variable. Por tanto, la implementación actual convertirá de forma adecuada una declaración incorrecta, como int i = "abc", en una constante local. Agregue un método de prueba para este caso:

        [TestMethod]
        public async Task DeclarationIsInvalid_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

Además, los tipos de referencia no se controlan correctamente. El único valor de constante permitido para un tipo de referencia es null, excepto en el caso de System.String, que admite los literales de cadena. En otras palabras, const string s = "abc" es legal, pero const object s = "abc" no lo es. Este fragmento de código comprueba esa condición:

        [TestMethod]
        public async Task DeclarationIsNotString_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

Para ser exhaustivo, debe agregar otra prueba para asegurarse de que puede crear una declaración de constante para una cadena. El fragmento de código siguiente define el código que genera el diagnóstico y el código después de haber aplicado la corrección:

        [TestMethod]
        public async Task StringCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|string s = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string s = ""abc"";
    }
}
");
        }

Por último, si una variable se declara con la palabra clave var, la corrección de código hace una función incorrecta y genera una declaración const var, que el lenguaje C# no admite. Para corregir este error, la corrección de código debe reemplazar la palabra clave var por el nombre del tipo deducido:

        [TestMethod]
        public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = 4;|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int item = 4;
    }
}
");
        }

        [TestMethod]
        public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string item = ""abc"";
    }
}
");
        }

Afortunadamente, todos los errores anteriores se pueden tratar con las mismas técnicas que acaba de aprender.

Para corregir el primer error, abra primero MakeConstAnalyzer.cs y busque el bucle foreach donde se comprueban todos los inicializadores de la declaración local para asegurarse de que se les hayan asignado valores constantes. Inmediatamente antes del primer bucle foreach, llame a context.SemanticModel.GetTypeInfo() para recuperar información detallada sobre el tipo declarado de la declaración local:

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

Después, dentro del bucle foreach, compruebe cada inicializador para asegurarse de que se puede convertir al tipo de variable. Agregue la siguiente comprobación después de asegurarse de que el inicializador es una constante:

// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
    return;
}

El siguiente cambio se basa en el último. Antes de cerrar la llave del primer bucle foreach, agregue el código siguiente para comprobar el tipo de declaración local cuando la constante es una cadena o null.

// Special cases:
//  * If the constant value is a string, the type of the local declaration
//    must be System.String.
//  * If the constant value is null, the type of the local declaration must
//    be a reference type.
if (constantValue.Value is string)
{
    if (variableType.SpecialType != SpecialType.System_String)
    {
        return;
    }
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
    return;
}

Debe escribir algo más de código en el proveedor de corrección de código para reemplazar la palabra clave var por el nombre de tipo correcto. Vuelva a MakeConstCodeFixProvider.cs. El código que se va a agregar realiza los pasos siguientes:

  • Compruebe si la declaración es una declaración var y, en su caso:
  • Cree un tipo para el tipo deducido.
  • Asegúrese de que la declaración de tipo no es un alias. Si es así, es válido declarar const var.
  • Asegúrese de que var no es un nombre de tipo en este programa. (Si es así, const var es válido).
  • Simplificación del nombre de tipo completo

Parece mucho código. Pero no lo es. Reemplace la línea que declara e inicializa newLocal con el código siguiente. Va inmediatamente después de la inicialización de newModifiers:

// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
    SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

    // Special case: Ensure that 'var' isn't actually an alias to another type
    // (e.g. using var = System.String).
    IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
    if (aliasInfo == null)
    {
        // Retrieve the type inferred for var.
        ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;

        // Special case: Ensure that 'var' isn't actually a type named 'var'.
        if (type.Name != "var")
        {
            // Create a new TypeSyntax for the inferred type. Be careful
            // to keep any leading and trailing trivia from the var keyword.
            TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
                .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
                .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

            // Add an annotation to simplify the type name.
            TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);

            // Replace the type in the variable declaration.
            variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
        }
    }
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
                           .WithDeclaration(variableDeclaration);

Deberá agregar una directiva using para usar el tipo Simplifier:

using Microsoft.CodeAnalysis.Simplification;

Ejecute las pruebas, y todas deberían pasar. Felicítese por ejecutar el analizador terminado. Presione Ctrl+F5 para ejecutar el proyecto de analizador en una segunda instancia de Visual Studio con la extensión de la versión preliminar de Roslyn cargada.

  • En la segunda instancia de Visual Studio, cree un proyecto de aplicación de consola de C# y agregue int x = "abc"; al método Main. Gracias a la primera corrección de errores, no se debe notificar ninguna advertencia para esta declaración de variable local (aunque hay un error del compilador según lo esperado).
  • A continuación, agregue object s = "abc"; al método Main. Debido a la segunda corrección de errores, no se debe notificar ninguna advertencia.
  • Por último, agregue otra variable local que usa la palabra clave var. Observará que se notifica una advertencia y que aparece una sugerencia debajo a la izquierda.
  • Mueva el símbolo de intercalación del editor sobre el subrayado ondulado y presione Ctrl+.. para mostrar la corrección de código sugerida. Al seleccionar la corrección de código, tenga en cuenta que la palabra clave var ahora se trata correctamente.

Por último, agregue el código siguiente:

int i = 2;
int j = 32;
int k = i + j;

Después de estos cambios, obtendrá un subrayado ondulado rojo solo en las dos primeras variables. Agregue const a i y j, y obtendrá una nueva advertencia sobre k porque ahora puede ser const.

¡Enhorabuena! Ha creado su primera extensión de .NET Compiler Platform que realiza un análisis de código sobre la marcha para detectar un problema y proporciona una solución rápida para corregirlo. Durante el proceso, ha aprendido muchas de las API de código que forman parte del SDK de .NET Compiler Platform (API de Roslyn). Puede comprobar su trabajo con el ejemplo completo en nuestro repositorio de ejemplos de GitHub.

Otros recursos