El soporte de Visual Studio 2019 en PVS-Studio afectó una serie de componentes: el complemento en sí, el analizador de línea de comandos, los núcleos de los analizadores C ++ y C #, y algunas utilidades. En este artículo, explicaré brevemente qué problemas encontramos al implementar el soporte del IDE y cómo los abordamos.
Antes de comenzar, me gustaría repasar la historia del soporte de las versiones anteriores de Visual Studio en PVS-Studio para que comprenda mejor nuestra visión de la tarea y las soluciones que surgieron en cada situación.
Desde la primera versión de PVS-Studio que se envió con un complemento para Visual Studio (era Visual Studio 2005 en aquel entonces), admitir nuevas versiones de este IDE ha sido una tarea bastante trivial para nosotros, que básicamente se redujo a actualizar el proyecto del complemento archivo y dependencias de las diversas extensiones de API de Visual Studio. De vez en cuando tendríamos que agregar soporte para nuevas características de C ++, con las cuales el compilador de Visual C ++ estaba aprendiendo gradualmente a trabajar, pero en general tampoco fue una tarea difícil y podría hacerse fácilmente justo antes de un nuevo lanzamiento de Visual Studio . Además, PVS-Studio solo tenía un analizador en ese momento, para C / C ++.
Las cosas cambiaron cuando se lanzó Visual Studio 2017. Además de los grandes cambios en muchas de las extensiones de API del IDE, también encontramos un problema con el mantenimiento de la compatibilidad con versiones anteriores del nuevo analizador C # agregado poco antes (así como de la nueva capa del analizador para que C ++ funcione con proyectos de MSBuild) Nuevas versiones de MSBuild \ Visual Studio.
Teniendo en cuenta todo esto, le recomiendo encarecidamente que vea un artículo relacionado sobre el soporte de Visual Studio 2017, "
Soporte de Visual Studio 2017 y Roslyn 2.0 en PVS-Studio: a veces no es tan fácil usar soluciones preparadas como puede parecer ", antes de seguir leyendo. Ese artículo analiza los problemas que enfrentamos la última vez y el modelo de interacción entre diferentes componentes (como PVS-Studio, MSBuild y Roslyn). Conocer estos detalles puede ayudarlo a comprender mejor el artículo actual.
Afrontar esos problemas en última instancia condujo a cambios significativos en el analizador, y esperábamos que los nuevos enfoques aplicados nos ayudaran a admitir futuras versiones de Visual Studio \ MSBuild mucho más fácil y más rápido. Esta esperanza ya comenzó a ser realista a medida que se lanzaron las numerosas actualizaciones de Visual Studio 2017. ¿El nuevo enfoque nos ayudó a apoyar Visual Studio 2019? Sigue leyendo para averiguarlo.
Complemento PVS-Studio para Visual Studio 2019
El comienzo parecía ser prometedor. No nos llevó mucho esfuerzo portar el complemento a Visual Studio 2019 y ejecutarlo y ejecutarlo bien. Pero ya encontramos dos problemas a la vez que podrían traer más problemas más adelante.
El primero tenía que ver con la interfaz
IVsSolutionWorkspaceService utilizada para admitir el modo Lightweight Solution Load (que, por cierto, se había deshabilitado en una de las actualizaciones anteriores, en Visual Studio 2017). Estaba decorado con el atributo
Desaprobado , que actualmente solo activaba una advertencia en el momento de la compilación, pero que se convertiría en un gran problema en el futuro. Este modo no duró mucho ... Eso fue fácil de solucionar, simplemente dejamos de usar esta interfaz.
El segundo problema fue el siguiente mensaje que recibimos al cargar Visual Studio con el complemento habilitado:
Visual Studio ha detectado una o más extensiones que están en riesgo o que no funcionan en una actualización VS de función.Los registros de los lanzamientos de Visual Studio (el archivo ActivityLog) ayudaron a aclararlo:
Advertencia: La extensión 'PVS-Studio' usa la característica 'carga automática síncrona' de Visual Studio. Esta característica ya no será compatible en una futura actualización de Visual Studio 2019, momento en el que esta extensión no funcionará. Póngase en contacto con el proveedor de la extensión para obtener una actualización.Lo que significaba para nosotros era que tendríamos que cambiar del modo de carga síncrono a asíncrono. Espero que no le importe si le ahorro los detalles de cómo interactuamos con las interfaces COM de Visual Studio, y solo describo brevemente los cambios.
Hay un artículo de Microsoft sobre la carga de complementos de forma asincrónica: "
Cómo: usar AsyncPackage para cargar VSPackages en segundo plano ". Sin embargo, ya estaba claro que había más cambios por venir.
Uno de los mayores cambios fue en el modo de carga, o más bien en el modo de inicialización. En versiones anteriores, toda la inicialización necesaria se realizaba utilizando dos métodos:
Inicializar nuestra clase heredando de
Package y
OnShellPropertyChange . Este último tuvo que agregarse porque cuando se cargaba sincrónicamente, Visual Studio podría estar en proceso de carga e inicialización y, por lo tanto, algunas de las acciones necesarias eran imposibles de realizar durante la inicialización del complemento. Una forma de solucionar esto fue retrasar la ejecución de esas acciones hasta que Visual Studio salga del estado 'zombie'. Fue esta parte de la lógica que seleccionamos en el método
OnShellPropertyChange con una verificación del estado 'zombie'.
El método
Initialize de la clase abstracta
AsyncPackage , que carga los complementos de forma asíncrona
heredada , está
sellado , por lo que la inicialización debe hacerse en el método reemplazado
InitializeAsync , que es exactamente lo que hicimos. La lógica de verificación 'zombie' también tuvo que cambiarse porque la información de estado ya no estaba disponible para nuestro complemento. Además, todavía teníamos que realizar esas acciones que debían realizarse después de la inicialización del complemento.
Resolvimos eso utilizando el método
OnPackageLoaded de la interfaz
IVsPackageLoadEvents , que es donde se realizaron esas acciones retrasadas.
Otro problema resultante de la carga asincrónica fue que los comandos del complemento no se podían usar hasta después de que Visual Studio se hubiera cargado. Al abrir el registro del analizador haciendo doble clic en el administrador de archivos (si necesita abrirlo desde Visual Studio), se inició la versión correspondiente de devenv.exe con un comando para abrir el registro. El comando de lanzamiento se parecía a esto:
"C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\Common7\IDE\devenv.exe" /command "PVSStudio.OpenAnalysisReport C:\Users\vasiliev\source\repos\ConsoleApp\ConsoleApp.plog"
El indicador "/ command" se usa aquí para ejecutar el comando registrado en Visual Studio. Este enfoque ya no funcionó ya que los comandos ya no estaban disponibles hasta después de que se cargara el complemento. La solución que se nos ocurrió fue analizar el comando de lanzamiento devenv.exe después de que el complemento se haya cargado y ejecutar el comando abrir registro si se encuentra en el comando de lanzamiento. Por lo tanto, descartar la idea de usar la interfaz "apropiada" para trabajar con comandos nos permitió mantener la funcionalidad necesaria, con la apertura retrasada del registro después de que el complemento se haya cargado por completo.
Uf, parece que lo hicimos por fin; el complemento se carga y se abre como se esperaba, sin advertencias.
Y aquí es cuando las cosas van mal. Paul (¡Hola Paul!) Instala el complemento en su computadora y pregunta por qué todavía no hemos cambiado a carga asincrónica.
Decir que nos sorprendió sería quedarse corto. Eso no puede ser! Pero es real: aquí está la nueva versión del complemento, y aquí hay un mensaje que dice que el paquete se está cargando sincrónicamente. Alexander (¡Hola, Alexander!) Y pruebo la misma versión en nuestras respectivas computadoras, funciona bien. ¿Cómo es eso posible? Luego se nos ocurre verificar las versiones de las bibliotecas PVS-Studio cargadas en Visual Studio, y encontramos que estas son las bibliotecas para Visual Studio 2017, mientras que el paquete VSIX contiene las nuevas versiones, es decir, para Visual Studio 2019.
Después de jugar con VSIXInstaller por un tiempo, logramos descubrir que el problema tenía que ver con la caché de paquetes. Esta teoría también fue apoyada por el hecho de que restringir el acceso al paquete en caché (C: \ ProgramData \ Microsoft \ VisualStudio \ Packages) causado por VSIXInstaller para enviar un mensaje de error en el registro. Curiosamente, cuando no se produjo el error, no apareció la información sobre la instalación de paquetes en caché.
Nota al margen . Mientras estudiaba el comportamiento del instalador de VSIX y las bibliotecas que lo acompañan, pensé que es genial que Roslyn y MSBuild sean de código abierto, lo que le permite leer y depurar convenientemente su código y rastrear su lógica de trabajo.
Entonces, esto es lo que sucedió: al instalar el complemento, el instalador de VSIX vio que el paquete correspondiente ya estaba en caché (en realidad era el paquete .vsix para Visual Studio 2017) e instaló ese paquete en lugar del nuevo. Por qué ignoró las restricciones / requisitos definidos en el archivo .vsixmanifest (que, entre otras cosas, restringió la instalación de extensiones a una versión específica de Visual Studio) es una pregunta que aún no se ha respondido. Como resultado, el complemento diseñado para Visual Studio 2017 se instaló en Visual Studio 2019, a pesar de las restricciones especificadas en el archivo .vsixmanifest.
Lo peor de todo, esa instalación rompió el gráfico de dependencias de Visual Studio, y aunque el IDE parecía estar funcionando bien, las cosas eran realmente terribles. No pudo instalar o eliminar extensiones, actualizaciones, etc. El proceso de "restauración" también fue doloroso ya que tuvimos que eliminar la extensión (es decir, los archivos que la contenían) manualmente y, también manualmente, editar los archivos de configuración que almacenan la información sobre el paquete instalado. En otras palabras, no fue divertido en absoluto.
Para solucionar eso y asegurarnos de que no nos encontremos en situaciones como esa en el futuro, decidimos hacer nuestro propio GUID para que el nuevo paquete tenga los paquetes para Visual Studio 2017 y Visual Studio 2019 aislados de forma segura ( los paquetes más antiguos estaban bien; siempre habían usado un GUID compartido).
Desde que comenzamos a hablar de sorpresas desagradables, aquí hay otra: después de actualizar a la Vista previa 2, el menú PVS-Studio "se movió" a la pestaña "Extensiones". No es un gran problema, pero hizo que acceder a la funcionalidad del complemento fuera menos conveniente. Este comportamiento persistió durante las próximas versiones de Visual Studio 2019, incluida la versión. No he encontrado menciones de esta "característica" ni en la documentación ni en el blog.
Bien, ahora las cosas se veían bien y parecía que finalmente habíamos terminado con el soporte de Visual Studio 2019. Esto resultó incorrecto al día siguiente después de lanzar PVS-Studio 7.02. Era el modo de carga asincrónica nuevamente. Al abrir la ventana de resultados del análisis (o comenzar el análisis), la ventana del analizador aparecerá "vacía" para el usuario: sin botones, sin cuadrícula, nada en absoluto.
De hecho, este problema ocurrió de vez en cuando durante el análisis. Pero afectó solo a una computadora y no apareció hasta que Visual Studio se actualizó a una de las primeras iteraciones de 'Vista previa'. Sospechamos que algo se había roto durante la instalación o actualización. Sin embargo, el problema desapareció algún tiempo después y no ocurriría incluso en esa computadora en particular, por lo que pensamos que "se solucionó por sí solo". Pero no, tuvimos suerte. O desafortunado, para el caso.
Como descubrimos, fue el orden en que se inicializó la ventana IDE (la clase derivada de
ToolWindowPane ) y su contenido (nuestro control con la cuadrícula y los botones). Bajo ciertas condiciones, el control se inicializaría antes del panel y, aunque las cosas funcionaban bien y el método
FindToolWindowAsync (crear la ventana cuando se accede por primera vez) funcionaba bien, el control permaneció invisible. Lo arreglamos agregando una inicialización diferida para nuestro control al código de relleno de panel.
Soporte de C # 8.0
Hay una gran ventaja sobre el uso de Roslyn como base para el analizador: no tiene que agregar soporte para nuevas construcciones de lenguaje de forma manual: se realiza automáticamente a través de las bibliotecas de análisis de código de Microsoft, y solo utilizamos las soluciones listas para usar. Significa que la nueva sintaxis es compatible simplemente actualizando las bibliotecas.
En cuanto al análisis en sí, tuvimos que ajustar las cosas por nuestra cuenta, por supuesto, en particular, manejar nuevas construcciones de lenguaje. Claro, teníamos el nuevo árbol de sintaxis generado automáticamente simplemente actualizando Roslyn, pero aún teníamos que enseñarle al analizador cómo interpretar y procesar exactamente los nodos de árbol de sintaxis nuevos o modificados.
Los tipos de referencia anulables son quizás la nueva característica más discutida de C # 8. No voy a hablar de ellos ahora porque un tema tan grande vale un artículo separado (que se está escribiendo actualmente). Por ahora, hemos decidido ignorar las anotaciones anulables en nuestro mecanismo de flujo de datos (es decir, entendemos, analizamos y omitimos). La idea es que a una variable, incluso de un tipo de referencia no anulable, se le puede asignar bastante fácilmente (o accidentalmente) el valor
nulo , terminando con un NRE al intentar desreferenciarlo. Nuestro analizador puede detectar tales errores e informar una posible desreferencia nula (si encuentra tal asignación en el código, por supuesto) incluso si la variable es de tipo referencia no anulable.
El uso de tipos de referencia anulables y la sintaxis asociada le permite escribir código bastante interesante. Lo llamamos "sintaxis emocional". Este fragmento es perfectamente compilable:
obj.Calculate(); obj?.Calculate(); obj.Calculate(); obj!?.Calculate(); obj!!!.Calculate();
Por cierto, mis experimentos me llevaron a descubrir un par de trucos que puedes usar para "bloquear" Visual Studio usando la nueva sintaxis. Se basan en el hecho de que puedes escribir tantos '!' personajes como quieras Significa que podría escribir no solo código como este:
object temp = null!
pero también así:
object temp = null!!!;
Y, empujándolo aún más, podrías escribir cosas locas como esta:
object temp = null!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!;
Este código es compilable, pero si intenta ver el árbol de sintaxis en Syntax Visualizer desde .NET Compiler Platform SDK, Visual Studio se bloqueará.
El informe de fallas se puede extraer del Visor de eventos:
Faulting application name: devenv.exe, version: 16.0.28803.352, time stamp: 0x5cc37012 Faulting module name: WindowsBase.ni.dll, version: 4.8.3745.0, time stamp: 0x5c5bab63 Exception code: 0xc00000fd Fault offset: 0x000c9af4 Faulting process id: 0x3274 Faulting application start time: 0x01d5095e7259362e Faulting application path: C:\Program Files (x86)\ Microsoft Visual Studio\2019\Community\Common7\IDE\devenv.exe Faulting module path: C:\WINDOWS\assembly\NativeImages_v4.0.30319_32\ WindowsBase\4480dfedf0d7b4329838f4bbf953027d\WindowsBase.ni.dll Report Id: 66d41eb2-c658-486d-b417-02961d9c3e4f Faulting package full name: Faulting package-relative application ID:
Si te vuelves aún más loco y agregas varias veces más signos de exclamación, Visual Studio comenzará a fallar por sí solo, sin la ayuda de Syntax Visualizer. Las bibliotecas Microsoft.CodeAnalysis y el compilador csc.exe tampoco pueden hacer frente a dicho código.
Estos ejemplos son artificiales, por supuesto, pero ese truco me pareció divertido.
Conjunto de herramientas
Era obvio que actualizar el conjunto de herramientas sería la parte más difícil. Al menos así era al principio, pero ahora tiendo a pensar que el soporte del complemento fue la parte más difícil. Por un lado, ya teníamos un conjunto de herramientas y un mecanismo para evaluar proyectos de MSBuild, lo cual era bueno, aunque todavía no se había extendido. El hecho de que no tuviéramos que escribir los algoritmos desde cero lo hizo mucho más fácil. La estrategia de confiar en "nuestro" conjunto de herramientas, a la que preferimos seguir cuando admitimos Visual Studio 2017, una vez más demostró ser correcta.
Tradicionalmente, el proceso comienza con la actualización de los paquetes NuGet. La pestaña para administrar paquetes NuGet para la solución actual contiene el botón "Actualizar" ... pero no ayuda. Actualizar todos los paquetes a la vez causó múltiples conflictos de versiones, y tratar de resolverlos no parecía una buena idea. Una forma más dolorosa pero presumiblemente más segura fue actualizar selectivamente los paquetes de destino de Microsoft.Build / Microsoft.CodeAnalysis.
Una diferencia se detectó de inmediato al probar los diagnósticos: la estructura del árbol de sintaxis cambió en un nodo existente. No es gran cosa; Lo arreglamos rápidamente.
Permítame recordarle que probamos nuestros analizadores (para C #, C ++, Java) en proyectos de código abierto. Esto nos permite probar a fondo los diagnósticos; por ejemplo, verificarlos para detectar falsos positivos o ver si hemos perdido algún caso (para reducir la cantidad de falsos negativos). Estas pruebas también nos ayudan a rastrear la posible regresión en el paso inicial de actualización de las bibliotecas / conjunto de herramientas. Esta vez también detectaron varios problemas.
Una fue que el comportamiento dentro de las bibliotecas de CodeAnalysis empeoró. Específicamente, al verificar ciertos proyectos, comenzamos a obtener excepciones del código de las bibliotecas en varias operaciones, como obtener información semántica, abrir proyectos, etc.
Aquellos de ustedes que hayan leído detenidamente el artículo sobre el soporte de Visual Studio 2017 recuerden que nuestra distribución viene con un ficticio: el archivo MSBuild.exe de 0 bytes.
Ahora teníamos que impulsar esta práctica aún más e incluir dummies vacíos para los compiladores csc.exe, vbc.exe y VBCSCompiler.exe. Por qué Se nos ocurrió esta solución después de analizar uno de los proyectos de nuestra base de pruebas y obtener informes de diferencias: la nueva versión del analizador no generaría algunas de las advertencias esperadas.
Descubrimos que tenía que ver con símbolos de compilación condicional, algunos de los cuales no se extrajeron correctamente al usar la nueva versión del analizador. Para llegar a la raíz del problema, tuvimos que profundizar en el código de las bibliotecas de Roslyn.
Los símbolos de compilación condicional se
analizan utilizando el método
GetDefineConstantsSwitch de la clase
Csc de la biblioteca
Microsoft.Build.Tasks.CodeAnalysis . El análisis se realiza utilizando el método
String.Split en varios separadores:
string[] allIdentifiers = originalDefineConstants.Split(new char[] { ',', ';', ' ' });
Este mecanismo de análisis funciona perfectamente; Todos los símbolos de compilación condicional se extraen correctamente. Bien, sigamos cavando.
El siguiente punto clave fue la llamada del método
ComputePathToTool de la clase
ToolTask . Este método calcula la ruta al archivo ejecutable (
csc.exe ) y comprueba si está allí. Si es así, el método le devuelve la ruta o, de lo contrario, es
nulo .
El código de llamada:
.... string pathToTool = ComputePathToTool(); if (pathToTool == null) {
Como no hay
un archivo
csc.exe (¿por qué lo necesitamos?), A
PathToTool se le asigna el valor
nulo en este punto, y el método actual (
ToolTask.Execute ) devuelve
falso . Los resultados de la ejecución de la tarea, incluidos los símbolos de compilación condicional extraídos, se ignoran.
Bien, veamos qué sucede si colocamos el archivo
csc.exe donde se espera que esté.
Ahora
pathToTool almacena la ruta real al archivo actual, y
ToolTask.Execute sigue ejecutándose. El siguiente punto clave es la llamada del método
ManagedCompiler.ExecuteTool :
protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands) { if (ProvideCommandLineArgs) { CommandLineArgs = GetArguments(commandLineCommands, responseFileCommands) .Select(arg => new TaskItem(arg)).ToArray(); } if (SkipCompilerExecution) { return 0; } .... }
La propiedad
SkipCompilerExecution es
verdadera (lógicamente, ya que no estamos compilando de verdad). El método de llamada (el
ToolTask.Execute ya mencionado) verifica si el valor de retorno para
ExecuteTool es 0 y, de ser así, devuelve
verdadero . Si su
csc.exe era un compilador real o "Guerra y paz" de Leo Tolstoy, no importa en absoluto.
Entonces, el problema tiene que ver con el orden en que se definieron los pasos:
- verificar el compilador;
- comprobar si se debe iniciar el compilador;
Y esperaríamos un orden inverso. Es para arreglar esto que se agregaron los dummies para los compiladores.
Bien, pero ¿cómo logramos obtener los símbolos de compilación, con el archivo csc.exe ausente (y los resultados de la tarea ignorados)?
Bueno, también hay un método para este caso:
CSharpCommandLineParser.ParseConditionalCompilationSymbols de la biblioteca
Microsoft.CodeAnalysis.CSharp . También realiza el análisis llamando al método
String.Split en varios separadores:
string[] values = value.Split(new char[] { ';', ',' } );
¿Ves cómo este conjunto de separadores es diferente del que maneja el método
Csc.GetDefineConstantsSwitch ? Aquí, un espacio no es un separador. Significa que los símbolos de compilación condicional separados por espacios no se analizarán correctamente mediante este método.
Eso es lo que sucedió cuando verificamos los proyectos problemáticos: utilizaron símbolos de compilación condicional separados por espacios y, por lo tanto, fueron analizados con éxito por el método
GetDefineConstantsSwitch pero no por el método
ParseConditionalCompilationSymbols .
Otro problema que apareció después de actualizar las bibliotecas fue el comportamiento roto en ciertos casos, específicamente en proyectos que no se compilaron. Afectó a las bibliotecas de Microsoft.
Code Analysis y se manifestó como excepciones de todo tipo:
ArgumentNullException (inicialización fallida de algún registrador interno),
NullReferenceException , etc.
Me gustaría contarle acerca de un error en particular que encontré bastante interesante.
Lo encontramos al verificar la nueva versión del proyecto Roslyn: una de las bibliotecas estaba lanzando una
NullReferenceException . Gracias a la información detallada sobre su fuente, encontramos rápidamente el código fuente del problema y, solo por curiosidad, decidimos verificar si el error persistiría al trabajar en Visual Studio.
Logramos reproducirlo en Visual Studio (versión 16.0.3). Para hacer eso, necesita una definición de clase como esta:
class C1<T1, T2> { void foo() { T1 val = default; if (val is null) { } } }
También necesitará Syntax Visualizer (viene con .NET Compiler Platform SDK). Busque el
TypeSymbol (haciendo clic en el elemento de menú "View TypeSymbol (if any)") del nodo del árbol de sintaxis de tipo
ConstantPatternSyntax (
nulo ). Visual Studio se reiniciará y la información de excepción, específicamente el seguimiento de la pila, estará disponible en el Visor de eventos:
Application: devenv.exe Framework Version: v4.0.30319 Description: The process was terminated due to an unhandled exception. Exception Info: System.NullReferenceException at Microsoft.CodeAnalysis.CSharp.ConversionsBase. ClassifyImplicitBuiltInConversionSlow( Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, System.Collections.Generic.HashSet'1 <Microsoft.CodeAnalysis.DiagnosticInfo> ByRef) at Microsoft.CodeAnalysis.CSharp.ConversionsBase.ClassifyBuiltInConversion( Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol, System.Collections.Generic.HashSet'1 <Microsoft.CodeAnalysis.DiagnosticInfo> ByRef) at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfoForNode( Microsoft.CodeAnalysis.CSharp.BoundNode, Microsoft.CodeAnalysis.CSharp.BoundNode, Microsoft.CodeAnalysis.CSharp.BoundNode) at Microsoft.CodeAnalysis.CSharp.MemberSemanticModel.GetTypeInfoWorker( Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode, System.Threading.CancellationToken) at Microsoft.CodeAnalysis.CSharp.SyntaxTreeSemanticModel.GetTypeInfoWorker( Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode, System.Threading.CancellationToken) at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfo( Microsoft.CodeAnalysis.CSharp.Syntax.PatternSyntax, System.Threading.CancellationToken) at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfoFromNode( Microsoft.CodeAnalysis.SyntaxNode, System.Threading.CancellationToken) at Microsoft.CodeAnalysis.CSharp.CSharpSemanticModel.GetTypeInfoCore( Microsoft.CodeAnalysis.SyntaxNode, System.Threading.CancellationToken) ....
Como puede ver, el problema es causado por una referencia de referencia nula.
Como ya mencioné, encontramos un problema similar al probar el analizador. Si lo compila utilizando las bibliotecas de depuración de Microsoft.
Code Analysis , puede ir directamente al punto problemático buscando el
TypeSymbol del nodo del árbol de sintaxis correspondiente.
Eventualmente nos llevará al método
ClassifyImplicitBuiltInConversionSlow mencionado en el seguimiento de la pila anterior:
private Conversion ClassifyImplicitBuiltInConversionSlow( TypeSymbol source, TypeSymbol destination, ref HashSet<DiagnosticInfo> useSiteDiagnostics) { Debug.Assert((object)source != null); Debug.Assert((object)destination != null); if (source.SpecialType == SpecialType.System_Void || destination.SpecialType == SpecialType.System_Void) { return Conversion.NoConversion; } Conversion conversion = ClassifyStandardImplicitConversion(source, destination, ref useSiteDiagnostics); if (conversion.Exists) { return conversion; } return Conversion.NoConversion; }
Aquí, el parámetro de
destino es
nulo , por lo que llamar a
destination.SpecialType resulta en arrojar una
NullReferenceException . Sí, la operación de desreferencia está precedida por
Debug.Assert , pero no ayuda porque, de hecho, no protege de nada, simplemente le permite detectar el problema en las versiones de depuración de las bibliotecas. O no lo hace.
Cambios en el mecanismo de evaluación de proyectos C ++
No hubo mucho interés en esta parte: los algoritmos existentes no requerían grandes modificaciones que valga la pena mencionar, pero es posible que desee saber sobre dos problemas menores.
La primera fue que tuvimos que modificar los algoritmos que se basaban en el valor numérico de ToolsVersion. Sin entrar en detalles, hay ciertos casos en los que necesita comparar conjuntos de herramientas y elegir, por ejemplo, la versión más reciente. La nueva versión, naturalmente, tiene un valor mayor. Esperábamos que ToolsVersion para el nuevo MSBuild / Visual Studio tuviera el valor 16.0. Si, claro! La siguiente tabla muestra cómo los valores de diferentes propiedades cambiaron a lo largo del historial de desarrollo de Visual Studio:
Sé que la broma sobre los números de versión desordenados de Windows y Xbox es antigua, pero demuestra que no se pueden hacer predicciones confiables sobre los valores (ya sea en el nombre o la versión) de los futuros productos de Microsoft. :)
Resolvimos eso fácilmente agregando prioridades para conjuntos de herramientas (es decir, priorizando como una entidad separada).
El segundo problema involucraba problemas al trabajar en Visual Studio 2017 o en un entorno relacionado (por ejemplo, cuando se establece la variable de entorno
VisualStudioVersion ). Ocurre porque los parámetros informáticos necesarios para evaluar un proyecto C ++ es una tarea mucho más difícil que evaluar un proyecto .NET. Para .NET, utilizamos nuestro propio conjunto de herramientas y el valor correspondiente de ToolsVersion. Para C ++, podemos utilizar tanto nuestro propio conjunto de herramientas como los proporcionados por el sistema. A partir de Build Tools for Visual Studio 2017, los conjuntos de herramientas se definen en el archivo
MSBuild.exe.config en lugar del registro. Es por eso que ya no pudimos obtenerlos de la lista global de conjuntos de herramientas (usando
Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.Toolsets , por ejemplo) a diferencia de los definidos en el registro (es decir, para Visual Studio 2015 y anteriores).
Todo esto nos impide evaluar un proyecto usando
ToolsVersion 15.0 porque el sistema no verá el conjunto de herramientas requerido. El conjunto de herramientas más reciente,
Current , seguirá estando disponible, ya que es nuestro propio conjunto de herramientas y, por lo tanto, no existe tal problema en Visual Studio 2019. La solución fue bastante simple y nos permitió solucionarlo sin cambiar los algoritmos de evaluación existentes: simplemente tuvo que incluir otro conjunto de herramientas,
15.0 , en la lista de nuestros propios conjuntos de herramientas además de
Current .
Cambios en el mecanismo de evaluación de proyectos de C # .NET Core
Esta tarea involucraba dos cuestiones interrelacionadas:
- agregar el conjunto de herramientas 'Actual' rompió el análisis de proyectos de .NET Core en Visual Studio 2017;
- el análisis no funcionaría para proyectos .NET Core en sistemas sin al menos una copia de Visual Studio instalada.
Ambos problemas provenían de la misma fuente: algunos de los archivos base .targets / .props se buscaron en rutas incorrectas. Esto nos impidió evaluar un proyecto utilizando nuestro conjunto de herramientas.
Si no tuviera instalada una instancia de Visual Studio, obtendría el siguiente error (con la versión anterior del conjunto de herramientas,
15.0 ):
The imported project "C:\Windows\Microsoft.NET\Framework64\ 15.0\Microsoft.Common.props" was not found.
Al evaluar un proyecto C # .NET Core en Visual Studio 2017, obtendría el siguiente error (con la versión actual del conjunto de herramientas,
Actual ):
The imported project "C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\MSBuild\Current\Microsoft.Common.props" was not found. ....
Dado que estos problemas son similares (que parecen ser), podríamos intentar matar dos pájaros de un tiro.
En los siguientes párrafos, explicaré cómo lo logramos, sin entrar en detalles. Estos detalles (sobre cómo se evalúan los proyectos de C # .NET Core, así como los cambios en el mecanismo de evaluación en nuestro conjunto de herramientas) serán el tema de uno de nuestros futuros artículos. Por cierto, si estaba leyendo este artículo cuidadosamente, probablemente haya notado que esta es la segunda referencia a nuestros futuros artículos. :)
Ahora, ¿cómo resolvimos ese problema? Ampliamos nuestro propio conjunto de herramientas con los archivos .targets / .props base de .NET Core SDK (
Sdk.props ,
Sdk.targets ). Eso nos dio más control sobre la situación y más flexibilidad en la gestión de importaciones, así como en la evaluación de proyectos .NET Core en general. Sí, nuestro conjunto de herramientas se hizo un poco más grande nuevamente, y también tuvimos que agregar lógica para configurar el entorno requerido para la evaluación de proyectos .NET Core, pero parece que vale la pena.
Hasta entonces, habíamos evaluado los proyectos de .NET Core simplemente solicitando la evaluación y confiando en MSBuild para hacer el trabajo.
Ahora que teníamos más control sobre la situación, el mecanismo cambió un poco:
- configurar el entorno requerido para evaluar proyectos .NET Core;
- evaluación:
- iniciar la evaluación utilizando archivos .targets / .props de nuestro conjunto de herramientas;
- Continuar la evaluación utilizando archivos externos.
Esta secuencia sugiere que la configuración del entorno persigue dos objetivos principales:
- iniciar la evaluación utilizando archivos .targets / .props de nuestro conjunto de herramientas;
- redirigir todas las operaciones posteriores a archivos externos .targets / .props.
Se utiliza una biblioteca especial Microsoft.DotNet.MSBuildSdkResolver para buscar los archivos .targets / .props necesarios. Con el fin de iniciar la configuración del entorno utilizando archivos de nuestro conjunto de herramientas, utilizamos una variable de entorno especial utilizada por esa biblioteca para que podamos apuntar a la fuente desde donde importar los archivos necesarios (es decir, nuestro conjunto de herramientas). Como la biblioteca está incluida en nuestra distribución, no hay riesgo de una falla lógica repentina.
Ahora tenemos los archivos Sdk de nuestro conjunto de herramientas importados primero, y dado que ahora podemos cambiarlos fácilmente, controlamos completamente el resto de la lógica de evaluación. Significa que ahora podemos decidir qué archivos y desde qué ubicación importar. Lo mismo se aplica a Microsoft.Common.props mencionado anteriormente. Importamos este y otros archivos base de nuestro conjunto de herramientas para no tener que preocuparnos por su existencia o contenido.
Una vez que se realizan todas las importaciones necesarias y se establecen las propiedades, pasamos el control sobre el proceso de evaluación al .NET Core SDK real, donde se realizan todas las demás operaciones requeridas.
Conclusión
En general, admitir Visual Studio 2019 fue más fácil que admitir Visual Studio 2017 por varias razones. Primero, Microsoft no cambió tantas cosas como lo hizo al actualizar Visual Studio 2015 a Visual Studio 2017. Sí, cambiaron el conjunto de herramientas base y obligaron a los complementos de Visual Studio a cambiar al modo de carga asíncrona, pero este cambio no fue que drástico En segundo lugar, ya teníamos una solución preparada que involucraba nuestro propio conjunto de herramientas y mecanismo de evaluación de proyectos y simplemente no teníamos que trabajar todo desde cero, solo construir sobre lo que ya teníamos. El proceso relativamente sencillo de respaldar el análisis de proyectos de .NET Core en nuevas condiciones (y en computadoras sin copias de Visual Studio instaladas) al extender nuestro sistema de evaluación de proyectos también nos da la esperanza de haber tomado la decisión correcta al tomar parte del control en nuestras manos
Pero me gustaría repetir la idea comunicada en el artículo anterior: a veces, usar soluciones preparadas no es tan fácil como parece.