欢迎回来!如果您错过了第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
,但它没有帮助,因为实际上它没有保护任何东西--它只是允许您在库的调试版本中发现问题。或者不是。
这一部分没有太多有趣之处:现有算法不需要任何值得一提的大修改,但是您可能想知道两个小问题。
第一个是我们必须修改依赖于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,添加到我们自己的工具集列表中当前。
这项任务涉及两个相互关联的问题:
这两个问题都来自同一个来源:一些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核心项目。
现在我们对局势有了更多的控制,机制发生了一些变化:
这个顺序表明设置环境追求两个主要目标:
一个特殊的库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核心项目的过程相对轻松,这也给我们带来了希望,即通过掌握一些控制权,我们已经做出了正确的选择。
但我想重复上一篇文章中的观点:有时使用现成的解决方案并不像看起来那么容易。