1. 程式人生 > >tsquery——一個方便的ast查詢工具

tsquery——一個方便的ast查詢工具

前言

最近在給公司的 web 框架做一個 vscode 的輔助外掛,其中有個對需要路由一些檔案進行解析,實現配置檔案和對應檔案的關聯資訊顯示和跳轉的功能。既然是對檔案進行解析,很自然就會想到使用 ast 的方式來做,加上需要對 TypeScript 也進行支援,我便選擇了使用 TypeScript 自帶的 ast 工具來進行解析。

在一開始我通過 ts 的forEachChild方法遍歷和對比節點的kind屬性來確定是否是我需要處理的節點,但是之後發現這個方式有幾個缺點:

  1. 當需要查詢滿足條件的子級的 ast 節點時,需要做多次比較
  2. 對滿足某一條件的多個不同型別的節點需要比較多次,編寫滿足條件麻煩
  3. 對分佈在同一檔案中的多個同名識別符號,不能統一提取和處理

為了解決這些,我找到並引入了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

  • 各種特殊的選擇器

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/,同時您也可以將簡歷投遞到我的內推郵箱:[email protected]