最近在給公司的 web 框架作一個 vscode 的輔助插件,其中有個對須要路由一些文件進行解析,實現配置文件和對應文件的關聯信息顯示和跳轉的功能。既然是對文件進行解析,很天然就會想到使用 ast 的方式來作,加上須要對 TypeScript 也進行支持,我便選擇了使用 TypeScript 自帶的 ast 工具來進行解析。css
在一開始我經過 ts 的forEachChild
方法遍歷和對比節點的kind
屬性來肯定是不是我須要處理的節點,可是以後發現這個方式有幾個缺點:前端
爲了解決這些,我找到並引入了tsquery
這個庫,它是 TypeScript 版的esquery
,可以讓咱們使用 css 選擇器的方式來快速查詢知足指定條件的 TypeScript ast 節點(也支持 JavaScript)。node
在介紹tsquery的使用方式以前,咱們先來看一個對比。git
對下面這段簡單的代碼:github
class Animal { constructor(public name: string) { } move(distanceInMeters: number = 0) { console.log(`${this.name} moved ${distanceInMeters}m.`); } }
若咱們要查找到Animal這個類的構造函數的全部參數並打印它們的名稱,在使用 tsquery 以前,咱們會編寫這樣一段代碼:web
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用到本身的項目中?express
那麼接下來,我就來介紹一下如何去使用tsquery:api
tsquery對象提供了下面幾個方法:數組
function ast(source: string, fileName?: string): SourceFile;
ast方法的功能如同其名,就是接收源代碼,返回一個解析後的ast語法樹,實際上就是調用了ts的createSourceFile
方法。
function parse(selector: string, options?: TSQueryOptions): TSQuerySelectorNode;
parse方法接收一個規則字符串,這個字符串會被解析成tsquery的選擇器對象並返回,再被用於下面的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
。
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節點的類型來看成查詢的選擇器,例如:類聲明: 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