在PVS-Studio中支持Visual Studio 2019


PVS-Studio对Visual Studio 2019的支持影响了许多组件:插件本身,命令行分析器,C ++和C#分析器的核心以及一些实用程序。 在本文中,我将简要说明在实现IDE支持时遇到的问题以及如何解决这些问题。

在开始之前,我想回顾一下在PVS-Studio中支持Visual Studio以前版本的历史,以便您更好地了解我们对每种情况下提出的任务和解决方案的看法。

自从附带用于Visual Studio的插件的第一版PVS-Studio(当时为Visual Studio 2005)以来,支持此IDE的新版本对我们来说是一项微不足道的任务,基本上归结为更新插件的项目文件和Visual Studio各种API扩展的依赖关系。 我们有时会不时地增加对C ++新功能的支持,Visual C ++编译器正在逐步学习使用C ++的新功能,但这通常也不是一件容易的事,并且可以在新的Visual Studio版本发布之前轻松完成。 。 此外,PVS-Studio当时只有一个分析仪-用于C / C ++。

Visual Studio 2017发布时情况发生了变化。 除了对许多IDE的API扩展进行了巨大的更改之外,我们还遇到了一个问题,即在此之前保持了不久前添加的新C#分析器(以及与MSBuild项目一起使用的C ++新分析器层)的向后兼容性。新版本的MSBuild \ Visual Studio。

考虑到所有这些,我强烈建议您阅读有关支持Visual Studio 2017的相关文章,“ PVS-Studio中对Visual Studio 2017和Roslyn 2.0的支持:有时使用现成的解决方案似乎并不那么容易 ”,然后继续阅读。 该文章讨论了我们上次遇到的问题以及不同组件(例如PVS-Studio,MSBuild和Roslyn)之间的交互模型。 了解这些详细信息可能有助于您更好地理解当前文章。

解决这些问题最终导致分析仪发生重大变化,我们希望那时所采用的新方法将有助于我们更轻松,更快地支持Visual Studio \ MSBuild的未来版本。 随着Visual Studio 2017的大量更新发布,这种希望已经开始变得现实。 新方法是否有助于我们支持Visual Studio 2019? 继续阅读以找出答案。

适用于Visual Studio 2019的PVS-Studio插件


开始似乎是有希望的。 将插件移植到Visual Studio 2019并使其正常启动和运行不需要花费太多精力。 但是我们已经同时遇到了两个问题,以后可能会带来更多麻烦。

第一个与用于支持轻型解决方案加载模式的IVsSolutionWorkspaceService接口有关(顺便说一句,早在Visual Studio 2017中,该更新已在较早的更新之一中被禁用)。 它使用Deprecated属性进行装饰,该属性当前仅在构建时触发警告,但将来将成为一个大问题。 实际上,这种模式并没有持续很长时间……这很容易解决-我们只是停止使用此界面。

第二个问题是在启用插件的情况下加载Visual Studio时,我们始终收到以下消息: Visual Studio已检测到一个或多个扩展存在风险或在功能VS更新中不起作用。

Visual Studio启动日志(ActivityLog文件)有助于清除它:

警告:扩展名“ PVS-Studio”使用Visual Studio的“同步自动加载”功能。 将来的Visual Studio 2019更新中将不再支持此功能,此时此扩展将不起作用。 请联系扩展供应商以获取更新。

对我们而言,这意味着我们必须从同步加载模式切换到异步加载模式。 我希望您能介意我如何与Visual Studio的COM界面进行交互的细节,只简要概述所做的更改。

微软有一篇关于异步加载插件的文章:“ 如何:使用AsyncPackage在后台加载VSPackages ”。 但是,已经很清楚,还会有更多的变化。

最大的变化之一是在加载模式下,或者在初始化模式下。 在早期版本中,所有必需的初始化都使用两种方法完成:继承自Package的类的初始化OnShellPropertyChange 。 必须添加后者,因为在同步加载时,Visual Studio本身可能仍在加载和初始化过程中,因此,在插件初始化期间无法执行某些必要的操作。 解决此问题的一种方法是将这些动作的执行延迟到Visual Studio退出“僵尸”状态为止。 这就是逻辑的这一部分,我们通过检查“僵尸”状态来挑选到OnShellPropertyChange方法中。

