O suporte do Visual Studio 2019 no PVS-Studio afetou vários componentes: o próprio plug-in, o analisador de linha de comando, os núcleos dos analisadores C ++ e C # e alguns utilitários. Neste artigo, explicarei brevemente quais problemas encontramos ao implementar o suporte ao IDE e como os solucionamos.
Antes de começarmos, gostaria de relembrar o histórico de suporte às versões anteriores do Visual Studio no PVS-Studio para entender melhor nossa visão da tarefa e das soluções que descobrimos em todas as situações.
Desde a primeira versão do PVS-Studio que acompanha um plug-in para o Visual Studio (era o Visual Studio 2005 naquela época), oferecer suporte a novas versões desse IDE tem sido uma tarefa bastante trivial para nós, que basicamente se resumiu à atualização do projeto do plug-in arquivo e dependências das várias extensões de API do Visual Studio. De vez em quando, teríamos que adicionar suporte para novos recursos do C ++, com os quais o compilador do Visual C ++ estava gradualmente aprendendo a trabalhar, mas geralmente também não era uma tarefa difícil e poderia ser feita facilmente antes de uma nova versão do Visual Studio. . Além disso, o PVS-Studio tinha apenas um analisador na época - para C / C ++.
As coisas mudaram quando o Visual Studio 2017 foi lançado. Além de grandes mudanças em muitas extensões de API do IDE, também encontramos um problema em manter a compatibilidade com versões anteriores do novo analisador C # adicionado pouco antes (assim como da nova camada do analisador para que o C ++ funcione com projetos MSBuild) com o novas versões do MSBuild \ Visual Studio.
Considerando tudo isso, recomendo vivamente que você veja um artigo relacionado sobre o suporte do Visual Studio 2017, "
Suporte do Visual Studio 2017 e Roslyn 2.0 no PVS-Studio: às vezes não é tão fácil usar soluções prontas como pode parecer ", antes de continuar lendo. Esse artigo discute os problemas que enfrentamos na última vez e o modelo de interação entre diferentes componentes (como PVS-Studio, MSBuild e Roslyn). Conhecer esses detalhes pode ajudar você a entender melhor o artigo atual.
A solução desses problemas levou a mudanças significativas no analisador, e esperávamos que as novas abordagens aplicadas nos ajudassem a oferecer suporte a versões futuras do Visual Studio \ MSBuild com muito mais facilidade e rapidez. Essa esperança já começou a se mostrar realista quando as inúmeras atualizações do Visual Studio 2017 foram lançadas. A nova abordagem nos ajudou no suporte ao Visual Studio 2019? Continue lendo para descobrir.
Plug-in PVS-Studio para Visual Studio 2019
O começo parecia promissor. Não foi necessário muito esforço para portar o plug-in para o Visual Studio 2019 e fazê-lo iniciar e executar bem. Mas já encontramos dois problemas ao mesmo tempo que poderiam trazer mais problemas posteriormente.
O primeiro tinha a ver com a interface
IVsSolutionWorkspaceService usada para oferecer suporte ao modo Lightweight Solution Load (que, a propósito, havia sido desativado em uma das atualizações anteriores, no Visual Studio 2017). Foi decorado com o atributo
Descontinuado , que atualmente acionava apenas um aviso no momento da construção, mas que se tornaria um grande problema no futuro. Esse modo não durou muito tempo ... Isso foi fácil de corrigir - simplesmente paramos de usar essa interface.
O segundo problema foi a seguinte mensagem que recebíamos ao carregar o Visual Studio com o plug-in habilitado: O
Visual Studio detectou uma ou mais extensões que estão em risco ou não estão funcionando em um recurso VS update.Os logs das ativações do Visual Studio (o arquivo ActivityLog) ajudaram a esclarecê-lo:
Aviso: a extensão 'PVS-Studio' usa o recurso 'carregamento automático síncrono' do Visual Studio. Esse recurso não será mais suportado em uma atualização futura do Visual Studio 2019, quando a extensão não funcionará. Entre em contato com o fornecedor da extensão para obter uma atualização.O que isso significava para nós era que teríamos que mudar do modo de carregamento síncrono para assíncrono. Espero que você não se importe se eu poupar os detalhes de como interagimos com as interfaces COM do Visual Studio e apenas descrever brevemente as alterações.
Há um artigo da Microsoft sobre o carregamento de plugins de forma assíncrona: "
Como usar o AsyncPackage para carregar o VSPackages em segundo plano ". No entanto, já estava claro que havia mais mudanças por vir.
Uma das maiores mudanças foi no modo de carregamento, ou melhor, no modo de inicialização. Nas versões anteriores, toda a inicialização necessária era feita usando dois métodos:
Inicializar nossa classe herdada de
Package e
OnShellPropertyChange . O último teve que ser adicionado porque, ao carregar de forma síncrona, o próprio Visual Studio ainda pode estar no processo de carregamento e inicialização e, portanto, algumas das ações necessárias eram impossíveis de executar durante a inicialização do plug-in. Uma maneira de corrigir isso era atrasar a execução dessas ações até o Visual Studio sair do estado 'zumbi'. Foi essa parte da lógica que escolhemos no método
OnShellPropertyChange com uma verificação do status de 'zumbi'.
O método
Initialize da classe abstrata
AsyncPackage , do qual são carregados os plugins de forma assíncrona, é
selado . Portanto, a inicialização deve ser feita no método substituído
InitializeAsync , que é exatamente o que fizemos. A lógica de verificação do 'zumbi' também teve que ser alterada porque as informações de status não estavam mais disponíveis para o nosso plugin. Além disso, ainda tínhamos que executar as ações que precisavam ser realizadas após a inicialização do plugin.
Resolvemos isso utilizando o método
OnPackageLoaded da interface
IVsPackageLoadEvents , que é onde essas ações atrasadas foram executadas.
Outro problema resultante do carregamento assíncrono era que os comandos do plug-in não podiam ser usados até depois do carregamento do Visual Studio. A abertura do log do analisador clicando duas vezes no gerenciador de arquivos (se você precisar abri-lo no Visual Studio) resultou no lançamento da versão correspondente do devenv.exe com um comando para abrir o log. O comando de inicialização se parecia com isso:
"C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\Common7\IDE\devenv.exe" /command "PVSStudio.OpenAnalysisReport C:\Users\vasiliev\source\repos\ConsoleApp\ConsoleApp.plog"
O sinalizador "/ command" é usado aqui para executar o comando registrado no Visual Studio. Essa abordagem não funcionou mais, pois os comandos não estavam mais disponíveis até o carregamento do plug-in. A solução alternativa que criamos foi analisar o comando devenv.exe launch após o carregamento do plug-in e executar o comando log open, se encontrado no comando launch. Portanto, descartar a idéia de usar a interface "apropriada" para trabalhar com comandos nos permitiu manter a funcionalidade necessária, com a abertura atrasada do log após o carregamento completo do plug-in.
Ufa, parece que finalmente conseguimos; o plug-in carrega e abre como esperado, sem nenhum aviso.
E é aqui que as coisas dão errado. Paul (Oi Paul!) Instala o plug-in no computador e pergunta por que ainda não mudamos para o carregamento assíncrono.
Dizer que ficamos chocados seria um eufemismo. Isso não poderia ser! Mas é real: aqui está a nova versão do plug-in, e aqui está uma mensagem dizendo que o pacote está carregando de forma síncrona. Alexander (Oi Alexander!) E eu tento a mesma versão em nossos respectivos computadores - funciona bem. Como isso é possível? Ocorre-nos para verificar as versões das bibliotecas PVS-Studio carregadas no Visual Studio - e descobrimos que são as bibliotecas do Visual Studio 2017, enquanto o pacote VSIX contém as novas versões, ou seja, para o Visual Studio 2019.
Depois de mexer no VSIXInstaller por um tempo, conseguimos descobrir que o problema estava relacionado ao cache de pacotes. Essa teoria também foi apoiada pelo fato de restringir o acesso ao pacote em cache (C: \ ProgramData \ Microsoft \ VisualStudio \ Packages) causado pelo VSIXInstaller para gerar uma mensagem de erro no log. Curiosamente, quando o erro não ocorreu, as informações sobre a instalação de pacotes em cache não apareceram.
Nota lateral . Enquanto estudava o comportamento do VSIX Installer e das bibliotecas associadas, pensei em como é legal o Roslyn e o MSBuild serem de código aberto, o que permite ler e depurar convenientemente o código deles e rastrear sua lógica de trabalho.
Então, foi o que aconteceu: ao instalar o plug-in, o VSIX Installer viu que o pacote correspondente já estava em cache (na verdade era o pacote .vsix do Visual Studio 2017) e instalou esse pacote em vez do novo. Por que ele ignorou as restrições / requisitos definidos no arquivo .vsixmanifest (que, entre outras coisas, restringiu a instalação de extensões a uma versão específica do Visual Studio) é uma questão ainda a ser respondida. Como resultado, o plug-in projetado para o Visual Studio 2017 foi instalado no Visual Studio 2019 - apesar das restrições especificadas no arquivo .vsixmanifest.
O pior de tudo é que essa instalação quebrou o gráfico de dependências do Visual Studio e, embora o IDE parecesse estar funcionando bem, as coisas estavam realmente terríveis. Você não pôde instalar ou excluir extensões, atualizar etc. O processo de "restauração" também foi doloroso, pois tivemos que excluir a extensão (ou seja, os arquivos que a contêm) manualmente e - também manualmente - editar os arquivos de configuração que armazenam as informações sobre o pacote instalado. Em outras palavras, não foi nada divertido.
Para corrigir isso e garantir que não enfrentemos situações como essa no futuro, decidimos criar nosso próprio GUID para o novo pacote, para que os pacotes do Visual Studio 2017 e Visual Studio 2019 sejam isolados um do outro ( os pacotes mais antigos eram bons; sempre usavam um GUID compartilhado).
Desde que começamos a falar de surpresas desagradáveis, eis outra: depois de atualizar para a Visualização 2, o menu PVS-Studio "mudou" para a guia "Extensões". Não é grande coisa, mas tornou o acesso à funcionalidade do plugin menos conveniente. Esse comportamento persistiu nas próximas versões do Visual Studio 2019, incluindo o lançamento. Eu encontrei menções a esse "recurso" nem na documentação nem no blog.
Ok, agora as coisas pareciam bem e parecia que finalmente tínhamos terminado o suporte do Visual Studio 2019. Isso se mostrou errado no dia seguinte após o lançamento do PVS-Studio 7.02. Era o modo de carregamento assíncrono novamente. Ao abrir a janela de resultados da análise (ou iniciar a análise), a janela do analisador pareceria "vazia" para o usuário - sem botões, sem grade, nada.
De fato, esse problema ocorreu de vez em quando durante a análise. Mas afetou apenas um computador e não apareceu até o Visual Studio ser atualizado para uma das primeiras iterações de 'Preview'. Suspeitamos que algo tivesse quebrado durante a instalação ou atualização. O problema, no entanto, desapareceu algum tempo depois e não ocorreria nem naquele computador em particular, por isso pensamos que "foi consertado sozinho". Mas não - nós apenas tivemos sorte. Ou infeliz, por falar nisso.
Como descobrimos, foi a ordem em que a própria janela do IDE (a classe derivada do
ToolWindowPane ) e seu conteúdo (nosso controle com a grade e os botões) foram inicializados. Sob certas condições, o controle seria inicializado antes do painel e, embora as coisas funcionassem bem, e o método
FindToolWindowAsync (criar a janela quando for acessada pela primeira vez) funcionasse bem, o controle permaneceria invisível. Corrigimos isso adicionando uma inicialização lenta para nosso controle ao código de preenchimento do painel.
Suporte do C # 8.0
Há uma grande vantagem em usar o Roslyn como base para o analisador: você não precisa adicionar suporte para novas construções de linguagem manualmente - isso é feito automaticamente pelas bibliotecas da Microsoft, análise de código e apenas usamos as soluções prontas. Isso significa que a nova sintaxe é suportada simplesmente atualizando as bibliotecas.
Quanto à análise em si, tivemos que ajustar as coisas por conta própria, é claro - em particular, lidar com novas construções de linguagem. Certamente, tivemos a nova árvore de sintaxe gerada automaticamente, simplesmente atualizando o Roslyn, mas ainda tivemos que ensinar ao analisador exatamente como interpretar e processar nós de árvore de sintaxe novos ou modificados.
Os tipos de referência anuláveis são talvez o novo recurso mais discutido no C # 8. Não falarei sobre eles agora, porque um tópico tão grande vale um artigo separado (que está sendo escrito atualmente). Por enquanto, decidimos ignorar anotações anuláveis em nosso mecanismo de fluxo de dados (ou seja, entendemos, analisamos e ignoramos). A idéia é que uma variável, mesmo de um tipo de referência não anulável, ainda possa receber o valor
nulo com bastante facilidade (ou acidentalmente), terminando com um NRE ao tentar desreferê-lo. Nosso analisador pode detectar esses erros e relatar uma possível desreferência nula (se encontrar essa atribuição no código, é claro), mesmo que a variável seja do tipo referência não nula.
O uso de tipos de referência anuláveis e sintaxe associada permite que você escreva um código bastante interessante. Nós o apelidamos de "sintaxe emocional". Este trecho é perfeitamente compilável:
obj.Calculate(); obj?.Calculate(); obj.Calculate(); obj!?.Calculate(); obj!!!.Calculate();
A propósito, minhas experiências me levaram a descobrir alguns truques que você pode usar para "travar" o Visual Studio usando a nova sintaxe. Eles são baseados no fato de que você tem permissão para escrever tantos '!' caracteres como você gosta. Isso significa que você pode escrever não apenas um código como este:
object temp = null!
mas também assim:
object temp = null!!!;
E, empurrando ainda mais, você pode escrever coisas loucas como esta:
object temp = null!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!;
Esse código é compilável, mas se você tentar exibir a árvore de sintaxe no Syntax Visualizer a partir do .NET Compiler Platform SDK, o Visual Studio falhará.
O relatório de falha pode ser extraído do Visualizador 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:
Se você ficar ainda mais louco e adicionar várias vezes mais pontos de exclamação, o Visual Studio começará a travar sozinho, sem a ajuda do Syntax Visualizer. As bibliotecas Microsoft.CodeAnalysis e o compilador csc.exe também não podem lidar com esse código.
Esses exemplos são inventados, é claro, mas achei esse truque engraçado.
Conjunto de ferramentas
Era óbvio que a atualização do conjunto de ferramentas seria a parte mais difícil. Pelo menos é o que parecia no começo, mas agora acho que o suporte ao plugin foi a parte mais difícil. Por um lado, já tínhamos um conjunto de ferramentas e um mecanismo para avaliar os projetos do MSBuild, o que foi bom, apesar de ainda não ter sido estendido. O fato de não termos que escrever os algoritmos do zero tornou mais fácil. A estratégia de contar com o "nosso" conjunto de ferramentas, ao qual preferimos seguir o suporte ao Visual Studio 2017, mais uma vez se mostrou correta.
Tradicionalmente, o processo começa com a atualização dos pacotes NuGet. A guia para gerenciar pacotes NuGet para a solução atual contém o botão "Atualizar" ... mas não ajuda. Atualizar todos os pacotes de uma vez causou vários conflitos de versão e tentar resolvê-los todos não parecia uma boa ideia. Uma maneira mais dolorosa e presumivelmente mais segura foi atualizar seletivamente os pacotes de destino do Microsoft.Build / Microsoft.CodeAnalysis.
Uma diferença foi identificada imediatamente ao testar o diagnóstico: a estrutura da árvore de sintaxe foi alterada em um nó existente. Não é grande coisa; consertamos isso rapidamente.
Deixe-me lembrá-lo, testamos nossos analisadores (para C #, C ++, Java) em projetos de código aberto. Isso nos permite testar minuciosamente os diagnósticos - por exemplo, verifique se há falsos positivos ou veja se perdemos algum caso (para reduzir o número de falsos negativos). Esses testes também nos ajudam a rastrear uma possível regressão na etapa inicial de atualização das bibliotecas / conjunto de ferramentas. Desta vez, eles pegaram uma série de questões também.
Uma era que o comportamento nas bibliotecas da CodeAnalysis piorava. Especificamente, ao verificar determinados projetos, começamos a obter exceções do código das bibliotecas em várias operações, como obter informações semânticas, abrir projetos etc.
Aqueles de vocês que leram atentamente o artigo sobre o suporte do Visual Studio 2017 lembram que nossa distribuição é fornecida com um manequim - o arquivo MSBuild.exe de 0 bytes.
Agora tivemos que aprimorar ainda mais essa prática e incluir manequins vazios para os compiladores csc.exe, vbc.exe e VBCSCompiler.exe. Porque Chegamos a essa solução depois de analisar um dos projetos de nossa base de testes e obter relatórios de diferenças: a nova versão do analisador não produziria alguns dos avisos esperados.
Descobrimos que isso tinha a ver com símbolos de compilação condicional, alguns dos quais não foram extraídos corretamente ao usar a nova versão do analisador. Para chegar à raiz do problema, tivemos que nos aprofundar no código das bibliotecas de Roslyn.
Os símbolos de compilação condicional são analisados usando o método
GetDefineConstantsSwitch da classe
Csc da biblioteca
Microsoft.Build.Tasks.CodeAnalysis . A análise é feita usando o método
String.Split em vários separadores:
string[] allIdentifiers = originalDefineConstants.Split(new char[] { ',', ';', ' ' });
Esse mecanismo de análise funciona perfeitamente; todos os símbolos de compilação condicional são extraídos corretamente. Ok, vamos continuar cavando.
O próximo ponto-chave foi a chamada do método
ComputePathToTool da classe
ToolTask . Este método calcula o caminho para o arquivo executável (
csc.exe ) e verifica se está lá. Nesse caso, o método retorna o caminho para ele ou
null caso contrário.
O código de chamada:
.... string pathToTool = ComputePathToTool(); if (pathToTool == null) {
Como não há arquivo
csc.exe (por que precisamos dele?), O
PathToTool recebe o valor
nulo neste momento, e o método atual (
ToolTask.Execute ) retorna
false . Os resultados da execução da tarefa, incluindo os símbolos de compilação condicional extraídos, são ignorados.
Ok, vamos ver o que acontece se colocarmos o arquivo
csc.exe onde ele deveria estar.
Agora
pathToTool armazena o caminho real para o arquivo agora presente, e o
ToolTask.Execute continua em execução. O próximo ponto-chave é a chamada do 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; } .... }
A propriedade
SkipCompilerExecution é
verdadeira (logicamente o suficiente, pois não estamos compilando de verdade). O método de chamada (o
ToolTask.Execute já mencionado) verifica se o valor de retorno para
ExecuteTool é 0 e, nesse caso, retorna
true . Se o seu
csc.exe foi um compilador real ou "Guerra e Paz" de Leo Tolstoy, não importa.
Portanto, o problema tem a ver com a ordem em que as etapas foram definidas:
- verifique se há compilador;
- verifique se o compilador deve ser iniciado;
E esperaríamos uma ordem inversa. É para corrigir isso que os manequins para os compiladores foram adicionados.
Ok, mas como conseguimos obter os símbolos de compilação, com o arquivo csc.exe ausente (e os resultados da tarefa ignorados)?
Bem, também existe um método para este caso:
CSharpCommandLineParser.ParseConditionalCompilationSymbols da biblioteca
Microsoft.CodeAnalysis.CSharp . Também faz a análise chamando o método
String.Split em vários separadores:
string[] values = value.Split(new char[] { ';', ',' } );
Veja como esse conjunto de separadores é diferente daquele tratado pelo método
Csc.GetDefineConstantsSwitch ? Aqui, um espaço não é um separador. Isso significa que símbolos de compilação condicional separados por espaços não serão analisados corretamente por esse método.
Foi o que aconteceu quando checamos os projetos problemáticos: eles usavam símbolos de compilação condicional separados por espaço e, portanto, foram analisados com êxito pelo método
GetDefineConstantsSwitch , mas não pelo método
ParseConditionalCompilationSymbols .
Outro problema que apareceu após a atualização das bibliotecas foi o comportamento interrompido em certos casos - especificamente, em projetos que não foram criados. Isso afetou as bibliotecas da Microsoft,
Code Analysis e se manifestou como exceções de todos os tipos:
ArgumentNullException (falha na inicialização de algum log interno),
NullReferenceException e assim por diante.
Gostaria de falar sobre um erro em particular que achei bastante interessante.
Encontramos isso ao verificar a nova versão do projeto Roslyn: uma das bibliotecas estava lançando uma
NullReferenceException . Graças a informações detalhadas sobre sua fonte, encontramos rapidamente o código-fonte do problema e, por curiosidade, decidimos verificar se o erro persistiria ao trabalhar no Visual Studio.
Conseguimos reproduzi-lo no Visual Studio (versão 16.0.3). Para fazer isso, você precisa de uma definição de classe como esta:
class C1<T1, T2> { void foo() { T1 val = default; if (val is null) { } } }
Você também precisará do Syntax Visualizer (ele vem com o .NET Compiler Platform SDK). Procure o
TypeSymbol (clicando no item de menu "View TypeSymbol (se houver)") do nó da árvore de sintaxe do tipo
ConstantPatternSyntax (
null ). O Visual Studio será reiniciado e as informações da exceção - especificamente, o rastreamento de pilha - ficarão disponíveis no Visualizador 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 você pode ver, o problema é causado por uma referência de referência nula.
Como já mencionei, encontramos um problema semelhante ao testar o analisador. Se você construí-lo usando as bibliotecas de depuração da Microsoft,
Code Analysis , você pode ir direto ao ponto do problema,
consultando o
TypeSymbol do nó da árvore de sintaxe correspondente.
Eventualmente, ele nos levará ao método
ClassifyImplicitBuiltInConversionSlow mencionado no rastreamento de pilha acima:
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; }
Aqui, o parâmetro de
destino é
nulo , portanto, chamar
destination.SpecialType resulta em lançar uma
NullReferenceException . Sim, a operação de desreferência é precedida por
Debug.Assert , mas não ajuda porque, na verdade, não protege de nada - simplesmente permite que você localize o problema nas versões de depuração das bibliotecas. Ou não.
Alterações no mecanismo de avaliação de projetos C ++
Não havia muita coisa interessante nesta parte: os algoritmos existentes não exigiam grandes modificações que merecessem ser mencionadas, mas você pode querer saber sobre dois pequenos problemas.
O primeiro foi que tivemos que modificar os algoritmos que dependiam do valor numérico de ToolsVersion. Sem entrar em detalhes, há certos casos em que você precisa comparar conjuntos de ferramentas e escolher, digamos, a versão mais recente. A nova versão, naturalmente, tem um valor maior. Esperávamos que ToolsVersion para o novo MSBuild / Visual Studio tivesse o valor 16.0. Sim claro! A tabela abaixo mostra como os valores das diferentes propriedades foram alterados ao longo do histórico de desenvolvimento do Visual Studio:
Sei que a piada sobre os números de versões complicadas do Windows e Xbox é antiga, mas prova que você não pode fazer previsões confiáveis sobre os valores (seja no nome ou na versão) dos futuros produtos da Microsoft. :)
Resolvemos isso facilmente adicionando a priorização de conjuntos de ferramentas (ou seja, destacando a prioridade como uma entidade separada).
O segundo problema envolveu problemas com o trabalho no Visual Studio 2017 ou ambiente relacionado (por exemplo, quando a variável de ambiente
VisualStudioVersion está definida). Isso ocorre porque os parâmetros de computação necessários para avaliar um projeto C ++ é uma tarefa muito mais difícil do que avaliar um projeto .NET. Para o .NET, usamos nosso próprio conjunto de ferramentas e o valor correspondente de ToolsVersion. Para C ++, podemos utilizar nosso próprio conjunto de ferramentas e os fornecidos pelo sistema. A partir do Build Tools para Visual Studio 2017, os conjuntos de ferramentas são definidos no arquivo
MSBuild.exe.config em vez do registro. É por isso que não conseguimos mais obtê-los da lista global de conjuntos de ferramentas (usando o
Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.Toolsets , por exemplo), diferente dos definidos no registro (por exemplo, para o Visual Studio 2015 e versões anteriores).
Tudo isso nos impede de avaliar um projeto usando o
ToolsVersion 15.0, porque o sistema não verá o conjunto de ferramentas necessário. O conjunto de ferramentas mais recente,
Current , ainda estará disponível, pois é nosso próprio conjunto de ferramentas e, portanto, não existe esse problema no Visual Studio 2019. A solução era bastante simples e nos permitiu corrigir isso sem alterar os algoritmos de avaliação existentes: apenas tivemos que incluir outro conjunto de ferramentas,
15.0 , na lista de nossos próprios conjuntos de ferramentas, além de
Atual .
Alterações no mecanismo de avaliação de projetos C # .NET Core
Esta tarefa envolveu dois problemas inter-relacionados:
- adicionar o conjunto de ferramentas 'Atual' interrompeu a análise dos projetos do .NET Core no Visual Studio 2017;
- a análise não funcionaria para projetos do .NET Core em sistemas sem pelo menos uma cópia do Visual Studio instalada.
Ambos os problemas vinham da mesma fonte: alguns arquivos base .targets / .props foram procurados por caminhos incorretos. Isso nos impediu de avaliar um projeto usando nosso conjunto de ferramentas.
Se você não tivesse uma instância do Visual Studio instalada, obteria o seguinte erro (com a versão anterior do conjunto de ferramentas,
15.0 ):
The imported project "C:\Windows\Microsoft.NET\Framework64\ 15.0\Microsoft.Common.props" was not found.
Ao avaliar um projeto C # .NET Core no Visual Studio 2017, você recebe o seguinte erro (com a versão atual do conjunto de ferramentas,
Atual ):
The imported project "C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\MSBuild\Current\Microsoft.Common.props" was not found. ....
Como esses problemas são semelhantes (o que parecem ser), podemos tentar matar dois coelhos com uma cajadada só.
Nos próximos parágrafos, explicarei como realizamos isso, sem entrar em detalhes. Esses detalhes (sobre como os projetos C # .NET Core são avaliados, bem como alterações no mecanismo de avaliação em nosso conjunto de ferramentas) serão o tópico de um de nossos futuros artigos. A propósito, se você estava lendo este artigo com atenção, provavelmente percebeu que esta é a segunda referência aos nossos futuros artigos. :)
Agora, como resolvemos esse problema? Estendemos nosso próprio conjunto de ferramentas com os arquivos .targets / .props base do .NET Core SDK (
Sdk.props ,
Sdk.targets ). Isso nos deu mais controle sobre a situação e mais flexibilidade no gerenciamento de importação, bem como na avaliação dos projetos do .NET Core em geral. Sim, nosso conjunto de ferramentas ficou um pouco maior novamente e também tivemos que adicionar lógica para configurar o ambiente necessário para a avaliação de projetos do .NET Core, mas parece valer a pena.
Até então, tínhamos avaliado os projetos do .NET Core simplesmente solicitando a avaliação e contando com o MSBuild para fazer o trabalho.
Agora que tínhamos mais controle sobre a situação, o mecanismo mudou um pouco:
- configurar o ambiente necessário para avaliar projetos do .NET Core;
- avaliação:
- inicie a avaliação usando arquivos .targets / .props do nosso conjunto de ferramentas;
- continue a avaliação usando arquivos externos.
Essa sequência sugere que a criação do ambiente persegue dois objetivos principais:
- iniciar avaliação usando arquivos .targets / .props do nosso conjunto de ferramentas;
- redirecione todas as operações subseqüentes para arquivos .targets / .props externos.
Uma biblioteca especial Microsoft.DotNet.MSBuildSdkResolver é usada para procurar os arquivos .targets / .props necessários. Para iniciar a configuração do ambiente usando arquivos do nosso conjunto de ferramentas, utilizamos uma variável de ambiente especial usada por essa biblioteca para que pudéssemos apontar para a fonte de onde importar os arquivos necessários (por exemplo, nosso conjunto de ferramentas). Como a biblioteca está incluída em nossa distribuição, não há risco de uma falha lógica repentina.
Agora, importamos primeiro os arquivos Sdk do nosso conjunto de ferramentas e, como podemos alterá-los facilmente agora, controlamos completamente o restante da lógica de avaliação. Isso significa que agora podemos decidir quais arquivos e de qual local importar. O mesmo se aplica ao Microsoft.Common.props mencionado acima. Importamos este e outros arquivos de base do nosso conjunto de ferramentas para não precisarmos nos preocupar com a existência ou o conteúdo deles.
Depois que todas as importações necessárias forem feitas e as propriedades definidas, passamos o controle sobre o processo de avaliação para o .NET Core SDK real, onde todas as demais operações necessárias são executadas.
Conclusão
O suporte ao Visual Studio 2019 geralmente era mais fácil do que o suporte ao Visual Studio 2017 por vários motivos. Primeiro, a Microsoft não mudou tantas coisas quanto na atualização do Visual Studio 2015 para o Visual Studio 2017. Sim, eles mudaram o conjunto de ferramentas base e forçaram os plug-ins do Visual Studio a mudar para o modo de carregamento assíncrono, mas essa alteração não foi que drástico. Segundo, já tínhamos uma solução pronta envolvendo nosso próprio conjunto de ferramentas e mecanismo de avaliação de projetos e simplesmente não precisávamos trabalhar tudo do zero - apenas desenvolver o que já tínhamos. O processo relativamente indolor de dar suporte à análise de projetos do .NET Core sob novas condições (e em computadores sem cópias do Visual Studio instaladas) estendendo nosso sistema de avaliação de projetos também nos dá esperança de que fizemos a escolha certa, assumindo parte do controle nossas mãos.
Mas gostaria de repetir a ideia divulgada no artigo anterior: às vezes, usar soluções prontas não é tão fácil quanto pode parecer.