PVS-Studio中对Visual Studio 2019的支持,第2部分


欢迎回来!如果您错过了第1部分,您可以查看它here

工具集

显然,更新工具集将是最困难的部分。至少一开始看起来是这样的,但是现在我倾向于认为插件的支持是最困难的部分。首先,我们已经有了一个工具集和一个评估MSBuild项目的机制,尽管它还有待扩展,但它还是很好的。事实上,我们不必从头开始编写算法,这使得它变得更加容易。我们在支持Visual Studio 2017时更愿意坚持的依赖“我们的”工具集的策略再次被证明是正确的。

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

在测试诊断时,我们马上发现了一个不同之处:语法树的结构在现有节点上发生了变化。没什么大不了的;我们很快就修好了。

让我提醒您,我们在开源项目上测试我们的分析器(针对C#,C++,Java)。这允许我们彻底测试诊断--例如,检查它们是否有假阳性,或者看看我们是否漏掉了任何病例(以减少假阴性的数量)。这些测试还帮助我们在更新库/工具集的初始步骤跟踪可能的回归。这一次,他们还发现了许多问题。

其中之一是代码分析库中的行为变得更糟。具体地说,在检查某些项目时,我们开始从库的各种操作(如获取语义信息,打开项目等)的代码中获得异常。

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

现在,我们必须进一步推进这种做法,并为编译器csc.exe,vbc.exe和vbcscompiler.exe包含空的虚设。为什么?我们在分析了测试基地中的一个项目并得到差异报告后,提出了这个解决方案:新版本的分析器不会输出一些预期的警告。

我们发现它与条件编译符号有关,其中一些符号在使用新版本的分析器时没有正确提取。为了正本清源,我们不得不深入挖掘Roslyn库的代码。

条件编译符号使用GetDefineConstantsSwitch 类的方法Csc 从图书馆Microsoft.Build.Tasks.CodeAnalysis解析是使用String.Split 方法在多个分隔符上:

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

这种解析机制工作得很好;正确提取所有条件编译符号。好吧,我们继续挖。

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

调用代码:

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

既然没有csc.exe文件(我们为什么需要它?),pathToTool被赋值此时,当前方法(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属性为(逻辑上足够,因为我们不是为真实而编译的)。调用方法(已经提到的ToolTask.Execute)检查的返回值是否ExecuteTool是0,如果是,则返回。你是否csc.exe是一个真正的编纂者还是列夫·托尔斯泰的《战争与和平》根本不重要。

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

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

我们会期待一个相反的顺序。为了解决这个问题,我们添加了编译器的假体。

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

嗯,对于这种情况也有一个方法:CSharpCommandLineParser.ParseconditionalCompilationSymbols从图书馆Microsoft.CodeAnalysis.csharp。它也通过调用字符串。拆分方法在多个分隔符上:

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

查看这组分隔符与Csc.GetDefineConstantsSwitch方法?在这里,空格不是分隔符。这意味着用空格分隔的条件编译符号不能用这种方法正确解析。

这就是我们检查问题项目时所发生的情况:它们使用了空格分隔的条件编译符号,因此被GetDefineConstantsSwitch方法,而不是ParseConditionalCompilationSymbols方法。

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

我想告诉你们一个我发现非常有趣的错误。

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

我们确实设法在Visual Studio(16.0.3)中复制了它。为此,您需要如下所示的类定义:

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

您还需要语法可视化器(它随。NET编译器平台SDK提供)。查找类型类型的语法树节点(通过单击“View TypeSymbol(如果有)”菜单项)ConstantPatternSyntax)。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.CodeAnalysis中的调试库生成它,则可以通过查找类型对应语法树节点的。

它最终会把我们带到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参数为,如此召唤destination.SpecialType投掷a的结果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版本号乱七八糟的笑话是一个老笑话,但它证明了你无法对未来产品的价值(无论是在名称还是版本中)做出任何可靠的预测。

我们通过为工具集添加优先级(即,将优先级作为一个单独的实体)很容易地解决了这个问题。

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

所有这些都使我们无法使用工具版本15.0因为系统看不到所需的工具集。最新的工具集,当前,将仍然可用,因为它是我们自己的工具集,因此,在Visual Studio2019中不存在这样的问题。解决方案非常简单,允许我们在不改变现有计算算法的情况下修复该问题:我们只需包含另一个工具集,15.0,添加到我们自己的工具集列表中当前

对C#.NET核心项目评估机制的更改

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

  • 添加“当前”工具集破坏了Visual Studio 2017中对。NET核心项目的分析。
  • 在没有安装至少一个Visual Studio副本的系统上,分析无法用于。NET核心项目。

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

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

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

在Visual Studio2017中评估C#.NET核心项目时,您会得到以下错误(对于当前工具集版本,当前):

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

由于这些问题是相似的(它们看起来确实是相似的),我们可以尝试一举两得。

在接下来的段落中,我将解释我们是如何实现这一目标的,而不进行详细说明。这些细节(关于如何评估C#.NET核心项目,以及对我们工具集中评估机制的更改)将成为我们未来一篇文章的主题。顺便说一下,如果您正在仔细阅读这篇文章,您很可能注意到这是我们以后文章的第二次引用。

我们是怎么解决这个问题的?我们用。NET Core SDK中的。targets/。props文件扩展了我们自己的工具集sdk.props,sdk.targets)。这使我们在导入管理和评估。NET核心项目方面获得了更多的控制和更大的灵活性。是的,我们的工具集又变得更大了,我们还必须添加逻辑来设置评估。NET核心项目所需的环境,但这似乎是值得的。

在此之前,我们通过简单地请求评估并依靠MSBuild来完成工作来评估。NET核心项目。

现在我们对局势有了更多的控制,机制发生了一些变化:

  • 设置评估。NET核心项目所需的环境。
  • 评价:
    • 使用工具集中的。targets/。props文件开始评估。
    • 使用外部文件继续评估。

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

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

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

现在我们有了首先导入的来自我们工具集的SDK文件,并且由于我们现在可以很容易地更改它们,所以我们完全控制其余的评估逻辑。这意味着我们现在可以决定从什么位置导入哪些文件。这同样适用于Microsoft.Common.props上面提到过。我们从工具集中导入这个和其他基本文件,所以我们不必担心它们的存在或内容。

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

结论

支持Visual Studio 2019通常比支持Visual Studio 2017更容易,原因有很多。首先,微软没有像从Visual Studio 2015更新到Visual Studio 2017时那样更改很多东西。是的,他们确实更改了基本工具集并强制Visual Studio插件切换到异步加载模式,但这种更改并不是那么剧烈。其次,我们已经有了一个现成的解决方案,包括我们自己的工具集和项目评估机制,我们不需要从头开始工作--只需要在我们已经有的基础上进行构建。通过扩展我们的项目评估系统,在新的条件下(以及在未安装Visual Studio副本的计算机上)支持分析。NET核心项目的过程相对轻松,这也给我们带来了希望,即通过掌握一些控制权,我们已经做出了正确的选择。

但我想重复上一篇文章中的观点:有时使用现成的解决方案并不像看起来那么容易。