Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
El SDK de .NET Compiler Platform proporciona las herramientas necesarias para crear diagnósticos personalizados (analizadores), correcciones de código, refactorización de código y supresores de diagnóstico que tienen como destino código de C# o Visual Basic. Un analizador contiene código que reconoce las infracciones de la regla. La corrección de código contiene el código que corrige la infracción. Las reglas que implemente pueden ser cualquier cosa, desde la estructura de código hasta el estilo de codificación a 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 mediante las API de Roslyn. Un analizador es una manera de realizar análisis de código fuente e informar de 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. En este tutorial se crea un analizador que busca declaraciones de variables locales que se podrían declarar mediante el const modificador, pero no. La corrección de código adjunta modifica esas declaraciones para agregar el const modificador.
Prerrequisitos
- Visual Studio 2019 , versión 16.8 o posterior
Deberá instalar el SDK de .NET Compiler Platform mediante el Instalador de Visual Studio:
Instrucciones de instalación: Instalador de Visual Studio
Hay dos maneras diferentes de encontrar el SDK de .NET Compiler Platform en el Instalador de Visual Studio:
Instalación mediante el Instalador de Visual Studio: vista Cargas de trabajo
El SDK de .NET Compiler Platform no se selecciona automáticamente como parte del conjunto de herramientas de desarrollo de extensiones de Visual Studio. Deba seleccionarlo como un componente opcional.
- Ejecución del instalador de Visual Studio
- Selección de Modificar
- Active la carga de trabajo Desarrollo de extensiones de Visual Studio.
- Abra el nodo desarrollo de extensiones de Visual Studio en el árbol de resumen.
- Marque la casilla para .NET Compiler Platform SDK. Lo encontrará en último lugar en los componentes opcionales.
Opcionalmente, también querrá que el editor DGML muestre gráficos en el visualizador:
- Abra el nodo Componentes individuales en el árbol de resumen.
- Active la casilla Editor de DGML.
Instalación mediante la pestaña Instalador de Visual Studio: componentes individuales
- Ejecución del instalador de Visual Studio
- Selección de Modificar
- Seleccione la pestaña Componentes individuales.
- Marque la casilla para .NET Compiler Platform SDK. Lo encontrará en la parte superior de la sección Compiladores, herramientas de compilación y entornos de ejecución .
Opcionalmente, también querrá que el editor DGML muestre gráficos en el visualizador:
- Active la casilla Editor de DGML. La encontrará en la sección Herramientas de código .
Hay varios pasos para crear y validar el analizador:
- Cree la solución.
- Registre el nombre y la descripción del analizador.
- Advertencias y recomendaciones del analizador de informes.
- Implemente la corrección de código para aceptar recomendaciones.
- Mejore el análisis mediante 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 Extensibilidad de Visual C#>, 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
El analizador con plantilla de corrección de código crea cinco proyectos:
- MakeConst, que contiene el analizador.
- MakeConst.CodeFixes, que contiene la corrección de código.
- MakeConst.Package, que se usa para 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 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 en el entorno de .NET Framework (Visual Studio).
Sugerencia
Al ejecutar el analizador, se inicia una segunda copia de Visual Studio. Esta segunda copia usa un subárbol del Registro diferente para almacenar la configuración. Esto le permite diferenciar la configuración visual en las dos copias de Visual Studio. Puede elegir otro tema 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. Esto mantiene la configuración diferente.
El subárbol incluye no solo el analizador en desarrollo, sino también los analizadores anteriores abiertos. Para restablecer Roslyn hive, debe eliminarlo manualmente de %LocalAppData%\Microsoft\VisualStudio. El nombre de carpeta de Roslyn hive finalizará 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 nuevo proyecto de aplicación de consola de C# (cualquier marco de trabajo de destino funcionará; los analizadores funcionan en el nivel de origen). Mantenga el puntero sobre el token con un subrayado ondulado y aparecerá el texto de advertencia proporcionado por un analizador.
La plantilla crea un analizador que informa de una advertencia en cada declaración de tipo donde el nombre de tipo contiene letras minúsculas, como se muestra en la ilustración siguiente:
La plantilla también proporciona una corrección de código que cambia cualquier nombre de tipo que contenga caracteres en minúsculas a mayúsculas. Puede hacer clic en la bombilla mostrada con la advertencia para ver los cambios sugeridos. Aceptar los cambios sugeridos actualiza el nombre de tipo y todas las referencias a ese tipo en la solución. Ahora que ha visto el analizador inicial en acción, cierre la segunda instancia de Visual Studio y vuelva al proyecto del analizador.
No es necesario iniciar una segunda copia de Visual Studio y crear código nuevo para probar cada cambio en el analizador. La plantilla también crea un proyecto de prueba unitaria automáticamente. Ese proyecto contiene dos pruebas.
TestMethod1 muestra el formato típico de una prueba que analiza el código sin desencadenar un diagnóstico.
TestMethod2 muestra el formato de una prueba que desencadena un diagnóstico y, a continuación, 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 para analizadores son mucho más rápidas que probarlas de forma interactiva con Visual Studio.
Sugerencia
Las pruebas unitarias del analizador son una excelente herramienta cuando 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 excelente herramienta para explorar y encontrar construcciones que es posible que aún no haya pensado.
En este tutorial, escribirá un analizador que informa al usuario de las declaraciones de variables locales que se pueden convertir en constantes locales. Por ejemplo, considere el código siguiente:
int x = 0;
Console.WriteLine(x);
En el código anterior, x se le asigna un valor constante y nunca se modifica. Se puede declarar mediante el const modificador :
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. La plataforma del compilador de .NET proporciona API que facilitan la realización de este análisis.
Creación de registros de analizadores
La plantilla crea la clase inicial DiagnosticAnalyzer , en el archivo MakeConstAnalyzer.cs . Este analizador inicial muestra dos propiedades importantes de cada analizador.
- Cada analizador de diagnóstico debe proporcionar un
[DiagnosticAnalyzer]atributo en el que se describe el idioma en el que opera. - Cada analizador de diagnóstico debe derivar (directa o indirectamente) de la DiagnosticAnalyzer clase .
La plantilla también muestra las características básicas que forman parte de cualquier analizador:
- Registrar acciones. Las acciones representan los cambios de código que deben desencadenar el analizador para examinar el código en busca de infracciones. Cuando Visual Studio detecta modificaciones de código que coinciden con una acción registrada, llama al método registrado del analizador.
- Cree diagnósticos. Cuando el analizador detecta una infracción, crea un objeto de diagnóstico que Visual Studio usa para notificar al usuario la infracción.
Registre acciones en la invalidación del método DiagnosticAnalyzer.Initialize(AnalysisContext). En este tutorial, visitará los nodos de sintaxis que buscan declaraciones locales y verá cuál de ellos tiene valores constantes. Si una declaración podría 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. Debería seguir esa práctica para hacer más fácil la localización. Abra el archivo Resources.resx para el proyecto del analizador MakeConst . Esto muestra el editor de recursos. Actualice los recursos de cadena de la siguiente manera:
- Cambia
AnalyzerDescriptiona "Variables that are not modified should be made constants.". - Cambia
AnalyzerMessageFormata "Variable '{0}' can be made constant". - Cambia
AnalyzerTitlea "Variable can be made constant".
Cuando haya terminado, el editor de recursos debería aparecer como se muestra en la ilustración siguiente:
Los cambios restantes se encuentran en el archivo del analizador. Abra MakeConstAnalyzer.cs en Visual Studio. Cambie la acción registrada de uno que actúa en símbolos a uno que actúe en la sintaxis. En el MakeConstAnalyzerAnalyzer.Initialize método , busque la línea que registra la acción en símbolos:
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
Reemplácelo por la línea siguiente:
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);
Después de ese cambio, puede eliminar el AnalyzeSymbol método . Este analizador examina las instrucciones SyntaxKind.LocalDeclarationStatement, no las de SymbolKind.NamedType. Tenga en cuenta que AnalyzeNode tiene un subrayado ondulado rojo debajo. El código que acaba de agregar hace referencia a un AnalyzeNode método que no se ha declarado. Declare ese método con el código siguiente:
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}
Cambie el Category por "Usage" en MakeConstAnalyzer.cs como se muestra en el siguiente código:
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 AnalyzeNode método. Debe buscar una sola declaración local que podría ser const, pero que no lo es, como el siguiente código:
int x = 0;
Console.WriteLine(x);
El primer paso es buscar 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 AnalyzeNode método . A continuación, compruebe la declaración en busca de modificadores const. Si los encuentra, vuelva inmediatamente. El código siguiente busca cualquier const modificador en la declaración local:
// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
return;
}
Por último, debe comprobar que la variable podría ser const. Esto significa asegurarse de que nunca se asigna después de haber sido inicializado.
Realizará algún tipo de análisis semántico mediante el SyntaxNodeAnalysisContext. Use el context argumento para determinar si se puede realizar constla declaración de variable local. Un Microsoft.CodeAnalysis.SemanticModel representa toda la información semántica en un único 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 asegurarse de que la variable local no se escribe con un nuevo valor en cualquier otro lugar. Llame al GetDeclaredSymbol miembro de extensión para recuperar el ILocalSymbol de la variable y compruebe que no está incluida en la DataFlowAnalysis.WrittenOutside colección en el análisis de flujo de datos. Agregue el código siguiente al final del AnalyzeNode método :
// 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 que acaba de agregar garantiza que la variable no se modifique y, por tanto, se puede realizar const. Es el momento de elevar el diagnóstico. Agregue el código siguiente como la última línea de 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, a continuación, agregar el código de prueba siguiente:
int x = 0;
Console.WriteLine(x);
Debería aparecer la bombilla y el analizador debe notificar un diagnóstico. Sin embargo, en función de la versión de Visual Studio, verá:
- 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" encontró 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
TypeDeclarationSyntaxen vez de los elementosLocalDeclarationStatementSyntax.
En la sección siguiente se explica cómo escribir la corrección de código.
Escribir 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 creó, puede proporcionar una corrección de código que inserte 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 al identificador de diagnóstico generado por el analizador de diagnóstico, pero aún no implementa la transformación de código correcta.
A continuación, elimine el MakeUppercaseAsync método . Ya no se aplica.
Todos los proveedores de correcciones de código derivan de CodeFixProvider. Todas sobrescriben CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) para informar sobre soluciones de código disponibles. En RegisterCodeFixesAsync, cambie el tipo de nodo antecesor que está buscando a un 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 nuevo documento que resulta de agregar el const modificador 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 MakeConstAsync método transformará el Document que representa el archivo de origen del usuario en un nuevo Document que ahora contiene una const declaración.
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 const token a la declaración mediante el código siguiente:
// 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);
A continuación, dé formato a la nueva declaración para que coincida con las reglas de formato de C#. Dar formato a los cambios para que coincidan con el código existente crea una mejor experiencia. Agregue la siguiente instrucción 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 using directiva a la parte superior del archivo:
using Microsoft.CodeAnalysis.Formatting;
El último paso es realizar la edición. Hay tres pasos para este proceso:
- Obtenga un identificador para el documento existente.
- Cree un nuevo documento reemplazando la declaración existente por la nueva declaración.
- Devuelve el nuevo documento.
Agregue el código siguiente al final del MakeConstAsync método :
// 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 probar. Presione F5 para ejecutar el proyecto del analizador en una segunda instancia de Visual Studio. En la segunda instancia de Visual Studio, cree un nuevo proyecto aplicación de consola de C# y agregue algunas declaraciones de variables locales inicializadas con valores constantes al método Main. Verá que se notifican como advertencias, tal como se indica a continuación.
Ha avanzado mucho. Hay subrayados ondulados debajo de las declaraciones que pueden convertirse en const. Pero todavía hay trabajo que hacer. Esto funciona bien si agrega const a las declaraciones comenzando con i, luego j y finalmente k. Sin embargo, si agrega el const modificador en un orden diferente, empezando con k, el analizador crea errores: k no se puede declarar const, a menos que i y j ya sean const. Tiene que realizar más análisis para asegurarse de controlar las distintas formas en que se pueden declarar e inicializar variables.
Crear 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 numerosas posibles declaraciones en las cuales 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 un analizador y una prueba unitaria de corrección de código.
TestMethod1 muestra el patrón de una prueba que garantiza que el analizador no informe de un diagnóstico cuando no debería hacerlo.
TestMethod2 muestra el patrón para notificar un diagnóstico y ejecutar la corrección de código.
La plantilla usa paquetes Microsoft.CodeAnalysis.Testing para pruebas unitarias.
Sugerencia
La biblioteca de pruebas admite una sintaxis de marcado especial, incluida la siguiente:
-
[|text|]: indica que se notifica un diagnóstico paratext. De forma predeterminada, este formulario solo se puede usar para probar analizadores con exactamente unoDiagnosticDescriptorproporcionado porDiagnosticAnalyzer.SupportedDiagnostics. -
{|ExpectedDiagnosticId:text|}: indica que se notifica un diagnóstico con IdExpectedDiagnosticIdparatext.
Reemplace las pruebas de plantilla de la MakeConstUnitTest clase por el siguiente método de prueba:
[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 seleccionando Prueba>Windows>Explorador de pruebas. A continuación, seleccione Ejecutar todo.
Creación de pruebas para declaraciones válidas
Como regla general, los analizadores deben salir lo antes posible, realizando un trabajo mínimo. Visual Studio llama a 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 gestiona varias de esas pruebas. Agregue los siguientes métodos de prueba para representar esos casos:
[TestMethod]
public async Task VariableIsAssigned_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i = 0;
Console.WriteLine(i++);
}
}
");
}
[TestMethod]
public async Task VariableIsAlreadyConst_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
const int i = 0;
Console.WriteLine(i);
}
}
");
}
[TestMethod]
public async Task NoInitializer_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i;
i = 0;
Console.WriteLine(i);
}
}
");
}
Estas pruebas se superan porque el analizador ya controla estas condiciones:
- El análisis de flujo de datos detecta variables asignadas después de la inicialización.
- Las declaraciones que ya están filtradas por
constse filtran al verificar la presencia de la palabra claveconst. - El análisis de flujo de datos controla las declaraciones sin inicializadores que detectan asignaciones fuera de la declaración.
A continuación, agregue métodos de prueba para las condiciones que aún no ha controlado:
Declaraciones en las que 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 aún más complicado porque C# permite varias declaraciones como una instrucción. Tenga en cuenta la siguiente constante de cadena de texto del 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 se puede hacer 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 verá que se producen errores en estos dos últimos casos de prueba.
Actualización del analizador para omitir las declaraciones correctas
Necesitas algunas mejoras en el método de tu analizador AnalyzeNode para filtrar el código que coincida con estas condiciones. Son todas las condiciones relacionadas, por lo que los cambios similares corregirán todas estas condiciones. Realice los siguientes cambios en AnalyzeNode:
- El análisis semántico examinó una sola declaración de variable. Este código debe estar en un
foreachbucle que examine todas las variables declaradas en la misma instrucción. - Cada variable declarada debe tener un inicializador.
- El inicializador de cada variable declarada debe ser una constante en 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;
}
con 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 foreach bucle examina cada declaración de variable mediante el análisis sintáctico. 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 están en un bucle independiente porque tiene un mayor impacto en el rendimiento. Vuelva a ejecutar las pruebas y observará que todas pasan.
Dar los toques finales
Casi ha terminado. Hay algunas condiciones más que el analizador tiene que cumplir. Visual Studio llama a los analizadores mientras el usuario escribe 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 lo tanto, la implementación actual convertirá felizmente 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 constante permitido para un tipo de referencia es null, excepto en el caso de System.String, que permite 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 aplicar 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 var palabra clave , la corrección de código realiza lo incorrecto y genera una const var declaración, que no es compatible con el lenguaje C#. Para corregir este error, la corrección de código debe reemplazar la var palabra clave por el nombre del tipo inferido:
[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 solucionar con las mismas técnicas que acaba de aprender.
Para corregir el primer error, primero abra MakeConstAnalyzer.cs y busque el bucle foreach donde se comprueba cada uno de los inicializadores de la declaración local para asegurarse de que están asignados con valores constantes. Inmediatamente antes del primer bucle foreach, llame context.SemanticModel.GetTypeInfo() a 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 un poco más de código en su proveedor de soluciones de código para reemplazar la palabra clave var por el nombre de tipo correcto. Vuelva a MakeConstCodeFixProvider.cs. El código que agregará realiza los pasos siguientes:
- Compruebe si la declaración es una
vardeclaración y si es: - Cree un nuevo tipo para el tipo inferido.
- Asegúrese de que la declaración de tipo no es un alias. Si es así, es legal declarar
const var. - Asegúrese de que
varno sea un nombre de tipo en este programa. (Si es así,const vares legal). - Simplificación del nombre de tipo completo
Eso suena como un montón de código. Pero no lo es. Reemplace la línea que declara e inicializa newLocal por 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 using directiva para usar el Simplifier tipo :
using Microsoft.CodeAnalysis.Simplification;
Ejecute sus pruebas, y todas deberían aprobarse. Felicítese por ejecutar el analizador terminado. Presione Ctrl+F5 para ejecutar el proyecto del analizador en una segunda instancia de Visual Studio con la extensión Roslyn Preview cargada.
- En la segunda instancia de Visual Studio, cree un nuevo 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 previsto). - 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 use la
varpalabra clave . Verá que se notifica una advertencia y aparece una sugerencia debajo, hacia la izquierda. - Mueva el cursor del editor sobre el subrayado ondulado y pulse 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
varahora 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 jy obtenga una nueva advertencia en k porque ahora puede ser const.
¡Felicidades! Ha creado la primera extensión de .NET Compiler Platform que realiza análisis de código sobre la marcha para detectar un problema y proporciona una corrección rápida para corregirlo. A lo largo del proceso, ha aprendido muchas de las API de código que forman parte del SDK de la plataforma del compilador de .NET (API de Roslyn). Puede comprobar el trabajo con el ejemplo completado en nuestro repositorio de GitHub de ejemplos.