La prise en charge de Visual Studio 2019 dans PVS-Studio a affecté un certain nombre de composants: le plug-in lui-même, l'analyseur de ligne de commande, les cœurs des analyseurs C ++ et C # et quelques utilitaires. Dans cet article, je vais expliquer brièvement quels problèmes nous avons rencontrés lors de la mise en œuvre du support de l'IDE et comment nous les avons résolus.
Avant de commencer, je voudrais revenir sur l'histoire de la prise en charge des versions précédentes de Visual Studio dans PVS-Studio afin que vous compreniez mieux notre vision de la tâche et des solutions que nous avons trouvées dans chaque situation.
Depuis la première version de PVS-Studio livrée avec un plugin pour Visual Studio (c'était Visual Studio 2005 à l'époque), la prise en charge de nouvelles versions de cet IDE a été une tâche assez banale pour nous, qui se résumait essentiellement à la mise à jour du projet du plugin fichier et dépendances des différentes extensions d'API de Visual Studio. De temps en temps, nous devions ajouter la prise en charge de nouvelles fonctionnalités de C ++, que le compilateur Visual C ++ apprenait progressivement à utiliser, mais ce n'était généralement pas une tâche difficile non plus et pouvait être facilement effectuée juste avant une nouvelle version de Visual Studio . De plus, PVS-Studio n'avait à l'époque qu'un seul analyseur - pour C / C ++.
Les choses ont changé lorsque Visual Studio 2017 est sorti. En plus d'énormes modifications apportées à de nombreuses extensions d'API de l'IDE, nous avons également rencontré un problème avec le maintien de la compatibilité descendante du nouvel analyseur C # ajouté peu de temps auparavant (ainsi que de la nouvelle couche d'analyseur pour C ++ pour fonctionner avec les projets MSBuild) avec le nouvelles versions de MSBuild \ Visual Studio.
Compte tenu de tout cela, je vous recommande fortement de consulter un article connexe sur la prise en charge de Visual Studio 2017, «
Prise en charge de Visual Studio 2017 et Roslyn 2.0 dans PVS-Studio: parfois, ce n'est pas si facile à utiliser des solutions prêtes à l'emploi, comme cela peut sembler ", avant de poursuivre la lecture. Cet article traite des problèmes que nous avons rencontrés la dernière fois et du modèle d'interaction entre différents composants (tels que PVS-Studio, MSBuild et Roslyn). La connaissance de ces détails peut vous aider à mieux comprendre l'article actuel.
La résolution de ces problèmes a finalement conduit à des modifications importantes de l'analyseur, et nous espérions que les nouvelles approches appliquées alors nous aideraient à prendre en charge les futures versions de Visual Studio \ MSBuild beaucoup plus facilement et plus rapidement. Cet espoir a déjà commencé à se révéler réaliste avec la sortie des nombreuses mises à jour de Visual Studio 2017. La nouvelle approche nous a-t-elle aidé à prendre en charge Visual Studio 2019? Lisez la suite pour le découvrir.
Plugin PVS-Studio pour Visual Studio 2019
Le début semblait prometteur. Il ne nous a pas fallu beaucoup d'efforts pour porter le plugin sur Visual Studio 2019 et le faire démarrer et fonctionner correctement. Mais nous avons déjà rencontré deux problèmes à la fois qui pourraient causer plus de problèmes plus tard.
La première concernait l'interface
IVsSolutionWorkspaceService utilisée pour prendre en charge le mode Lightweight Solution Load (qui, soit dit en passant, avait été désactivé dans l'une des mises à jour précédentes, de retour dans Visual Studio 2017). Il était décoré de l'attribut
obsolète , qui ne déclenchait actuellement qu'un avertissement au moment de la construction mais allait devenir un gros problème à l'avenir. Ce mode n'a pas duré longtemps en effet ... C'était facile à corriger - nous avons simplement arrêté d'utiliser cette interface.
Le deuxième problème était le message suivant que nous recevions toujours lors du chargement de Visual Studio avec le plugin activé:
Visual Studio a détecté une ou plusieurs extensions à risque ou ne fonctionnant pas dans une mise à jour VS de fonctionnalité.Les journaux des lancements de Visual Studio (le fichier ActivityLog) ont aidé à le clarifier:
Avertissement: l'extension «PVS-Studio» utilise la fonction «chargement automatique synchrone» de Visual Studio. Cette fonctionnalité ne sera plus prise en charge dans une future mise à jour de Visual Studio 2019, auquel cas cette extension ne fonctionnera pas. Veuillez contacter le fournisseur d'extensions pour obtenir une mise à jour.Cela signifiait pour nous que nous devions passer du mode de chargement synchrone au mode de chargement asynchrone. J'espère que cela ne vous dérangera pas si je vous épargne les détails de la façon dont nous interagissons avec les interfaces COM de Visual Studio, et ne décris que brièvement les changements.
Il y a un article de Microsoft sur le chargement des plugins de manière asynchrone: "
Comment: utiliser AsyncPackage pour charger des VSPackages en arrière-plan ". Il était cependant déjà clair qu'il y avait d'autres changements à venir.
L'un des changements les plus importants concernait le mode de chargement, ou plutôt le mode d'initialisation. Dans les versions antérieures, toute l'initialisation nécessaire était effectuée à l'aide de deux méthodes:
initialisation de notre classe héritant de
Package et
OnShellPropertyChange . Ce dernier a dû être ajouté car lors du chargement synchrone, Visual Studio lui-même pouvait encore être en cours de chargement et d'initialisation, et, par conséquent, certaines des actions nécessaires étaient impossibles à effectuer pendant l'initialisation du plugin. Une façon de résoudre ce problème était de retarder l'exécution de ces actions jusqu'à ce que Visual Studio quitte l'état «zombie». C'est cette partie de la logique que nous avons distinguée dans la méthode
OnShellPropertyChange avec une vérification du statut de «zombie».
La méthode
Initialize de la classe abstraite
AsyncPackage , dont les plugins de chargement asynchrones héritent, est
scellée , donc l'initialisation doit être effectuée dans la méthode surchargée
InitializeAsync , qui est exactement ce que nous avons fait. La logique de vérification des «zombies» a également dû être modifiée car les informations d'état n'étaient plus disponibles pour notre plugin. En outre, nous devions toujours effectuer les actions qui devaient être effectuées après l'initialisation du plugin. Nous avons résolu cela en utilisant la méthode
OnPackageLoaded de l'interface
IVsPackageLoadEvents , qui est l'endroit où ces actions différées ont été effectuées.
Un autre problème résultant de la charge asynchrone était que les commandes du plugin ne pouvaient pas être utilisées avant le chargement de Visual Studio. L'ouverture du journal de l'analyseur en double-cliquant dans le gestionnaire de fichiers (si vous aviez besoin de l'ouvrir à partir de Visual Studio) a entraîné le lancement de la version correspondante de devenv.exe avec une commande d'ouverture du journal. La commande de lancement ressemblait à ceci:
"C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\Common7\IDE\devenv.exe" /command "PVSStudio.OpenAnalysisReport C:\Users\vasiliev\source\repos\ConsoleApp\ConsoleApp.plog"
L'indicateur "/ command" est utilisé ici pour exécuter la commande enregistrée dans Visual Studio. Cette approche ne fonctionnait plus puisque les commandes n'étaient plus disponibles avant le chargement du plugin. La solution de contournement que nous avons trouvée consistait à analyser la commande de lancement devenv.exe après le chargement du plug-in et à exécuter la commande log open si elle se trouve dans la commande de lancement. Ainsi, rejeter l'idée d'utiliser l'interface "appropriée" pour travailler avec des commandes nous a permis de conserver les fonctionnalités nécessaires, avec une ouverture retardée du journal après le chargement complet du plugin.
Ouf, on dirait que nous l'avons enfin fait; le plugin se charge et s'ouvre comme prévu, sans aucun avertissement.
Et voici quand les choses tournent mal. Paul (Salut Paul!) Installe le plugin sur son ordinateur et demande pourquoi nous ne sommes toujours pas passés en charge asynchrone.
Dire que nous avons été choqués serait un euphémisme. Ça ne pouvait pas être ça! Mais c'est réel: voici la nouvelle version du plugin, et voici un message disant que le package se charge de manière synchrone. Alexander (Salut Alexander!) Et j'essaie la même version sur nos ordinateurs respectifs - cela fonctionne très bien. Comment est-ce possible? Ensuite, il nous vient à l'esprit de vérifier les versions des bibliothèques PVS-Studio chargées dans Visual Studio - et nous constatons que ce sont les bibliothèques pour Visual Studio 2017, tandis que le package VSIX contient les nouvelles versions, c'est-à-dire pour Visual Studio 2019.
Après avoir bricolé avec VSIXInstaller pendant un certain temps, nous avons réussi à découvrir que le problème était lié au cache des packages. Cette théorie a également été appuyée par le fait que la restriction d'accès au package mis en cache (C: \ ProgramData \ Microsoft \ VisualStudio \ Packages) provoquée par VSIXInstaller pour afficher un message d'erreur dans le journal. Curieusement, lorsque l'erreur ne s'est pas produite, les informations sur l'installation des packages mis en cache n'apparaissent pas.
Note de côté . En étudiant le comportement de VSIX Installer et des bibliothèques qui l'accompagnent, j'ai pensé à quel point il est cool que Roslyn et MSBuild soient open-source, ce qui vous permet de lire et de déboguer facilement leur code et de suivre sa logique de travail.
Donc, c'est ce qui s'est passé: lors de l'installation du plugin, le programme d'installation de VSIX a vu que le package correspondant était déjà mis en cache (il s'agissait en fait du package .vsix pour Visual Studio 2017) et a installé ce package au lieu du nouveau. Pourquoi il a ignoré les restrictions / exigences définies dans le fichier .vsixmanifest (qui, entre autres, restreint l'installation d'extensions à une version spécifique de Visual Studio) est une question à laquelle il reste à répondre. En conséquence, le plug-in conçu pour Visual Studio 2017 a été installé sur Visual Studio 2019 - malgré les restrictions spécifiées dans le fichier .vsixmanifest.
Pire encore, cette installation a brisé le graphique des dépendances de Visual Studio, et bien que l'EDI semble bien fonctionner, les choses étaient en fait terribles. Vous ne pouvez pas installer ou supprimer des extensions, des mises à jour, etc. Le processus de "restauration" a également été pénible car nous avons dû supprimer l'extension (c'est-à-dire les fichiers qui la contiennent) manuellement et - également manuellement - éditer les fichiers de configuration stockant les informations sur le package installé. En d'autres termes, ce n'était pas amusant du tout.
Pour résoudre ce problème et nous assurer que nous ne rencontrerons pas de telles situations à l'avenir, nous avons décidé de créer notre propre GUID pour le nouveau package afin que les packages pour Visual Studio 2017 et Visual Studio 2019 soient isolés les uns des autres en toute sécurité ( les anciens paquets étaient bien, ils avaient toujours utilisé un GUID partagé).
Depuis que nous avons commencé à parler de mauvaises surprises, en voici une autre: après la mise à jour vers l'aperçu 2, le menu PVS-Studio "s'est déplacé" vers l'onglet "Extensions". Pas très grave, mais cela rendait l'accès aux fonctionnalités du plugin moins pratique. Ce comportement a persisté dans les prochaines versions de Visual Studio 2019, y compris la version. Je n'ai trouvé de mention de cette "fonctionnalité" ni dans la documentation ni dans le blog.
D'accord, maintenant les choses allaient bien et nous semblions avoir enfin fini avec le support de Visual Studio 2019. Cela s'est avéré faux le lendemain après la sortie de PVS-Studio 7.02. C'était à nouveau le mode de chargement asynchrone. Lors de l'ouverture de la fenêtre des résultats d'analyse (ou du démarrage de l'analyse), la fenêtre de l'analyseur apparaîtrait "vide" pour l'utilisateur - pas de boutons, pas de grille, rien du tout.
Ce problème s'est en fait produit de temps en temps au cours de l'analyse. Mais cela n'a affecté qu'un seul ordinateur et n'est pas apparu avant la mise à jour de Visual Studio vers l'une des premières itérations de «Aperçu». Nous soupçonnions que quelque chose s'était cassé pendant l'installation ou la mise à jour. Le problème, cependant, a disparu quelque temps plus tard et ne se produirait même pas sur cet ordinateur particulier, nous avons donc pensé qu'il "s'est réglé tout seul". Mais non - nous avons juste eu de la chance. Ou malchanceux d'ailleurs.
Comme nous l'avons découvert, c'était l'ordre dans lequel la fenêtre IDE elle-même (la classe dérivée de
ToolWindowPane ) et son contenu (notre contrôle avec la grille et les boutons) étaient initialisés. Dans certaines conditions, le contrôle serait initialisé avant le volet et même si les choses fonctionnaient bien et que la méthode
FindToolWindowAsync (créant la fenêtre lors de son premier accès) faisait bien son travail, le contrôle restait invisible. Nous avons corrigé cela en ajoutant une initialisation paresseuse pour notre contrôle au code de remplissage de volet.
Prise en charge de C # 8.0
Il y a un grand avantage à utiliser Roslyn comme base pour l'analyseur: vous n'avez pas à ajouter manuellement la prise en charge des nouvelles constructions de langage - cela se fait automatiquement via les bibliothèques Microsoft. Code Analysis, et nous utilisons simplement les solutions prêtes à l'emploi. Cela signifie que la nouvelle syntaxe est prise en charge en mettant simplement à jour les bibliothèques.
Quant à l'analyse elle-même, nous avons dû modifier les choses par nous-mêmes, bien sûr - en particulier, gérer de nouvelles constructions de langage. Bien sûr, le nouvel arbre de syntaxe a été généré automatiquement en mettant simplement à jour Roslyn, mais nous avons encore dû enseigner à l'analyseur comment interpréter et traiter exactement les nœuds d'arbre de syntaxe nouveaux ou modifiés.
Les types de référence nullables sont peut-être la nouvelle fonctionnalité la plus largement discutée de C # 8. Je ne vais pas en parler maintenant, car un sujet aussi important mérite un article séparé (qui est actuellement en cours d'écriture). Pour l'instant, nous avons décidé d'ignorer les annotations annulables dans notre mécanisme de flux de données (c'est-à-dire que nous les comprenons, les analysons et les ignorons). L'idée est qu'une variable, même d'un type de référence non nullable, peut toujours être assez facilement (ou accidentellement) affectée à la valeur
null , se terminant par un NRE lors d'une tentative de déréférencement. Notre analyseur peut détecter de telles erreurs et signaler une éventuelle déréférence nulle (s'il trouve une telle affectation dans le code, bien sûr) même si la variable est de type référence non nullable.
L'utilisation de types de référence nullables et de la syntaxe associée vous permet d'écrire du code assez intéressant. Nous l'avons surnommé «syntaxe émotionnelle». Cet extrait est parfaitement compilable:
obj.Calculate(); obj?.Calculate(); obj.Calculate(); obj!?.Calculate(); obj!!!.Calculate();
Au fait, mes expériences m'ont amené à découvrir quelques astuces que vous pouvez utiliser pour "planter" Visual Studio en utilisant la nouvelle syntaxe. Ils sont basés sur le fait que vous êtes autorisé à écrire autant de '!' personnages comme vous le souhaitez. Cela signifie que vous pouvez écrire non seulement du code comme celui-ci:
object temp = null!
mais aussi comme ça:
object temp = null!!!;
Et, en poussant encore plus loin, vous pourriez écrire des choses folles comme ceci:
object temp = null!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!;
Ce code est compilable, mais si vous essayez d'afficher l'arborescence de syntaxe dans Syntax Visualizer à partir du SDK de la plate-forme du compilateur .NET, Visual Studio se bloque.
Le rapport d'échec peut être extrait de l'Observateur d'événements:
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 vous devenez encore plus fou et ajoutez plusieurs fois plus de points d'exclamation, Visual Studio commencera à se bloquer tout seul, sans l'aide de Syntax Visualizer. Les bibliothèques Microsoft.CodeAnalysis et le compilateur csc.exe ne peuvent pas non plus gérer ce code.
Ces exemples sont artificiels, bien sûr, mais j'ai trouvé cette astuce drôle.
Jeu d'outils
Il était évident que la mise à jour de l'ensemble d'outils serait la partie la plus difficile. C'est du moins ce à quoi cela ressemblait au début, mais maintenant j'ai tendance à penser que le support du plugin était la partie la plus difficile. D'une part, nous avions déjà un ensemble d'outils et un mécanisme pour évaluer les projets MSBuild, ce qui était bien car il était encore à étendre. Le fait que nous n'ayons pas dû écrire les algorithmes à partir de zéro a rendu la tâche beaucoup plus facile. La stratégie consistant à s'appuyer sur «notre» jeu d'outils, à laquelle nous avons préféré nous tenir lors de la prise en charge de Visual Studio 2017, s'est une fois de plus avérée correcte.
Traditionnellement, le processus commence par la mise à jour des packages NuGet. L'onglet de gestion des packages NuGet pour la solution actuelle contient le bouton "Mettre à jour" ... mais cela n'aide pas. La mise à jour de tous les packages à la fois a provoqué plusieurs conflits de versions et essayer de les résoudre tous ne semblait pas une bonne idée. Un moyen plus douloureux mais sans doute plus sûr était de mettre à jour de manière sélective les packages cibles de Microsoft.Build / Microsoft.CodeAnalysis.
Une différence a été immédiatement détectée lors du test des diagnostics: la structure de l'arbre de syntaxe a changé sur un nœud existant. Pas un gros problème; nous avons corrigé cela rapidement.
Je vous rappelle que nous testons nos analyseurs (pour C #, C ++, Java) sur des projets open-source. Cela nous permet de tester minutieusement les diagnostics - par exemple, de les vérifier pour les faux positifs ou de voir si nous avons raté des cas (pour réduire le nombre de faux négatifs). Ces tests nous aident également à tracer une éventuelle régression à l'étape initiale de la mise à jour des bibliothèques / outils. Cette fois, ils ont également détecté un certain nombre de problèmes.
L'une était que le comportement à l'intérieur des bibliothèques CodeAnalysis s'était aggravé. Plus précisément, lors de la vérification de certains projets, nous avons commencé à obtenir des exceptions du code des bibliothèques sur diverses opérations telles que l'obtention d'informations sémantiques, l'ouverture de projets, etc.
Ceux d'entre vous qui ont lu attentivement l'article sur la prise en charge de Visual Studio 2017 se souviennent que notre distribution est livrée avec un mannequin - le fichier MSBuild.exe de 0 octet.
Nous devions maintenant pousser cette pratique encore plus loin et inclure des variables muettes vides pour les compilateurs csc.exe, vbc.exe et VBCSCompiler.exe. Pourquoi? Nous avons trouvé cette solution après avoir analysé l'un des projets de notre base de test et obtenu des rapports de diff: la nouvelle version de l'analyseur ne produirait pas certains des avertissements attendus.
Nous avons constaté qu'il s'agissait de symboles de compilation conditionnelle, dont certains n'étaient pas extraits correctement lors de l'utilisation de la nouvelle version de l'analyseur. Afin d'aller à la racine du problème, nous avons dû approfondir le code des bibliothèques de Roslyn.
Les symboles de compilation conditionnelle sont analysés à l'aide de la méthode
GetDefineConstantsSwitch de la classe
Csc de la bibliothèque
Microsoft.Build.Tasks.CodeAnalysis . L'analyse est effectuée à l'aide de la méthode
String.Split sur un certain nombre de séparateurs:
string[] allIdentifiers = originalDefineConstants.Split(new char[] { ',', ';', ' ' });
Ce mécanisme d'analyse fonctionne parfaitement; tous les symboles de compilation conditionnelle sont extraits correctement. D'accord, continuons à creuser.
Le point clé suivant a été l'appel de la méthode
ComputePathToTool de la classe
ToolTask . Cette méthode calcule le chemin d'accès au fichier exécutable (
csc.exe ) et vérifie s'il s'y trouve. Si tel est le cas, la méthode renvoie le chemin d'accès ou
null sinon.
Le code appelant:
.... string pathToTool = ComputePathToTool(); if (pathToTool == null) {
Puisqu'il n'y a pas de fichier
csc.exe (pourquoi en avons-nous besoin?),
PathToTool reçoit la valeur
null à ce stade et la méthode actuelle (
ToolTask.Execute ) renvoie
false . Les résultats de l'exécution de la tâche, y compris les symboles de compilation conditionnelle extraits, sont ignorés.
Bon, voyons ce qui se passe si nous plaçons le fichier
csc.exe où il devrait se trouver.
PathToTool stocke maintenant le chemin réel vers le fichier maintenant présent et
ToolTask.Execute continue de s'exécuter. Le point clé suivant est l'appel de la méthode
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 propriété
SkipCompilerExecution est
vraie (assez logiquement puisque nous ne compilons pas pour de vrai). La méthode appelante (
ToolTask.Execute déjà mentionnée) vérifie si la valeur de retour pour
ExecuteTool est 0 et, si tel est le cas, renvoie
true . Que votre
csc.exe soit un véritable compilateur ou "War and Peace" de Leo Tolstoy n'a pas d'importance du tout.
Ainsi, le problème est lié à l'ordre dans lequel les étapes ont été définies:
- vérifier le compilateur;
- vérifier si le compilateur doit être lancé;
Et nous nous attendrions à un ordre inverse. C'est pour corriger cela que les mannequins des compilateurs ont été ajoutés.
D'accord, mais comment avons-nous réussi à obtenir des symboles de compilation, avec le fichier csc.exe absent (et les résultats de la tâche ignorés)?
Eh bien, il existe également une méthode pour ce cas:
CSharpCommandLineParser.ParseConditionalCompilationSymbols de la bibliothèque
Microsoft.CodeAnalysis.CSharp . Il effectue également l'analyse en appelant la méthode
String.Split sur un certain nombre de séparateurs:
string[] values = value.Split(new char[] { ';', ',' } );
Vous voyez comment cet ensemble de séparateurs est différent de celui géré par la méthode
Csc.GetDefineConstantsSwitch ? Ici, un espace n'est pas un séparateur. Cela signifie que les symboles de compilation conditionnelle séparés par des espaces ne seront pas analysés correctement par cette méthode.
C'est ce qui s'est produit lorsque nous avons vérifié les projets problématiques: ils ont utilisé des symboles de compilation conditionnelle séparés par des espaces et ont donc été analysés avec succès par la méthode
GetDefineConstantsSwitch mais pas la méthode
ParseConditionalCompilationSymbols .
Un autre problème qui est apparu après la mise à jour des bibliothèques était un comportement cassé dans certains cas - en particulier sur les projets qui ne se sont pas construits. Il a affecté les bibliothèques Microsoft.
Code Analysis et s'est manifesté sous la forme d'exceptions de toutes sortes:
ArgumentNullException (échec de l'initialisation d'un enregistreur interne),
NullReferenceException , etc.
Je voudrais vous parler d'une erreur particulière que j'ai trouvée assez intéressante.
Nous l'avons rencontré lors de la vérification de la nouvelle version du projet Roslyn: l'une des bibliothèques lançait une
NullReferenceException . Grâce à des informations détaillées sur sa source, nous avons rapidement trouvé le code source du problème et - juste pour la curiosité - nous avons décidé de vérifier si l'erreur persisterait lors du travail dans Visual Studio.
Nous avons réussi à le reproduire dans Visual Studio (version 16.0.3). Pour ce faire, vous avez besoin d'une définition de classe comme celle-ci:
class C1<T1, T2> { void foo() { T1 val = default; if (val is null) { } } }
Vous aurez également besoin du visualiseur de syntaxe (il est fourni avec le SDK de la plate-forme du compilateur .NET). Recherchez le
TypeSymbol (en cliquant sur l'élément de menu "Afficher TypeSymbol (le cas échéant)") du nœud d'arborescence de syntaxe de type
ConstantPatternSyntax (
null ). Visual Studio va redémarrer et les informations d'exception - en particulier, la trace de la pile - seront disponibles dans l'Observateur d'événements:
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) ....
Comme vous pouvez le voir, le problème est dû à une déréférence de référence nulle.
Comme je l'ai déjà mentionné, nous avons rencontré un problème similaire lors du test de l'analyseur. Si vous le construisez à l'aide de bibliothèques de débogage de Microsoft.
Analyse de code , vous pouvez accéder directement à l'emplacement du problème en recherchant le
TypeSymbol du nœud d'arborescence de syntaxe correspondant.
Cela nous mènera finalement à la méthode
ClassifyImplicitBuiltInConversionSlow mentionnée dans la trace de pile ci-dessus:
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; }
Ici, le paramètre de
destination est
nul , donc l'appel à
destination.SpecialType entraîne la
levée d' une
NullReferenceException . Oui, l'opération de déréférencement est précédée de
Debug.Assert , mais elle n'aide pas car en fait elle ne protège de rien - elle vous permet simplement de repérer le problème dans les versions de débogage des bibliothèques. Ou ce n'est pas le cas.
Modifications du mécanisme d'évaluation des projets C ++
Il n'y avait pas grand-chose d'intéressant dans cette partie: les algorithmes existants ne nécessitaient pas de grandes modifications à noter, mais vous voudrez peut-être connaître deux problèmes mineurs.
La première était que nous devions modifier les algorithmes qui s'appuyaient sur la valeur numérique de ToolsVersion. Sans entrer dans les détails, il existe certains cas où vous devez comparer des ensembles d'outils et choisir, par exemple, la version la plus récente. La nouvelle version a naturellement une valeur plus élevée. Nous nous attendions à ce que ToolsVersion pour le nouveau MSBuild / Visual Studio ait la valeur 16.0. Ouais, bien sûr! Le tableau ci-dessous montre comment les valeurs des différentes propriétés ont changé tout au long de l'historique de développement de Visual Studio:
Je sais que la blague sur les numéros de version foirés de Windows et Xbox est ancienne, mais elle prouve que vous ne pouvez pas faire de prédictions fiables sur les valeurs (que ce soit dans le nom ou la version) des futurs produits Microsoft. :)
Nous avons résolu cela facilement en ajoutant la hiérarchisation des ensembles d'outils (c.-à-d. En distinguant la priorité en tant qu'entité distincte).
Le deuxième problème concernait des problèmes de travail dans Visual Studio 2017 ou dans un environnement associé (par exemple, lorsque la variable d'environnement
VisualStudioVersion est définie). Cela se produit car le calcul des paramètres nécessaires à l'évaluation d'un projet C ++ est une tâche beaucoup plus difficile que l'évaluation d'un projet .NET. Pour .NET, nous utilisons notre propre ensemble d'outils et la valeur correspondante de ToolsVersion. Pour C ++, nous pouvons utiliser à la fois notre propre ensemble d'outils et ceux fournis par le système. À partir de Build Tools pour Visual Studio 2017, les jeux d'outils sont définis dans le fichier
MSBuild.exe.config au lieu du registre. C'est pourquoi nous ne pouvions plus les obtenir à partir de la liste globale des jeux d'outils (à l'aide de
Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.Toolsets , par exemple) contrairement à ceux définis dans le registre (c'est-à-dire pour Visual Studio 2015 et versions antérieures).
Tout cela nous empêche d'évaluer un projet à l'aide de
ToolsVersion 15.0 car le système ne verra pas le jeu d'outils requis. Le jeu d'outils le plus récent,
Current , sera toujours disponible car il s'agit de notre propre jeu d'outils, et, par conséquent, il n'y a pas un tel problème dans Visual Studio 2019. La solution était assez simple et nous a permis de résoudre cela sans changer les algorithmes d'évaluation existants: nous venons de a dû inclure un autre jeu d'outils,
15.0 , dans la liste de nos propres jeux d'outils en plus de
Current .
Modifications du mécanisme d'évaluation des projets C # .NET Core
Cette tâche impliquait deux problèmes interdépendants:
- l'ajout de l'ensemble d'outils «actuel» a interrompu l'analyse des projets .NET Core dans Visual Studio 2017;
- l'analyse ne fonctionnerait pas pour les projets .NET Core sur les systèmes sans au moins une copie de Visual Studio installée.
Les deux problèmes provenaient de la même source: certains des fichiers de base .targets / .props étaient recherchés sur des chemins incorrects. Cela nous a empêchés d'évaluer un projet à l'aide de notre ensemble d'outils.
Si vous n'aviez aucune instance de Visual Studio installée, vous obtiendrez l'erreur suivante (avec la version précédente de l'ensemble d'outils,
15.0 ):
The imported project "C:\Windows\Microsoft.NET\Framework64\ 15.0\Microsoft.Common.props" was not found.
Lors de l'évaluation d'un projet C # .NET Core dans Visual Studio 2017, vous obtenez l'erreur suivante (avec la version actuelle du jeu d'outils,
Current ):
The imported project "C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\MSBuild\Current\Microsoft.Common.props" was not found. ....
Comme ces problèmes sont similaires (ce qu'ils semblent être), nous pourrions essayer de tuer deux oiseaux avec une pierre.
Dans les paragraphes suivants, j'expliquerai comment nous y sommes parvenus, sans entrer dans les détails. Ces détails (sur la façon dont les projets C # .NET Core sont évalués ainsi que les modifications apportées au mécanisme d'évaluation dans notre ensemble d'outils) seront le sujet de l'un de nos futurs articles. Soit dit en passant, si vous lisiez attentivement cet article, vous avez probablement remarqué qu'il s'agit de la deuxième référence à nos futurs articles. :)
Maintenant, comment avons-nous résolu ce problème? Nous avons étendu notre propre ensemble d'outils avec les fichiers de base .targets / .props du SDK .NET Core (
Sdk.props ,
Sdk.targets ). Cela nous a donné plus de contrôle sur la situation et plus de flexibilité dans la gestion des importations ainsi que l'évaluation des projets .NET Core en général. Oui, notre ensemble d'outils est redevenu un peu plus grand, et nous avons également dû ajouter une logique pour configurer l'environnement requis pour l'évaluation des projets .NET Core, mais cela en vaut la peine.
Jusque-là, nous avions évalué des projets .NET Core en demandant simplement l'évaluation et en comptant sur MSBuild pour faire le travail.
Maintenant que nous avions plus de contrôle sur la situation, le mécanisme a un peu changé:
- configurer l'environnement requis pour évaluer les projets .NET Core;
- évaluation:
- commencer l'évaluation en utilisant les fichiers .targets / .props de notre ensemble d'outils;
- poursuivre l'évaluation à l'aide de fichiers externes.
Cette séquence suggère que la configuration de l'environnement poursuit deux objectifs principaux:
- lancer l'évaluation à l'aide des fichiers .targets / .props de notre ensemble d'outils;
- redirige toutes les opérations suivantes vers des fichiers externes .targets / .props.
Une bibliothèque spéciale Microsoft.DotNet.MSBuildSdkResolver est utilisée pour rechercher les fichiers .targets / .props nécessaires. Afin de lancer la configuration de l'environnement à l'aide des fichiers de notre ensemble d'outils, nous avons utilisé une variable d'environnement spéciale utilisée par cette bibliothèque afin que nous puissions pointer vers la source d'où importer les fichiers nécessaires (c'est-à-dire notre ensemble d'outils). Puisque la bibliothèque est incluse dans notre distribution, il n'y a aucun risque de panne logique soudaine.
Nous avons maintenant les fichiers Sdk de notre ensemble d'outils importés en premier, et puisque nous pouvons facilement les modifier maintenant, nous contrôlons entièrement le reste de la logique d'évaluation. Cela signifie que nous pouvons maintenant décider quels fichiers et de quel emplacement importer. Il en va de même pour Microsoft.Common.props mentionné ci-dessus. Nous importons ce fichier et d'autres fichiers de base de notre ensemble d'outils afin de ne pas avoir à nous soucier de leur existence ou de leur contenu.
Une fois toutes les importations nécessaires effectuées et les propriétés définies, nous transférons le contrôle du processus d'évaluation au SDK .NET Core réel, où toutes les autres opérations requises sont effectuées.
Conclusion
La prise en charge de Visual Studio 2019 était généralement plus facile que la prise en charge de Visual Studio 2017 pour un certain nombre de raisons. Tout d'abord, Microsoft n'a pas changé autant de choses que lors de la mise à jour de Visual Studio 2015 vers Visual Studio 2017. Oui, ils ont changé le jeu d'outils de base et forcé les plug-ins Visual Studio à passer en mode de chargement asynchrone, mais ce changement n'a pas été que drastique. Deuxièmement, nous avions déjà une solution prête à l'emploi impliquant notre propre ensemble d'outils et un mécanisme d'évaluation de projet et nous n'avions tout simplement pas à tout recommencer à zéro - uniquement à partir de ce que nous avions déjà. Le processus relativement indolore de prise en charge de l'analyse des projets .NET Core dans de nouvelles conditions (et sur des ordinateurs sans copie de Visual Studio installée) en étendant notre système d'évaluation de projet nous donne également l'espoir que nous avons fait le bon choix en prenant une partie du contrôle dans nos mains.
Mais je voudrais répéter l'idée communiquée dans l'article précédent: parfois, l'utilisation de solutions toutes faites n'est pas aussi simple que cela puisse paraître.