语法树是编译器 API 公开的基本不可变数据结构。 这些树表示源代码的词法和语法结构。 它们有两个重要用途:
- 若要允许工具(如 IDE、加载项、代码分析工具和重构工具)查看和处理用户项目中源代码的语法结构。
- 若要使工具(如重构和 IDE)能够以自然方式创建、修改和重新排列源代码,而无需使用直接文本编辑。 通过创建和操作树结构,工具可以轻松创建和重新排列源代码。
语法树
语法树是用于编译、代码分析、绑定、重构、IDE 功能和代码生成的主要结构。 没有首先识别并分类为许多已知结构语言元素之一的源代码,就无法理解源代码的一部分。
注释
RoslynQuoter 是一种开源工具,它显示了用于构造程序语法树的语法工厂 API 调用。 若要实时试用,请参阅 http://roslynquoter.azurewebsites.net。
语法树具有三个关键属性:
- 它们完全保真地保存所有源信息。 完全保真意味着语法树包含源文本、每个语法构造、每个词法标记以及介于两者之间的所有其他信息,包括空格、注释和预处理器指令。 例如,源中提及的所有文本都完全按照键入的形式表示。 当程序不完整或格式不正确时,语法树还捕获源代码中的错误,方法是表示跳过或丢失的标记。
- 他们可以生成与经过解析得到的文本完全一致的文本。 从任何语法节点,都可以获取以该节点为根的子树的文本表示形式。 此功能意味着语法树可用作构造和编辑源文本的方法。 通过创建一棵树,实际上你已经创建了等效的文本;而通过对现有树进行更改来构建新树,相当于有效地编辑了文本。
- 它们是不可变的且线程安全的。 获取树后,它是代码当前状态的快照,永远不会更改。 这样,多个用户就可以在不同的线程中同时与同一语法树进行交互,而无需锁定或重复。 由于树是不可变的,因此无法直接对树进行任何修改,因此工厂方法通过创建树的其他快照来帮助创建和修改语法树。 树在重复使用基础节点的方式非常高效,因此可以快速重新生成新版本,并且只需要很少的额外内存。
字面上,语法树是一种树形数据结构,其中非终结结构元素作为父节点连接其他元素。 每个语法树由节点、令牌和琐事组成。
语法节点
语法节点是语法树的主要元素之一。 这些节点表示语法构造,例如声明、语句、子句和表达式。 每个语法节点类别由派生自 Microsoft.CodeAnalysis.SyntaxNode的单独类表示。 节点类集不可扩展。
所有语法节点都是语法树中的非终端节点,这意味着它们始终具有其他节点和令牌作为子节点。 作为另一个节点的子节点,每个节点都有一个可以通过属性访问的 SyntaxNode.Parent 父节点。 节点和树是不可变的,因此节点的父节点永远不会改变。 树的根以 null 为父级。
每个节点都有一种方法 SyntaxNode.ChildNodes() ,该方法根据子节点在源文本中的位置按顺序返回子节点的列表。 此列表不包含令牌。 每个节点还具有检查后代的方法,例如 DescendantNodes、DescendantTokens 或 DescendantTrivia,这些代表根节点的子树中存在的所有节点、令牌或杂项的列表。
此外,每个语法节点子类通过强类型属性公开所有相同的子级。 例如, BinaryExpressionSyntax 节点类具有特定于二进制运算符的三个附加属性: Left、 OperatorToken和 Right。 Left的类型为Right,ExpressionSyntax的类型为Right,而SyntaxToken的类型为。
某些语法节点具有可选的子节点。 例如,IfStatementSyntax 具有可选的 ElseClauseSyntax。 如果没有子级,则该属性返回 null。
语法标记
语法标记是语言语法的终端,表示代码的最小语法片段。 它们从不作为其他节点或标记的父级。 语法标记由关键字、标识符、文本和标点符号组成。
为了提高效率,该 SyntaxToken 类型是 CLR 值类型。 因此,与语法节点不同,所有类型的标记共用一种结构。不同类型的标记根据其含义,可能体现不同的属性。
例如,整数文本标记表示数值。 除了原始源文本和标记范围外,文本标记还包含 Value 属性,用于告知精确解码的整数值。 此属性的类型化为 Object 因为它可能是许多基元类型之一。
ValueText 属性告诉你的信息与 Value 属性相同;不过,此属性的类型始终为 String。 C# 源文本中的标识符可能包含 Unicode 转义字符,但转义序列本身的语法并不被视为标识符名称的一部分。 因此,虽然标记跨越的原始文本包含转义序列,但 ValueText 属性却不包含转义序列。 而是包括转义识别的 Unicode 字符。 例如,如果源文本包含写为 \u03C0
的标识符,则此令牌的 ValueText 属性将返回 π
。
语法趣闻
源代码中那些对代码正常理解基本无关紧要的部分被称为语法琐碎,例如空格、注释和预处理器指令。 与语法标记类似,琐碎内容为值类型。 单一Microsoft.CodeAnalysis.SyntaxTrivia类型用于描述各种琐碎的事情。
由于无关紧要的细节不是语言的正常语法结构的一部分,并且可以在任意两个标记之间出现,因此它们不会被包括在语法树中,成为节点的子级。 然而,由于它们在实现重构等功能时非常重要,并且为了保持对源文本的完全保真度,因此它们确实作为语法树的一部分存在。
可通过检查标记的 SyntaxToken.LeadingTrivia 或 SyntaxToken.TrailingTrivia 集合来访问琐碎内容。 分析源文本时,琐碎内容序列与标记关联。 通常情况下,一个标记拥有其后位于同一行中下一个标记之前的任意琐碎内容。 该行之后的任意琐碎内容与下一个标记关联。 源文件中的第一个标记会获取所有初始的零碎信息,而文件中最后一个零碎信息序列会附加到文件末尾标记上,否则该标记的宽度为零。
与语法节点和语法标记不同,语法琐碎内容没有父级。 然而,由于它们是树的一部分,并且每一个都与单个令牌相关联,因此您可以使用 SyntaxTrivia.Token 属性访问其关联的令牌。
跨度
每个节点、令牌或琐碎内容都知道其在源文本中的位置及其包含的字符数。 文本位置表示为 32 位整数,它是从 char
零开始的索引。 对象 TextSpan 是起始位置和字符计数,两者都表示为整数。 如果 TextSpan 长度为零,则表示两个字符之间的位置。
每个节点都有两个 TextSpan 属性: Span 和 FullSpan。
该 Span 属性是从节点子树中第一个标记的开头到最后一个标记末尾的文本范围。 此范围不包括任何前导或尾随琐碎内容。
FullSpan 属性表示的文本范围包括节点的正常范围,加上任何前导或尾随琐碎内容的范围。
例如:
if (x > 3)
{
|| // this is bad
|throw new Exception("Not right.");| // better exception?||
}
块内语句节点的范围由单个竖线 (|) 指示。 它包括字符 throw new Exception("Not right.");
。 整个跨度由双竖线 (||) 指示。 它包含的字符与前导和尾随琐碎内容的相关范围和字符相同。
种类
每个节点、令牌或琐碎项都有一个类型为 SyntaxNode.RawKind 的 System.Int32 属性,以标识所表示的确切语法元素。 此值可强制转换为特定语言的枚举。 每个语言 C# 或 Visual Basic 都有一 SyntaxKind
个枚举(Microsoft.CodeAnalysis.CSharp.SyntaxKind 以及 Microsoft.CodeAnalysis.VisualBasic.SyntaxKind分别列出语法中所有可能的节点、标记和琐碎元素)。 可以通过访问 CSharpExtensions.Kind 或 VisualBasicExtensions.Kind 扩展方法自动完成此转换。
该 RawKind 属性允许轻松区分共享同一节点类的语法节点类型。 对于令牌和琐事,此属性是区分一种元素类型与另一种元素的唯一方法。
例如,单个BinaryExpressionSyntax类具有Left和OperatorTokenRight子级。 该Kind属性用于区分语法节点的类型,即它是AddExpression、SubtractExpression或MultiplyExpression类型的语法节点。
错误
即使源文本包含语法错误,也会展示一个能够回溯到源文本的完整语法树。 当分析器遇到不符合语言定义的语法的代码时,它使用两种技术之一来创建语法树:
如果解析器期待特定类型的标记但找不到,它可能会在预期标记出现的位置将缺失的标记插入到语法树中。 缺少的令牌表示预期的实际令牌,但它的跨度为空,并且其 SyntaxNode.IsMissing 属性返回
true
。解析器可能会跳过标记,直到找到可以继续解析的标记。 在这种情况下,跳过的令牌附加为 SkippedTokensTrivia 类型的琐碎内容节点。