异步加载插件继承的抽象类AsyncPackageInitialize方法是密封的 ,因此初始化必须在重写的方法InitializeAsync中进行 ,这正是我们所做的。 “僵尸”检查逻辑也必须更改,因为状态信息不再可用于我们的插件。 此外,我们仍然必须执行插件初始化之后必须执行的那些操作。 我们通过利用IVsPackageLoadEvents接口的OnPackageLoaded方法解决了该问题,在该方法中执行了这些延迟的操作。

异步加载导致的另一个问题是,只有在Visual Studio加载后才能使用插件的命令。 通过双击文件管理器(如果需要从Visual Studio打开它)来打开分析器日志,会导致使用打开日志的命令启动相应版本的devenv.exe。 启动命令如下所示:

"C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\Common7\IDE\devenv.exe" /command "PVSStudio.OpenAnalysisReport C:\Users\vasiliev\source\repos\ConsoleApp\ConsoleApp.plog" 

此处使用“ /命令”标志来运行在Visual Studio中注册的命令。 这种方法不再起作用,因为直到插件加载后命令才可用。 我们想出的解决方法是在插件加载后解析devenv.exe启动命令,并在启动命令中找到该命令后运行log open命令。 因此,放弃使用“适当的”接口来处理命令的想法使我们得以保留必要的功能,并在插件完全加载后延迟打开日志。

ew,看来我们终于做到了; 该插件会按预期加载并打开,没有任何警告。

这是当事情出错时。 Paul(嗨,Paul!)在他的计算机上安装了插件,并询问为什么我们仍然没有切换到异步负载。

