type
status
date
slug
summary
tags
category
icon
password
fullWidth
fullWidth
前言
TypeScript提供了一套完整的编译器API,可见微软官方文档:Using the Compiler API · microsoft/TypeScript Wiki
模块 | 用途 |
ts.CompilerHost | 自定义编译器文件系统交互 |
ts.Program | 表示整个TypeScript项目 |
ts.SourceFile | 单个文件的AST表示 |
ts.TransformerFactory | 代码转换功能 |
ts.TypeChecker | 类型系统访问 |
允许开发者以编程方式分析、检查和生成代码,有以下用途:
- 静态代码分析:在运行前发现潜在问题,如API过时
- 自动代码转换:用于批量升级代码库
- 自定义检查规则:强制执行特定编码规范
用代码处理代码是一件比较抽象的工作,需要用机器视角阅读代码文本,借助可视化辅助工具AST Viewer帮助理解语法树,本文以实际业务场景为入口,给出自定义检查规则的实现方案和细节。
抽象语法树
抽象语法树(Abstract Syntax Tree, AST)是源代码语法结构的一种抽象表示,它以树状形式表现编程语言的语法结构。
TypeScript AST是TypeScript代码的树状结构表示:
- 去掉了源代码中的无关细节(如分号、括号等)
- 保留了代码的语法结构
- 每个节点(Node)表示源代码中的一个构造
这个网站可以查看TypeScript代码对应的语法树:TypeScript AST Viewer

更复杂的语法也可以解析出来:

节点类型
SyntaxKind
是TypeScript编译器API中的枚举类型,它定义了所有可能的语法节点类型;每个 AST节点都有一个kind
属性,其值是SyntaxKind
的枚举成员。以下是一些常见的
SyntaxKind
值的含义:- CallExpression
- AsExpression
- Identifier
- StringLiteral
- ObjectLiteralExpression
- PropertyAccessExpression
- VariableDeclaration
- PropertyAssignment
- TrueKeyword & FalseKeyword
- TypeReference
- SpreadAssignment
自定义检查实现
以一个具体的业务需求为例,说明自定义检查的实现思路:用接口作为泛型构造Schema,检查Schema中的
Fields
属性中的各子Scheme的Optional
字段是否和接口中的可选性一致例如,
positionRollbackConfigScheme
是用接口IPositionRollbackConfig
声明的Schema:- 接口中
Range?
和Position?
是可选字段
- Schema的
Fields
中的Range
和Position
对应的Schema中Optional
必须为true
以下代码是符合规则的示例:
构建工程
从检查的入口构建TypeScript工程:
- 基于项目
tsconfig.json
配置初始化工程
- 通配符匹配
interfaceDir
和schemeDir
所有.ts
文件加入分析范围
- 用
SourceFile.getInterfaces()
得到所有接口声明并处理
- 处理Action、Component、Var开头的子目录下的所有Schema文件
- 通过
getVariableDeclarations()
得到所有变量声明 - 调用
scanObjectSchemeNode()
解析每个变量声明节点
- 在检查之前调用
ResolveAllUnresolved()
处理Schema依赖关系
- 最后调用
CheckAllIsValid()
验证所有Schema的必填字段和类型约束
处理接口
处理接口的重点是找到接口的继承关系,可以用AST Viewer进行查看一些复杂接口的语法结构:

对于接口对象的定义如下:
遍历接口对象
- 遍历每个接口声明
- 提取接口基本信息(名称、属性、可选标记)
- 记录继承关系并标记需要后续解决的接口
- 最后统一解决所有继承关系
递归合并父接口属性
SolveUnsolved()
深度优先遍历继承树
mergeParentProperties()
自底向上合并属性
- 最后要将
HeritageList
设置成undefined
- 避免循环继承造成无限递归
- 避免菱形继承造成重复合并的开销
处理Schema
遍历Schema对象
已知项目中所有的Schema对象都是通过函数调用来创建的,有两种方式:
- 调用函数创建对象
- 调用函数后类型转换
在抽象语法树中,如果调用了函数之后进行类型转换,
CallExpression
会被AsExpression
包裹,作为子节点而存在:
因此需要特殊处理,如果获取不到
CallExpression
,尝试获取AsExpression
的Child:如果识别到为函数表达式,则要继续通过
resolveObjectSchemeCall()
解析:- 解析调用泛型函数时的接口名称
interfaceName
,只识别I开头的接口作为Schema的关联接口 - 比如,
ITrackLocations
是locationsScheme
的关联接口 - 比如,虽然
buffIdScheme
在创建时用了<number>
作为接口类型,但这里不认为它是接口,需要设置成undefined
- 比如,以下Schema没有关联接口,得到的接口名是
undefined
- 得到作为对象字面量的参数
objectArgument
- 遍历每个参数(
objectArgument
的子节点)解析各属性
- 最后将生成的
SchemeInfo
加入缓存(可能会有多个Schema同名,因此需要用Map<string, SchemeInfo[]>
存储)
- 对于
Fields
属性对应的节点,要进一步解析
解析
Fields
属性对应的节点,从属性中读取每个子字段的可选性,生成ObjectSchemeState
:- 尝试寻找函数调用节点(同样地,对于函数转型节点要提取出它的子节点函数调用节点)
- 找到,说明是以下属性赋值形式
- 得到所有属性赋值节点,尝试寻找
Optional
属性 - 存在,提取
Optional
的布尔值,如果为true
则加入到optionalKeys
- 不存在,如果里面有展开表达式,则解析展开表达式(内部也是加入
unResolvedMap
) - 未找到,说明是以下属性赋值形式
- 生成segment加入
unResolvedMap
resolveIdentifierAssignment()
和resolveSpreadAssignment()
处理对象属性直接引用其他Schema的语法(如field: otherScheme
和field: createScheme({...otherScheme})
),提取被引用Schema的定义路径,并记录依赖关系到unResolvedMap
最后,使用
Add()
将生成的ObjectSchemeState
添加到缓存中:- 添加拥有未处理字段的Schema的
Key
到集合
- 添加所有Schema的
Key
和ObjectSchemeState
到映射
未处理字段
注意到前面处理了未处理字段和对应Schema的依赖Schema,在检查开始之前要进行处理:
- 对于每个存在未处理字段的Schema(
UnsolvedStates
中的键值对),遍历所有UnresolvedKeys
: - 使用
GetSchemeIsOptional()
尝试获取它的可选项 - 如果可选则加入
OptionalKeys
集合
由于在
ResolveAllUnresolved()
调用之前已经完成了所有Schema的遍历,因此可以从缓存MySchemeCache
中获取名称为SchemeName
的Schema列表(存在同名Schema,但一定在不同脚本文件中):- 从
segment
中解析Schema的名称和定义所在的脚本文件
- 从列表中找到所在脚本文件匹配的Schema
- 尝试获取该Schema的可选性
- 能获取到,直接返回可选性布尔值
- 获取不到,则递归调用获取它的关联Schema(声明时用…Object展开的另一个Schema)的可选性
辅助函数
在源文件中查找符号的导入路径:
解析相对路径为绝对路径:
通过标识符获取Schema定义所在的脚本文件路径:
最终检查
在
SchemeManager::CheckAllIsValid()
中进行最终检查:对于每个Schema的检查在
CheckSchemeOptionalIsValid()
中进行:- Author:Yuki
- URL:http://shirakoko.xyz/article/typescript-ast
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts