tsquery——一個方便的ast查詢工具
前言
最近在給公司的 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
- 各種特殊的選擇器
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]