说我们感到震惊将是轻描淡写。 那不可能! 但这是真的:这是插件的新版本,并且有一条消息说软件包正在同步加载。 亚历山大(亚历山大!嗨! 那怎么可能 然后我们想到检查在Visual Studio中加载的PVS-Studio库的版本-我们发现它们是Visual Studio 2017的库,而VSIX软件包包含新版本,即Visual Studio 2019。

在修改了VSIXInstaller一段时间之后,我们设法发现问题与软件包缓存有关。 由于VSIXInstaller限制了对缓存包(C:\ ProgramData \ Microsoft \ VisualStudio \ Packages)的访问以在日志中输出错误消息,因此也支持了该理论。 奇怪的是,当没有发生错误时,没有显示有关安装缓存软件包的信息。

旁注 。 在研究VSIX Installer和随附库的行为时,我认为Roslyn和MSBuild是开源的,这真是太酷了,它使您可以方便地读取和调试其代码并跟踪其工作逻辑。

因此,发生了这样的事情:安装插件时,VSIX Installer看到相应的软件包已被缓存(实际上是Visual Studio 2017的.vsix软件包)并安装了该软件包而不是新的软件包。 为什么它忽略了.vsixmanifest文件中定义的限制/要求(其中包括限制安装到特定版本的Visual Studio的扩展),这是一个有待解决的问题。 结果,尽管在.vsixmanifest文件中指定了限制,但为Visual Studio 2017设计的插件已安装在Visual Studio 2019中。

最糟糕的是,该安装破坏了Visual Studio的依赖关系图,尽管IDE似乎运行良好,但实际情况却很糟糕。 您无法安装或删除扩展程序,更新等。 “还原”过程也很麻烦,因为我们必须手动删除扩展名(即包含扩展名的文件),并且还必须手动编辑存储存储有关已安装软件包信息的配置文件。 换句话说,这根本不好玩。

为了解决此问题并确保将来不会遇到任何类似情况,我们决定为新软件包创建自己的GUID,以使Visual Studio 2017和Visual Studio 2019的软件包彼此安全隔离(较旧的软件包很好;他们一直使用共享的GUID)。

自从我们开始谈论令人不愉快的惊喜以来,这是另一个:更新到预览2后,PVS-Studio菜单“移至”“扩展”选项卡。 没什么大不了的,但是这使得访问插件功能变得不那么方便。 在包括发行版在内的下一个Visual Studio 2019版本中,此行为仍然存在。 我在文档和博客中都没有提到此“功能”。

好的,现在看起来一切正常,我们似乎终于完成了对Visual Studio 2019的支持。 在发布PVS-Studio 7.02之后的第二天,这证明是错误的。 再次是异步加载模式。 当打开分析结果窗口(或开始分析)时,分析器窗口对用户将显示为“空”-没有按钮,没有网格,什么也没有。

实际上,此问题在分析过程中时不时发生。 但是它只影响一台计算机,直到Visual Studio更新到“预览”的第一个迭代之一时才出现。 我们怀疑在安装或更新过程中发生了故障。 但是,问题在一段时间后消失了,即使在那台特定的计算机上也不会发生,因此我们认为它“自行解决”。 但是,不,我们只是幸运。 或不幸的是,就此而言。

正如我们发现的那样,这是IDE窗口本身(从ToolWindowPane派生的类)及其内容(我们使用网格和按钮的控件)初始化的顺序。 在某些条件下,该控件将在窗格之前初始化,即使一切运行顺利并且FindToolWindowAsync方法(在首次访问该窗口时创建该窗口)执行得很好,但该控件仍然不可见。 我们通过将控件的延迟初始化添加到窗格填充代码中来解决此问题。

支持C#8.0


使用Roslyn作为分析器的基础有一个很大的优势:您不必手动添加对新语言结构的支持-它是通过​​Microsoft代码分析库自动完成的,而我们只是使用现成的解决方案。 这意味着只需更新库即可支持新语法。

至于分析本身,我们当然必须自己调整一些东西-特别是处理新的语言构造。 当然,我们可以通过简单地更新Roslyn来自动生成新的语法树,但是我们仍然必须教会分析器如何准确地解释和处理新的或修改的语法树节点。

可为空的引用类型可能是C#8中讨论最广泛的新功能。我现在不再谈论它们了,因为一个大的主题值得一本单独的文章(目前正在撰写)。 现在,我们已经决定忽略数据流机制中的可为空的注释(即,我们理解,解析和跳过它们)。 这个想法是,即使是非空引用类型的变量,也仍然可以很容易地(或偶然地)将值赋值为null ,并在尝试取消引用时以NRE结尾。 即使变量的类型为非空引用,我们的分析器也可以发现此类错误并报告潜在的空取消引用(当然,如果在代码中找到了这样的赋值)。

使用可为空的引用类型和关联的语法使您可以编写非常有趣的代码。 我们将其昵称为“情感语法”。 该代码段是完全可编译的:

 obj.Calculate(); obj?.Calculate(); obj.Calculate(); obj!?.Calculate(); obj!!!.Calculate(); 

顺便说一下,我的实验使我发现了一些技巧,您可以使用这些技巧来使用新语法“崩溃” Visual Studio。 它们基于这样的事实,即您可以写很多“!”。 您喜欢的字符。 这意味着您不仅可以编写如下代码:

 object temp = null! 

但也这样:

 object temp = null!!!; 

而且,进一步推动它,您可以编写如下这样的疯狂内容:

 object temp = null!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!; 

该代码是可编译的,但是如果您尝试从.NET Compiler Platform SDK查看Syntax Visualizer中的语法树,Visual Studio将崩溃。

可以从事件查看器中提取故障报告:

 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: 

如果您更加疯狂,并添加了数倍的感叹号,则Visual Studio将自行崩溃,而无需Syntax Visualizer的任何帮助。 Microsoft.CodeAnalysis库和csc.exe编译器也无法处理此类代码。

当然,这些示例是人为设计的,但是我发现这个技巧很有趣。

工具集


显然,更新工具集将是最困难的部分。 至少一开始就是这样,但是现在我倾向于认为对插件的支持是最困难的部分。 一方面,我们已经有了一个用于评估MSBuild项目的工具集和一种机制,尽管还没有扩展,但它还是不错的。 我们不必从头开始编写算法,这一事实使它变得更容易。 再次证明了依赖“我们的”工具集的策略是正确的,在支持Visual Studio 2017时我们更喜欢坚持这一策略。

传统上,该过程从更新NuGet包开始。 用于管理当前解决方案的NuGet软件包的选项卡包含“更新”按钮...,但无济于事。 一次更新所有软件包会导致多个版本冲突,而试图解决所有冲突似乎不是一个好主意。 一种更痛苦但可能更安全的方法是有选择地更新Microsoft.Build/Microsoft.CodeAnalysis的目标程序包。

在测试诊断程序时立即发现了一个区别:语法树的结构在现有节点上已更改。 没什么大不了的; 我们很快解决了。

让我提醒您,我们在开源项目上测试分析器(针对C#,C ++,Java)。 这使我们能够彻底测试诊断程序-例如,检查它们是否存在误报或查看我们是否错过任何情况(以减少误报的数量)。 这些测试还帮助我们在更新库/工具集的初始步骤中跟踪可能的回归。 这次他们也遇到了许多问题。

一是CodeAnalysis库内部的行为变得更糟。 具体来说,当检查某些项目时,我们开始从库代码中的各种操作中获取异常,例如获取语义信息,打开项目等。

那些仔细阅读过有关Visual Studio 2017支持的文章的人请记住,我们的发行版附带了一个虚拟文件-0字节的MSBuild.exe文件。

现在,我们不得不进一步推动这种做法,并为编译器csc.exe,vbc.exe和VBCSCompiler.exe添加空的虚拟变量。 怎么了 在分析了测试基础中的一个项目并获得了差异报告后,我们提出了该解决方案:新版本的分析器不会输出某些预期的警告。

我们发现它与条件编译符号有关,其中某些条件在使用新版本的分析仪时无法正确提取。 为了找到问题的根源,我们不得不更深入地研究Roslyn库的代码。

使用库Microsoft.Build.Tasks.CodeAnalysis中的类CscGetDefineConstantsSwitch方法解析条件编译符号。 使用String.Split方法对多个分隔符进行解析:

 string[] allIdentifiers = originalDefineConstants.Split(new char[] { ',', ';', ' ' }); 

这种解析机制可以完美地工作。 正确提取了所有条件编译符号。 好吧,让我们继续挖掘。

下一个关键点是对ToolTask类的ComputePathToTool方法的调用。 此方法计算可执行文件( csc.exe )的路径,并检查其是否存在。 如果是这样,则该方法返回其路径,否则返回null

调用代码:

 .... string pathToTool = ComputePathToTool(); if (pathToTool == null) { // An appropriate error should have been logged already. return false; } .... 

由于没有csc.exe文件(为什么需要它?), PathToTool此时被分配为null值,并且当前方法( ToolTask​​.Execute )返回false 。 包括提取的条件编译符号在内的任务执行结果将被忽略。

好的,让我们看看如果将csc.exe文件放在预期的位置会发生什么。

现在pathToTool存储到当前文件的实际路径,并且ToolTask​​.Execute继续执行。 下一个关键点是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; } .... } 

SkipCompilerExecution属性为true (从逻辑上讲是足够的,因为我们没有进行真正的编译)。 调用方法(已经提到的ToolTask​​.Execute )检查ExecuteTool的返回值是否为0,如果是,则返回true 。 您的csc.exe是实际的编译器,还是Leo Tolstoy的“战争与和平”,都没有关系。

因此,问题与步骤定义的顺序有关:

  • 检查编译器;
  • 检查是否应该启动编译器;

而且我们期望相反的顺序。 为了解决这个问题,为编译器添加了虚拟变量。

好的,但是在没有csc.exe文件的情况下(如何忽略任务结果),我们如何设法获得编译符号?

好了,也有一种针对这种情况的方法:库Microsoft.CodeAnalysis.CSharp中的 CSharpCommandLineParser.ParseConditionalCompilationSymbols 。 它也通过在多个分隔符上调用String.Split方法来进行解析:

 string[] values = value.Split(new char[] { ';', ',' } /*, StringSplitOptions.RemoveEmptyEntries*/); 

看到这组分隔符与Csc.GetDefineConstantsSwitch方法处理的分隔符有何不同? 在这里,空格不是分隔符。 这意味着用这种方法无法正确解析由空格分隔的条件编译符号。

这就是我们检查问题项目时发生的事情:它们使用以空格分隔的条件编译符号,因此已通过GetDefineConstantsSwitch方法成功解析,但未通过ParseConditionalCompilationSymbols方法解析。

更新库后出现的另一个问题是在某些情况下的行为损坏-特别是在未构建的项目上。 它影响了Microsoft.Code Analysis库,并表现为各种异常: ArgumentNullException (某些内部记录器的初始化失败), NullReferenceException等。

我想告诉您一个特别有趣的错误。

我们在检查Roslyn项目的新版本时遇到了它:一个库抛出NullReferenceException 。 由于有了有关其源代码的详细信息,我们很快找到了问题的源代码,并且出于好奇,决定检查在Visual Studio中工作时错误是否仍然存在。

我们确实设法在Visual Studio(版本16.0.3)中重现了它。 为此,您需要这样的类定义:

 class C1<T1, T2> { void foo() { T1 val = default; if (val is null) { } } } 

您还需要Syntax Visualizer(.NET编译器平台SDK附带)。 查找类型为ConstantPatternSyntaxnull )的语法树节点的TypeSymbol (通过单击“查看TypeSymbol(如果有)”菜单项)。 Visual Studio将重新启动,并且异常信息-特别是堆栈跟踪-将在事件查看器中变为可用:

 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) .... 

如您所见,问题是由空引用取消引用引起的。

正如我已经提到的,在测试分析仪时,我们遇到了类似的问题。 如果使用Microsoft Code Analysis的调试库进行构建,则可以通过查找相应语法树节点的TypeSymbol来直接解决问题。

最终,它将带我们到上面的堆栈跟踪中提到的ClassifyImplicitBuiltInConversionSlow方法:

 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; } 

此处, destination参数为null ,因此调用destination.SpecialType引发NullReferenceException 。 是的,取消引用操作在Debug.Assert之前进行,但是它无济于事 ,因为实际上它不能提供任何保护,它只是使您能够在库的调试版本中发现问题。 或没有。

评估C ++项目的机制的变化


这部分没有什么有趣的:现有算法不需要任何值得一提的大修改,但是您可能想知道两个小问题。

首先是我们必须修改依赖ToolsVersion数值的算法。 在不赘述的情况下,在某些情况下,您需要比较工具集并选择最新版本。 自然,新版本具有更大的价值。 我们期望新的MSBuild / Visual Studio的ToolsVersion的值为16.0。 是的,当然! 下表显示了在Visual Studio的整个开发历史中,不同属性的值如何变化:
Visual Studio产品名称
Visual Studio版本号
工具版本
PlatformToolset版本
Visual Studio 2010
10.0
4.0
100
Visual Studio 2012
11.0
4.0
110
Visual Studio 2013
12.0
12.0
120
Visual Studio 2015
14.0
14.0
140
Visual Studio 2017
15.0
15.0
141
Visual Studio 2019
16.0
现时
142

