TypeScript 源码详细解读(4)语法1-语法树
在上一节介绍了标记的解析,就相当于识别了一句话里有哪些词语,接下来就是把这些词语组成完整的句子,即拼装标记为语法树。
树(tree)
树是计算机数据结构里的专业术语。就像一个学校有很多年级,每个年级下面有很多班,每个班级下面有很多学生,这种组织结构就叫树。
- 组成树的每个部分称为节点(Node);
- 最顶层的节点(即例子中的学校)称为根节点(Root Node);
- 和每个节点的下级节点称为这个节点的子节点(Child Node,注意不叫 Subnode)(班级是年级的子节点);
- 反过来,每个节点的上级节点称为这个节点的父节点(Parent node)(年级是班级的父节点);
- 一个节点的子节点以及子节点的子节点统称为这个节点的后代节点(Descendant node);
- 一个节点的父节点以及父节点的父节点统称为这个节点的祖父节点(Ancestor node)。
很多人一提到树就想起二叉树,说明你压根不懂什么是树。二叉树只是树的一种。二叉树被用的最多的地方在试卷,请忘掉这个词。
从树中的任一个节点开始,都可以遍历这个节点的所有后代节点。因为节点不会出现循环关系,所以遍历树也不会出现死循环。
遍历节点的顺序有有很多,没特别说明的话,是按照先父节点、再子节点,同级节点则从左到右的顺序(图中编号顺序)。
语法树(Syntax Tree)
语法树用于表示解析之后的代码结构的一种树。
比如以下代码解析后的语法树如图:
var x = ['l', [100]]
if (x) {
foo(x)
}
其中,源文件(Source File)是语法树的根节点。
语法树中有很多种类的节点,根据种类的不同,这些节点的子节点种类也会变化。比如:
- “if 语句”节点,只有“条件表达式”、“则部分”和“否则部分”(可能为空)三个子节点。
- “双目表达式(x + y)”节点,只有“左值表达式”和“右值表达式”两个子节点。
TypeScript 中,节点有约 100 种,它们都继承 “Node” 接口:
export interface Node extends TextRange {
kind: SyntaxKind;
flags: NodeFlags;
parent: Node;
// ...(略)
}
Node 接口中 kind 枚举用于标记这个节点的种类。
TypeScript 将表示标记种类的枚举和表示节点种类的枚举合并成一个了(这可能也是导致很多人读不懂代码的原因之一):
export const enum SyntaxKind {
// ...(略)
TemplateSpan,
SemicolonClassElement,
// Element
Block,
EmptyStatement,
VariableStatement,
ExpressionStatement,
IfStatement,
DoStatement,
WhileStatement,
ForStatement,
ForInStatement,
ForOfStatement,
ContinueStatement,
BreakStatement,
// ...(略)
}
如果你深知“学而不思则罔”的道理,现在应该会思考这样一个问题:那到底有哪 100 种语法节点呢?
这里先推荐一个工具:https://astexplorer.net/
这个工具可以在左侧输入代码,右侧查看实时生成的语法树(以 JSON 方式展示)。读者可以在这个工具顶部选择“JavaScript”语言和“typescript”编译器,查看 TypeScript 生成的语法树结构。
为了帮助英文文盲们更好地理解语法类型,读者可参考:https://github.com/meriyah/meriyah/wiki/ESTree-Node-Types-Table
语法节点分类
虽然语法节点种类很多,但其实只有四类:
- 类型节点(Type Node):一般出现在“:”后面(var a: 类型节点),可以解析为一个类型。
- 表达式节点(Expression):可以计算得到一个值的节点,表达式节点只能依附于一个语句节点,不能独立使用。
- 语句节点(Statement):可以直接在最外层使用的节点,俗称的几行代码就是指几个语句节点。
- 其它节点:其它内嵌在表达式或语句节点的特定节点,比如 case 节点。
在 TypeScript 中,节点命名比较规范,一般类型节点以 TypeNode 结尾;表达式节点以 Expression 结尾;语句节点以 Statement 结尾。
比如 if 语句节点:
export interface IfStatement extends Statement {
kind: SyntaxKind.IfStatement;
expression: Expression;
thenStatement: Statement;
elseStatement?: Statement;
}
鉴于有些读者对部分语法比较陌生,这里可以说明一些可能未正确理解的节点类型
表达式语句(ExpressionStatement)
export interface ExpressionStatement extends Statement, JSDocContainer {
kind: SyntaxKind.ExpressionStatement;
expression: Expression;
}
表达式是不能直接出现在最外层的,但以下代码是允许的:
var x = 1;
1 + 1; // 这是表达式
因为 1 + 1 是表达式,它们同时又是一个表达式语句。所以以上代码的语法树如图:
常见的赋值、函数调用语句都其实是一个表达式语句。
块语句(BlockStatement)
一对“{}”本身也是一个语句,称为块语句。一个块语句可以包含若干个语句。
export interface Block extends Statement {
kind: SyntaxKind.Block;
statements: NodeArray<Statement>;
/*@internal*/ multiLine?: boolean;
}
比如 while 语句的主体只能是一条语句:
export interface IterationStatement extends Statement {
statement: Statement;
}
export interface WhileStatement extends IterationStatement {
kind: SyntaxKind.WhileStatement;
expression: Expression;
}
但 while 里面明明是可以写很多语句的:
while(x_d) {
var a = 120;
var b = 100;
}
本质上,当我们使用 {} 时,就已经使用了一个块语句,while 的主体仍然是一个语句:块语句。其它语句都是块语句的子节点。
标签语句(LabeledStatement)
export interface LabeledStatement extends Statement, JSDocContainer {
kind: SyntaxKind.LabeledStatement;
label: Identifier;
statement: Statement;
}
通过标签语句可以为语句命名,比如:
label: var x = 120;
命名后有啥用?可以在 break 或 continue 中引用该名称,以此实现跨级 break 和 continue 的效果:
export interface BreakStatement extends Statement {
kind: SyntaxKind.BreakStatement;
label?: Identifier; // 跳转的标签名
} export interface ContinueStatement extends Statement {
kind: SyntaxKind.ContinueStatement;
label?: Identifier; // 跳转的标签名
}
运算符的优先级
比如 x + y * z 中,需要先算乘号。生成的语法树节点如下:
通过节点的层次关系,实现了这种优先级的效果(因为永远不会把图里的 x 和 y 先处理)。
因此创建语法树的同时,也就处理了优先级的问题,括号完全可以从语法树中删除。
类
一个复杂的类,也能用语法树表示?
当然,任何语法最后都是用语法树表达的,只不过类确实复杂一些:
export interface Declaration extends Node {
_declarationBrand: any;
} export interface NamedDeclaration extends Declaration {
name?: DeclarationName;
} export interface ClassLikeDeclarationBase extends NamedDeclaration, JSDocContainer {
kind: SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression;
name?: Identifier;
typeParameters?: NodeArray<TypeParameterDeclaration>;
heritageClauses?: NodeArray<HeritageClause>;
members: NodeArray<ClassElement>;
} export interface ClassDeclaration extends ClassLikeDeclarationBase, DeclarationStatement {
kind: SyntaxKind.ClassDeclaration;
/** May be undefined in `export default class { ... }`. */
name?: Identifier;
}
类、函数、变量、导入声明严格意义上是独立的一种语法分类,但鉴于它和其它语句用法一致,为了便于理解,这里把声明作语句的一种看待。
节点位置
当源代码被解析成语法树后,源代码就不再需要了。如果后续流程发现一个错误,编译器需要向用户报告,并指出错误位置。
为了可以得到这个位置,需要将节点在源文件种的位置保存下来:
export interface TextRange {
pos: number;
end: number;
}
export interface Node extends TextRange {
kind: SyntaxKind;
flags: NodeFlags;
parent: Node;
// ...(略)
}
通过节点的 parent 可以找到节点的根节点,即所在的文件;通过节点的 pos 和 end 可以确定节点在源文件的行列号(具体已经在第二节:标记位置 中介绍)。
遍历节点
为了方便程序中遍历任意节点,TypeScript 提供了一个工具函数:
/**
* Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes
* stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise,
* embedded arrays are flattened and the 'cbNode' callback is invoked for each element. If a callback returns
* a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned.
*
* @param node a given node to visit its children
* @param cbNode a callback to be invoked for all child nodes
* @param cbNodes a callback to be invoked for embedded array
*
* @remarks `forEachChild` must visit the children of a node in the order
* that they appear in the source code. The language service depends on this property to locate nodes by position.
*/
export function forEachChild<T>(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
if (!node || node.kind <= SyntaxKind.LastToken) {
return;
}
switch (node.kind) {
case SyntaxKind.QualifiedName:
return visitNode(cbNode, (<QualifiedName>node).left) ||
visitNode(cbNode, (<QualifiedName>node).right);
case SyntaxKind.TypeParameter:
return visitNode(cbNode, (<TypeParameterDeclaration>node).name) ||
visitNode(cbNode, (<TypeParameterDeclaration>node).constraint) ||
visitNode(cbNode, (<TypeParameterDeclaration>node).default) ||
visitNode(cbNode, (<TypeParameterDeclaration>node).expression);
case SyntaxKind.ShorthandPropertyAssignment:
return visitNodes(cbNode, cbNodes, node.decorators) ||
visitNodes(cbNode, cbNodes, node.modifiers) ||
visitNode(cbNode, (<ShorthandPropertyAssignment>node).name) ||
visitNode(cbNode, (<ShorthandPropertyAssignment>node).questionToken) ||
// ...(略)
}
}
forEachChild 函数只会遍历节点的直接子节点,如果用户需要递归遍历所有子节点,需要递归调用 forEachChild。forEachChild 接收一个函数用于遍历,并允许用户返回一个 true 型的值并终止循环。
小结
掌握语法树是掌握整个编译系统的基础。你应该可以深刻地知道语法树的大概样子,并清楚每种语法的语法树结构。如果还没有彻底掌握,可以使用上文推荐的工具。
下一节将介绍生成语法树的全过程。#更新于 2020-2-8 #
TypeScript 源码详细解读(4)语法1-语法树的更多相关文章
- TypeScript 源码详细解读(1)总览
TypeScript 由微软在 2012 年 10 月首发,经过几年的发展,已经成为国内外很多前端团队的首选编程语言.前端三大框架中的 Angular 和 Vue 3 也都改用了 TypeScript ...
- TypeScript 源码详细解读(3)词法2-标记解析
在上一节主要介绍了单个字符的处理,现在我们已经有了对单个字符分析的能力,比如: 判断字符是否是换行符:isLineBreak 判断字符是否是空格:isWhiteSpaceSingleLine 判断字符 ...
- AQS源码详细解读
AQS源码详细解读 目录 AQS源码详细解读 基础 CAS相关知识 通过标识位进行线程挂起的并发编程范式 MPSC队列的实现技巧 代码讲解 独占模式 独占模式下请求资源 独占模式下的释放资源 共享模式 ...
- TS 原理详细解读(5)语法2-语法解析
在上一节介绍了语法树的结构,本节则介绍如何解析标记组成语法树. 对应的源码位于 src/compiler/parser.ts. 入口函数 要解析一份源码,输入当然是源码内容(字符串),同时还提供路径( ...
- Thrift之代码生成器Compiler原理及源码详细解析1
我的新浪微博:http://weibo.com/freshairbrucewoo. 欢迎大家相互交流,共同提高技术. 又很久没有写博客了,最近忙着研究GlusterFS,本来周末打算写几篇博客的,但是 ...
- 基于LNMP的Zabbbix之Zabbix Agent源码详细安装,但不给图
基于LNMP的Zabbbix之Zabbix Server源码详细安装:http://www.cnblogs.com/losbyday/p/5828547.html wget http://jaist. ...
- LinkedHashMap 源码详细分析(JDK1.8)
1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题.除此之外,Linke ...
- SpringMVC+Maven开发项目源码详细介绍
代码地址如下:http://www.demodashi.com/demo/11638.html Spring MVC概述 Spring MVC框架是一个开源的Java平台,为开发强大的基于Java的W ...
- 一文读懂Spring动态配置多数据源---源码详细分析
Spring动态多数据源源码分析及解读 一.为什么要研究Spring动态多数据源 期初,最开始的原因是:想将答题服务中发送主观题答题数据给批改中间件这块抽象出来, 但这块主要使用的是mq消息的方式 ...
随机推荐
- ASP.NET MVC 实现页落网资源分享网站+充值管理+后台管理(6)之配置文件设置
现在该有的结构和层级都有了,下面我们就开始实际应用,首先把需要用的js,css,图片放到Content文件夹中. 这里不详细讲解,大家可根据自己的实际情况,使用合适自己的前端框架,也可以点击下载本项目 ...
- H3C擦除配置
- Javascript 防扒站,防止镜像网站
自己没日没夜敲出来的站,稍微漂亮一点,被人看上了就难逃一扒,扒站是难免的,但不能让他轻轻松松就扒了: 前些天有个朋友做的官网被某不法网站镜像,严重影响到 SEO,当时的解决方法是通过屏蔽目标 IP 来 ...
- layui框架实现多图片手动上传和随表单提交方法
首先在官方文档并没有手动上传的说明文档,这里手动实现上传原理是:在表单中有三个按钮,分别是上传图片按钮.隐藏上传按钮.表单提交按钮,点击上传图片按钮之后,图片添加在前端但是并没有真正的上传,而是在点击 ...
- The Preliminary Contest for ICPC Asia Shanghai 2019 C Triple(FFT+暴力)
The Preliminary Contest for ICPC Asia Shanghai 2019 C Triple(FFT+暴力) 传送门:https://nanti.jisuanke.com/ ...
- boostrap-非常好用但是容易让人忽略的地方【1】:modal
使用bootstrap框架好久了,在开发中也用到了或者遇到了很多的问题,所以跟大家分享一下 bootstrap modal 组件的样式 .modal-lg .modal-sm 说明:这个是bootst ...
- AbstactFactory模式
AbstractFactory模式就是用来解决这类问题的:要创建一组相关或者相互依赖的对象. AbstractFactory模式关键就是将这一组对象的创建封装到一个用于创建对象的类(ConcreteF ...
- 泛圈科技Yottachain区块链云存储打破传统云迎来价值数据存储
随着物联网时代的发展,更多的数据随之产生.从智能设备到电脑再到视频游戏机,各种各样的信息从不同的电子产品源源不断地涌入.通常,人们将数据存储在本地驱动器中.但是,由于产生的数据量是无限的,超过了本地存 ...
- Mac常用的软件推荐
Alfred 效率软件,让能更快的启动各种软件 VScode 编辑器,市面上最热的编辑器,好用的不只是一点点,加上Vim插件简直就是秒杀市面上各种IDE PicGo 一个开源图床软件,支持各大网站的图 ...
- 关于SAM和广义SAM
关于SAM和广义SAM 不是教程 某些思考先记下来 SAM 终于学会了这个东西诶...... 一部分重要性质 确定一个重要事情,S构造出的SAM的一个重要性质是当且仅当对于S的任意一个后缀,可以从1号 ...