在 CodeDOM 中解析符号引用(第 7 部分)






4.75/5 (6投票s)
解析 CodeDOM 中的符号引用。
介绍
本文介绍如何在 CodeDOM 中解析符号引用。包含源代码。这是关于 CodeDOM 系列的第 7 部分,但对于任何希望解析 C# 源代码中的符号引用的人都可能有用。在前面的部分中,我已讨论了“CodeDOM”,提供了 C# CodeDOM、WPF UI 和 IDE、C# 解析器、解决方案/项目 CodeDOM 类,并涵盖了加载类型元数据。
解析 CodeDOM 树
在本系列的前几篇文章中,我构建了一个 C# CodeDOM,它可以从现有的 C# 源文件解析自身,生成一个代码对象树,其中大多数符号引用未解析,并由 UnresolvedRef
对象表示。在上一篇文章中,我添加了对从引用的程序集中加载类型元数据的支持,现在是时候添加必要的逻辑来使用下表中显示的类替换那些 UnresolvedRef
对象,并建立具体的引用了。
CodeDOM 类 | 目的 |
SymbolicRef
|
所有符号引用的基类。 |
TypeRefBase
|
所有类型引用的基类(支持泛型参数、数组)。 |
UnresolvedRef
|
未解析的引用。 |
UnresolvedThisRef
|
显式接口实现索引器的未解析的“this”。 |
TypeRef
|
引用类型声明。 |
AliasRef
|
引用别名(指向类型或命名空间)。 |
TypeParameterRef
|
引用类型参数。 |
MethodRef
|
引用方法。 |
AnonymousMethodRef
|
引用匿名方法。 |
ConstructorRef
|
引用构造函数。 |
OperatorRef
|
引用运算符声明。 |
GotoTargetRef
|
“goto”目标引用的基类。 |
LabelRef
|
引用标签。 |
SwitchItemRef
|
引用 switch 语句项(case 或 default)。 |
SelfRef
|
当前对象实例引用的基类。 |
BaseRef
|
引用当前对象实例的基类。 |
ThisRef
|
引用当前对象实例。 |
VariableRef
|
所有变量引用的基类。 |
PropertyRef
|
引用属性。 |
IndexerRef
|
引用索引器。 |
EventRef
|
引用事件。 |
EnumMemberRef
|
引用枚举成员。 |
FieldRef
|
引用字段。 |
LocalRef
|
引用局部变量。 |
ParameterRef
|
引用参数。 |
ExternAliasRef
|
引用外部别名。 |
NamespaceRef
|
引用命名空间。 |
DirectiveSymbolRef
|
引用预处理器指令符号。 |
我们需要从上到下遍历 CodeDOM 树,查找 UnresolvedRef
对象,然后尝试将每个对象解析为指向正确声明的适当的特定引用对象,遵循 C# 语言规范规定的各种作用域和解析规则。在考虑如何解析引用时,一个明显的问题是,引用的类型通常可以根据上下文确定。例如,`using` 指令之后的引用应代表命名空间,变量类型和方法返回类型应代表类型(带可选的命名空间前缀)等。枚举 ResolveCategory
代表基于当前上下文的可能的引用类型类别。
CodeObject
基类上的虚拟方法 “CodeObject Resolve(ResolveCategory resolveCategory, ResolveFlags flags)
” 用于执行树的自顶向下遍历,并由所有代码对象根据需要进行重写,以执行任何特殊逻辑并解析所有子对象。对于 UnresolvedRef
对象,重写版本会尝试解析它们,并在成功时返回适当的新引用对象,父对象将结果分配给它们的子属性。ResolveFlags
枚举参数用于表示特殊模式,例如下面的 3 个阶段、在文档注释内进行解析以及“取消解析”模式(用于在必要时将已解析的引用转换回 UnresolvedRef
)。
对于 Solution
、Project
和 CodeUnit
对象,Resolve()
重写版本分 3 个阶段执行:
- 阶段 1 解析所有类型——
CodeUnit
和NamespaceDecl
中的所有语句,以及TypeDecl
的头部(包括任何基列表),在类型体处停止。 - 阶段 2 解析所有类型成员——方法、属性、字段的定义,在方法体、属性或字段初始化器处停止。
- 阶段 3 解析所有代码——方法体、属性和字段初始化器。
请注意,这不是 3 次完整的遍历,而是更像广度优先而不是深度优先的树遍历,它允许在单次遍历中解析所有引用,而不会出现求值顺序依赖问题(另一个特殊情况是 Switch
需要在处理所有 `Case` 表达式后再处理所有语句体,以便处理可能的“goto case …
”的向前引用)。
解析 UnresolvedRef
UnresolvedRef
的 Resolve()
重写首先调用其类型参数子对象的 Resolve()
,然后它尝试通过创建 Resolver
类的实例、将其自身传递给构造函数并调用其 Resolve()
来解析自身。Resolver
类根据 ResolveCategory
进行操作,在查找特定引用类型时与在表达式中查找引用(可能为任何类型)时的行为不同。对可能匹配对象的类型的验证以及任何错误消息的文本也基于类别。
Resolver
类包含各种特殊情况逻辑,但主要功能包括调用 ResolveRef()
在当前作用域中查找名称匹配的声明,如果未找到,则调用 ResolveRefUp()
在树的更高层继续搜索,直到找到为止。根据解析类别,它可能会在到达树顶之前停止,如果这样做有意义的话。
当找到名称匹配的声明时,会在 Resolver
实例上调用 AddMatch()
方法,该方法会创建一个 MatchCandidate
实例,然后验证匹配对象的类型对于该解析类别是否有效。如果匹配的是方法,则必须尝试推断泛型方法的任何缺失类型参数,并经过大量复杂的重载逻辑来确定参数类型是否与提供的参数类型匹配。还会有检查以验证候选对象是静态的还是非静态的(根据需要),以及访问说明符是否允许在当前作用域中访问它。它会确定候选对象是“完整”匹配还是仅部分匹配,这反过来又决定了搜索是否会继续到其他更高的作用域。
如果此过程找到单个有效匹配,Resolver.Resolve()
会通过调用其 CreateRef()
创建一个指向已匹配声明的新引用并返回它,这会导致 UnresolvedRef
对象被替换。如果没有找到匹配项,或者找到多个匹配项,则会生成适当的错误消息并将其附加到 UnresolvedRef
对象(这些消息会传播到 Solution
级别并记录到控制台或显示在 UI 中)。
表达式类型求值
为了确定重载方法的正确匹配,有必要对参数表达式的类型进行求值,以查看它们是否与参数类型匹配。一个名为“TypeRefBase EvaluateType()
”的虚拟方法已添加到 Expression
中,并由子类根据需要重写以求值它们的类型。此外,一个名为“TypeRefBase EvaluateTypeArgumentTypes()
”的虚拟方法已添加到 TypeRefBase
中,用于求值类型或方法引用上的任何泛型类型参数的类型。Resolver
上的 AddMatch()
方法使用这些来求值传递给方法的每个参数表达式的类型,然后调用 ParameterRef.MatchParameter()
来确定类型是否与参数类型匹配(内部调用 EvaluateParameter()
)。
其他各种对类型求值过程必不可少的方法包括 TypeRef
类中的以下成员:FindTypeArgument()
用于求值类型参数,IsImplicitlyConvertibleTo()
和 FindUserDefinedImplicitConversion()
用于处理隐式转换,以及 GetCommonType()
用于确定可以表示给定两种类型的通用类型。
方法组
有时,重载方法的名称会单独使用,没有括号或参数。这被称为“方法组”,通常分配给委托类型的变量或传递给委托类型的参数。这种方法组由 UnresolvedRef
类表示,在这种情况下,它将有多个匹配的候选对象。然后,方法组通常会解析为单个方法引用,通过其分配(或传递)的委托类型来确定参数类型,从而确定单个匹配方法。
生成的代码
在某些情况下,C# 源文件会在编译时生成,其中包含必须由编译器与“代码隐藏”文件合并的局部类。这些文件的扩展名如“`.Designer.cs`”或“`.g.cs`”,可能位于输出目录中作为临时文件。既然我们正在解析符号引用,我们也需要加载和处理这些生成的代码,否则将有很多符号无法解析。这通过 Project
类中的逻辑来实现,该逻辑可以检测并包含这些文件到项目中。还添加了忽略此类文件的自动代码清理和保存尝试的逻辑。*Nova 尚未自动生成这些缺失的文件(像 VS 那样),因此如果一个项目尚未为特定配置构建,生成的代码文件将显示为项目缺失,并为引用其中的声明的符号引用生成解析错误。*
文档注释内的代码
文档注释内的代码——在 <code>
或 <c>
标签内,或 'cref'
属性中——默认会自动解析。但是,在此类代码中发生的任何解析或解析错误都将被视为仅警告。可以通过将 DocCode.ParseContentAsCode
选项设置为 false 来关闭 <code>
标签内内容的解析,如果未解析,则无内容可供解析。
Nova Studio 改进
Nova Studio 现在在加载解决方案或项目时会自动解析所有符号引用。因此,缺失的引用或其他此类问题现在可能导致大量错误消息。已向上下文菜单添加了“转到声明”选项,以导航到符号引用的目标。此外,任何评估为常量值的表达式现在在其工具提示中显示常量值。下面的屏幕截图显示了引用已被解析。
性能
Nova Studio IDE 现在具有与 VS IDE 类似的功能,可以加载解决方案、项目、文件和引用的程序集到内存中,以及解析所有源并显示错误消息。出于好奇,这里有一个性能比较。
解决方案 | 项目 | 文件 | 代码对象 | 加载(秒) | 内存(MB) | ||
Nova | VS2010 | Nova | VS2010 | ||||
Nova | 10 | 820 | 439,649 | 5 | 22 | 201 | 424 |
SubText 2.5.2 | 7 | 849 | 277,163 | 4 | 26 | 218 | 410 |
Mono Tests | 1,939 | 1,945 | 157,634 | 9 |
4,000+
|
329 |
3,000+
|
MS EntLib Tests | 70 | 2,445 | 866,065 | 10 | 43 | 338 | 583 |
大型专有项目 | 43 | 4,677 | 1,829,942 | 19 | 72 | 426 | 615 |
SharpDevelop 4.2 | 93 | 5,289 | 2,556,585 | 23 | 76 | 485 | 631 |
所示数字为近似值,使用任务管理器检查加载时的峰值工作集,以及 UI 响应大致灵敏的时间(CPU 使用率低于 15%)。Nova 尚未优化,尤其是解析部分,但其性能似乎比 VS 快 3-4 倍。多年来我注意到 VS(或者可能是它使用的 MSBuild)似乎为每个项目执行一些耗时半秒到一秒的操作,这使得加载大量项目非常缓慢。尽管多年来有所改进,但如果你有几十个项目,加载速度一直很慢,更不用说上百个项目了。Mono 的一个包含近 2000 个测试项目的解决方案的测试案例简直可笑——我等了一个多小时,工作集超过 3 GB 后就放弃了!这似乎表明性能与项目数量呈指数关系。Nova 在不到 10 秒的时间内完成了所有需要的工作。我知道这不是典型的用例,但这仍然非常令人 sad……因为它可能并不难修复,如果他们这样做了,VS 在加载更典型的解决方案时会更加敏捷。我认为数百万用户只需花费一半的时间加载解决方案,就值几个月的人力开发工作来清理加载过程。嘿,微软,有人在听吗?
使用附加源代码
已添加一个新的 Resolving
文件夹,其中包含与解析支持相关的新类:Resolver
和 MatchCandidate
(以及 MatchCandidates
集合)。它还包含 ResolveCategory
和 ResolveFlags
枚举。已将各种解析相关的方法——如 Resolve()
、ResolveRef()
、ResolveRefUp()
等——添加到大多数现有的 CodeDOM 类中,并将它们分隔在具有“RESOLVING”注释的区域中。Nova.Examples 项目已添加了新的示例,包括演示 LoadOptions.DoNotResolve
标志,以便在不需要时加载解决方案/项目而不进行解析。Nova Studio 现在默认进行解析,并且所有红色的未解析引用都应该消失(除了 C# 2.0 之后的特性,我已经实现但尚未开源)。使用它加载提供的源代码(“Nova.sln”)并检查源文件以查看。像往常一样,提供了一个单独的 ZIP 文件,其中包含二进制文件,以便您在不构建它们的情况下运行它们。
摘要
我的 CodeDOM 现在支持加载、解析和解析 C# 解决方案和项目。Nova Studio 开始看起来像一个真正的 IDE 了!现在既然整个解决方案都可以加载和解析,是时候添加一些基本的代码分析功能了。到目前为止可能的一切都很有趣,但现在是时候真正做些有用的事情了!在下一篇文章中,我将探讨计算 CodeDOM 的度量以及在其上进行各种类型的搜索。