我知道有关Windows和Xbox的版本号混乱的笑话是一个古老的笑话,但事实证明,您不能对未来的Microsoft产品的值(无论是名称还是版本)做出任何可靠的预测。 :)

我们通过为工具集添加优先级来轻松解决此问题(即,将优先级选为一个单独的实体)。

第二个问题涉及在Visual Studio 2017或相关环境中工作的问题(例如,设置了VisualStudioVersion环境变量时)。 发生这种情况的原因是,评估C ++项目所需的计算参数比评估.NET项目困难得多。 对于.NET,我们使用自己的工具集和相应的ToolsVersion值。 对于C ++,我们既可以利用我们自己的工具集,也可以利用系统提供的工具集。 从Visual Studio 2017的构建工具开始,工具集在文件MSBuild.exe.config中而不是在注册表中定义。 这就是为什么我们不能再从注册表的全局列表(例如,使用Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.Toolsets )中获取它们的原因,而不是注册表中定义的(例如,对于Visual Studio 2015及更早版本)。

所有这些使我们无法使用ToolsVersion 15.0评估项目,因为系统看不到所需的工具集。 最新的工具集Current仍然是可用的,因为它是我们自己的工具集,因此,在Visual Studio 2019中不存在此类问题。解决方案非常简单,可以在不更改现有评估算法的情况下对其进行修复:除了Current之外,我们还必须将另一个工具集15.0包含在我们自己的工具集中。

