前言
最近在给公司的 web 框架做一个 vscode 的辅助插件,其中有个对需要路由一些文件进行解析,实现配置文件和对应文件的关联信息显示和跳转的功能。既然是对文件进行解析,很自然就会想到使用 ast 的方式来做,加上需要对 TypeScript 也进行支持,我便选择了使用 TypeScript 自带的 ast 工具来进行解析。
在一开始我通过 ts 的forEachChild
方法遍历和对比节点的kind
属性来确定是否是我需要处理的节点,但是之后发现这个方式有几个缺点:
- 当需要查找满足条件的子级的 ast 节点时,需要做多次比较
- 对满足某一条件的多个不同类型的节点需要比较多次,编写满足条件麻烦
- 对分布在同一文件中的多个同名标识符,不能统一提取和处理
为了解决这些,我找到并引入了tsquery
这个库,它是 TypeScript 版的esquery
,能够让我们使用 css 选择器的方式来快速查询满足指定条件的 TypeScript ast 节点(也支持 JavaScript)。
比较 demo
在介绍tsquery的使用方式之前,我们先来看一个对比。
对下面这段简单的代码:
class Animal {
constructor(public name: string) { }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
若我们要查找到Animal这个类的构造函数的所有参数并打印它们的名称,在使用 tsquery 之前,我们会编写这样一段代码:
import { ClassDeclaration, createSourceFile, Node, ScriptTarget, ConstructorDeclaration, SyntaxKind } from 'TypeScript';
import { code } from './code';
const sourceFile = createSourceFile('fileName', code, ScriptTarget.Latest, true);
sourceFile.forEachChild(findClass);
function findClass(node: Node): void {
if (node.kind === SyntaxKind.ClassDeclaration) {
const { name } = node as ClassDeclaration;
if (name && name.text === 'Animal') {
node.forEachChild(findConstructor);
return;
}
}
node.forEachChild(findClass);
}
function findConstructor(node: Node): void {
if (node.kind === SyntaxKind.Constructor) {
printParameters(node as ConstructorDeclaration);
}
}
function printParameters(node: ConstructorDeclaration) {
node.parameters.forEach(parameter => {
console.log(parameter.name.getText());
})
}
而在我们引入了tsquery之后,只需要下面这么几行简单的代码:
import { tsquery } from '@phenomnomnominal/tsquery';
import * as ts from 'TypeScript';
import { code } from './code';
const parameters = tsquery.query<ts.ParameterDeclaration>(code, 'ClassDeclaration[name.name="Animal"] > Constructor > Parameter');
parameters.forEach(param => console.log(param.name.getText()));
怎么样,是不是对比强烈,让你迫不及待得想把tsquery用到自己的项目中?
使用方式
那么接下来,我就来介绍一下如何去使用tsquery:
API
tsquery对象提供了下面几个方法:
- ast:
function ast(source: string, fileName?: string): SourceFile;
ast方法的功能如同其名,就是接收源代码,返回一个解析后的ast语法树,实际上就是调用了ts的createSourceFile
方法。
- parse:
function parse(selector: string, options?: TSQueryOptions): TSQuerySelectorNode;
parse方法接收一个规则字符串,这个字符串会被解析成tsquery的选择器对象并返回,再被用于下面的match方法中。
- match:
function match<T extends Node = Node>(ast: Node | TSQueryNode<T>, selector: TSQuerySelectorNode, options?: TSQueryOptions): Array<TSQueryNode<T>>;
match方法接收一个ast对象和一个parse解析后得到的选择器对象,返回从ast中搜索得到的所有满足选择器条件的节点的数组。
结合上面三个函数,我们可以得到tsquery的基本使用方法:
const ast = tsquery.ast(code); // 获得ast语法树
const selector = tsquery.parse(selectorStr); // 获得选择器
const result = tsquery.match(ast, selector); // 查找节点
如果语法树和选择器可能被多次使用,则建议使用变量将它们分别保存下来,避免重复解析导致的资源浪费和时间开销(ast的生成和遍历还是比较花时间的)。
如果语法树和选择器不会被重复使用,那么可以使用更简单的方法 query
。
- query:
function query<T extends Node = Node>(ast: string | Node | TSQueryNode<T>, selector: string, options?: TSQueryOptions): Array<TSQueryNode<T>>;
query封装了ast、parse和match三个方法,可以更方便地完成一次查询,同时tsquery自身也是一个query方法。
const result = tsquery.query(code, selectorStr);
// const result = tsquery(code, selectorStr);
选择器规则
- 通用选择器
和css中的一样,*
表示选择所有的节点。
- AST节点类型选择器
你可以直接使用一个ast节点的类型来当作查询的选择器,例如:类声明: ClassDeclaration
,变量声明:VariableDeclaration
等,就跟你使用css选择器选择某种HTML元素一样。
- 属性选择器
tsquery支持使用css中属性选择器的方式来搜索满足属性条件的节点,你可以仅仅只声明一个属性的名称(例如:[text]
),也可以指定属性的值所满足的条件(例如:[text="foo"]
),其中操作符可以是=
、’!=’、’>’、'<‘、'<=’、’>=’,值也可以是字符串、数字、正则表达式中的任意一种。
tsquery支持多级的属性选择,所以你也可以使用.
来组合属性(例如:[members.length<3]
)。
- 常见的后代、兄弟节点选择器等
后代节点选择器:node otherNode
子节点选择器:node > otherNode
同级节点选择器:node ~ otherNode
相邻节点选择器:node + otherNode
群组选择器:node, otherNode
- 各种特殊的选择器
not选择器::not(ClassDeclaration)
用来选择所有不是类声明的节点
has选择器:IfStatement:has([left.text="foo"])
用来选择含有符合[left.text="foo"]
属性选择器的子节点的if语句
第n个节点的选择器:包含 :first-child
、:last-child
、:nth-child(n)
、:nth-last-child(n)
这几种选择器,其中需要注意的是,tsquery并不支持an+b
这种类型的序号匹配
类型选择器:区分于AST节点类型选择器,这个选择器是用来选择某种共通类型的(比如所有声明、所有表达式等),目前支持的有:statement
, :expression
, :declaration
, :function
, 和 :pattern
以上所有的选择器都可以混合使用
总结
tsquery 是一个非常方便和值得使用的 ast 辅助工具,它使用极为简单的 api 和学习成本较低的选择器规则,提供了对抽象和复杂的 AST 语法树较强的查询能力,可以在我们对 AST 进行处理时节省大量的编写成本。
如果你对 tsquery 的选择器规则抱有疑问,可以在 TSQuery Playground 上进行在线的测试。
参考内容:
在文章最后打个招聘广告:
有赞招聘前端工程师,实习、校招、社招都可,具体要求可以参考https://job.youzan.com/,同时您也可以将简历投递到我的内推邮箱:zhangshikai@youzan.com