资讯专栏INFORMATION COLUMN

tsquery——一个方便的ast查询工具

Neilyo / 1560人阅读

摘要:结合上面三个函数,我们可以得到的基本使用方法获得语法树获得选择器查找节点如果语法树和选择器可能被多次使用,则建议使用变量将它们分别保存下来,避免重复解析导致的资源浪费和时间开销的生成和遍历还是比较花时间的。

前言

最近在给公司的 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(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(ast: Node | TSQueryNode, selector: TSQuerySelectorNode, options?: TSQueryOptions): Array>;

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(ast: string | Node | TSQueryNode, selector: string, options?: TSQueryOptions): Array>;

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 上进行在线的测试。

参考内容:

Easier TypeScript tooling with TSQuery

在文章最后打个招聘广告:

有赞招聘前端工程师,实习、校招、社招都可,具体要求可以参考https://job.youzan.com/,同时您也可以将简历投递到我的内推邮箱:zhangshikai@youzan.com

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97820.html

相关文章

  • 探索babel和babel插件是怎么工作

    摘要:我们更多要去做的是去修改和改变生成的这个抽象语法树。我们已经知道会遍历节点组成的抽象语法树,每一个节点都会有自己对应的比如变量节点等。 你有可能会听到过这个词 webpack工程师 ,这个看似像是一个专业很强的职位其实很多时候是一些前端对现在前端工作方式对一些吐槽,对于一个之前没有接触过webpack,nodejs,babel 之类的工具的人来说,看到大量的配置文件后很多人都会看懵 s...

    dongxiawu 评论0 收藏0
  • 使用 Phan 为你 PHP 项目保驾护航 - 代码静态扫描

    摘要:比如上面的例子文件文件我们利用做了语法解析检测,代码如下报错哪里类重复了不存在查看该属性是否存在于父类中原理能就是对解析出来的继续做分析,但是前人栽树后人乘凉,这样的完整工具已经有大神帮我们做好了。 原文:我的个人博客 https://mengkang.net/1356.html 工作了两三年,技术停滞不前,迷茫没有方向,不如看下我的直播 PHP 进阶之路 (金三银四跳槽必考,一般人...

    array_huang 评论0 收藏0
  • Hive介绍

    摘要:通过给用户提供的一系列交互接口,接收到用户的指令,使用自己的,结合元数据,将这些指令翻译成,提交到中执行,最后,将执行返回的结果输出到用户交互接口。什么是Hivehive简介hive是由Facebook开源用于解决海量结构化日志的数据统计工具,是基于Hadoop的一个数据仓库工具,可以将结构化的数据文件映射为一张表,并提供类 SQL查询功能。Hive本质将HQL转化成MapReduce程序。...

    Tecode 评论0 收藏0
  • Babylon-AST初探-代码查询(Retrieve)

    摘要:针对语法树节点的查询操作通常伴随着和这两种方法见下一篇文章。注意上述代码打印出的和中的并不完全一致。如函数,在中的为,但其实际的为。这个大家一定要注意哦,因为在我们后面的实际代码中也有用到。   在上一篇文章中,我们介绍了AST的Create。在这篇文章中,我们接着来介绍AST的Retrieve。  针对语法树节点的查询(Retrieve)操作通常伴随着Update和Remove(这两...

    wangdai 评论0 收藏0
  • AST抽象语法树——最基础javascript重点知识,99%人根本不了解

    摘要:抽象语法树,是一个非常基础而重要的知识点,但国内的文档却几乎一片空白。事实上,在世界中,你可以认为抽象语法树是最底层。通过抽象语法树解析,我们可以像童年时拆解玩具一样,透视这台机器的运转,并且重新按着你的意愿来组装。 抽象语法树(AST),是一个非常基础而重要的知识点,但国内的文档却几乎一片空白。本文将带大家从底层了解AST,并且通过发布一个小型前端工具,来带大家了解AST的强大功能 ...

    godiscoder 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<