C#.NET Core项目评估机制的更改


该任务涉及两个相互关联的问题:

  • 添加``当前''工具集破坏了Visual Studio 2017中对.NET Core项目的分析;
  • 没有安装至少一个Visual Studio副本的系统上的.NET Core项目将无法进行分析。

这两个问题都来自同一个来源:某些基本的.targets / .props文件在错误的路径下查找。 这使我们无法使用工具集评估项目。

如果没有安装Visual Studio实例,则会出现以下错误(使用先前的工具集版本15.0 ):

 The imported project "C:\Windows\Microsoft.NET\Framework64\ 15.0\Microsoft.Common.props" was not found. 

在Visual Studio 2017中评估C#.NET Core项目时,会出现以下错误(使用当前工具集版本Current ):

 The imported project "C:\Program Files (x86)\Microsoft Visual Studio\ 2017\Community\MSBuild\Current\Microsoft.Common.props" was not found. .... 

由于这些问题很相似(看起来确实如此),我们可以尝试用一块石头杀死两只鸟。

在接下来的几段中,我将解释我们如何完成此任务,而无需赘述。 这些详细信息(有关如何评估C#.NET Core项目以及我们的工具集中的评估机制的更改)将成为我们未来文章之一的主题。 顺便说一句,如果您仔细阅读本文,您可能会注意到这是对我们将来文章的第二次引用。 :)

现在,我们如何解决这个问题? 我们使用.NET Core SDK中的基本.targets / .props文件( Sdk.propsSdk.targets )扩展了自己的工具集。 这样一来,我们就可以更好地控制情况,并在导入管理以及总体上评估.NET Core项目方面更具灵活性。 是的,我们的工具集又变得更大了,我们还必须添加逻辑来设置评估.NET Core项目所需的环境,但这似乎是值得的。

在此之前,我们仅通过请求评估并依靠MSBuild来完成.NET Core项目的评估。

现在,我们对情况有了更多的控制权,该机制有了一些变化:

  • 设置评估.NET Core项目所需的环境;
  • 评价:
    • 使用我们工具集中的.targets / .props文件开始评估;
    • 继续使用外部文件进行评估。

此顺序表明设置环境追求两个主要目标:

  • 使用我们工具集中的.targets / .props文件启动评估;
  • 将所有后续操作重定向到外部.targets / .props文件。

特殊的库Microsoft.DotNet.MSBuildSdkResolver用于查找必要的.targets / .props文件。 为了使用工具集中的文件启动环境的设置,我们利用了该库使用的特殊环境变量,以便我们可以指向从中导入必要文件的源(即工具集)。 由于该库已包含在我们的发行版中,因此不会发生逻辑突然故障的风险。

现在,我们首先导入了工具集中的Sdk文件,并且由于我们现在可以轻松更改它们,因此我们可以完全控制其余的评估逻辑。 这意味着我们现在可以决定要导入的文件和位置。 这同样适用于上述Microsoft.Common.props。 我们从工具集中导入此基本文件和其他基本文件,因此我们不必担心它们的存在或内容。

一旦完成了所有必要的导入并设置了属性,我们便将对评估过程的控制权传递给实际的.NET Core SDK,在此执行所有其余必需的操作。

结论


由于多种原因,通常比支持Visual Studio 2017更容易支持Visual Studio 2019。 首先,Microsoft从Visual Studio 2015升级到Visual Studio 2017时所做的更改不多。是的,他们确实更改了基本工具集并强制Visual Studio插件切换到异步加载模式,但此更改不是太厉害了 其次,我们已经有了一个包含我们自己的工具集和项目评估机制的现成解决方案,我们根本不必从头开始进行所有工作,而只是在现有的基础上进行工作。 通过扩展项目评估系统,在新条件下(和未安装Visual Studio副本的计算机上)支持对.NET Core项目进行分析的相对轻松的过程,也使我们希望我们可以通过控制某些项目来做出正确的选择。我们的手。

但是,我想重复上一篇文章中传达的想法:有时使用现成的解决方案似乎并不那么容易。

Source: https://habr.com/ru/post/zh-CN454736/


All Articles