Tree-sitterを使用すると、Lispプログラムで小規模な宣言型言語を用いてパターンマッチングを行うことができます。このパターンマッチングは2つのステップで構成されています。まずTree-sitterが構文木のノードに対してパターンを照合し、次にパターンに一致した特定のノードをキャプチャして、それらのキャプチャ済みノードを返します。
まず、最も基本的なクエリパターンの書き方とパターン内でのノードの捕捉方法について説明し、次にパターンマッチング関数、そして最後に、より高度なパターン構文について解説します。
クエリは、複数のパターンで構成されます。各パターンは、構文ノード内の特定のノードにマッチするs式です。パターンは(type (child…))という形式をとります。
例えば、number_literal 子ノードを含む binary_expression ノードにマッチするパターンは、次のようになります。
(binary_expression (number_literal))
上記のクエリパターンを用いてノードをキャプチャするには、キャプチャしたいノードパターンの後に @capture-name を付加します。例えば、
(binary_expression (number_literal) @number-in-exp)
キャプチャ名 number-in-exp の binary_expression ノード内にある number_literal ノードをキャプチャします。
binary_expression ノードも、例えばキャプチャ名 biexp としてキャプチャできます:
(binary_expression (number_literal) @number-in-exp) @biexp
ここでクエリ関数を紹介します。
この関数は、node 内の query のパターンにマッチします。引数 query には、s式、文字列、またはコンパイル済みクエリオブジェクトのいずれかを指定できます。ここではまず s式の構文に焦点を当て、文字列の構文とコンパイル済みクエリについては本節の最後で説明します。
引数 node は、パーサーや言語シンボルである場合もあります。パーサーとはそのルートノードを使用することを意味し、言語シンボルとは、現在のバッファ内でその言語用のパーサーを検索または作成し、そのルートノードを使用することを意味します。
この関数は、キャプチャされたすべてのノードを、(capture_name . node) 形式の要素を持つアリストとして返します。node-only が nil 以外の場合は、代わりに node のリストを返します。デフォルトでは node
のテキスト全体が検索対象となりますが、beg と end
がともに nil
以外である場合、それらはこの関数がノードをマッチさせるべきバッファテキストの範囲を指定します。beg と
end
の間の範囲とスパンが重なるマッチしたノードはすべてキャプチャされます。そのノードが必ずしも範囲内に完全に収まっている必要はありません。
grouped が nil
でない場合、この関数はキャプチャされたノードのリストをグループ化したリストを返します。グループ化は query によって決定されます。query
のパターンの同一マッチにおけるキャプチャは、まとめてグループ化されます。
この関数は、query の形式が不正な場合に treesit-query-error
エラーを発生させます。シグナルデータには、具体的なエラーの内容が含まれています。treesit-query-validate を使用して、クエリの検証やデバッグを行うことができます。
例えば、nodeのテキストが1 + 2で、queryが
(setq query
'((binary_expression
(number_literal) @number-in-exp) @biexp)
そのクエリに一致すると
(treesit-query-capture node query)
⇒ ((biexp . <node for "1 + 2">)
(number-in-exp . <node for "1">)
(number-in-exp . <node for "2">))
前述の通り、query には複数のパターンを含めることができます。例えば、2つのトップレベルパターンを持つことができます:
(setq query
'((binary_expression) @biexp
(number_literal) @number)
この関数は string を language として解析し、そのルートノードを query と照合して結果を返します。
ノードタイプやキャプチャ名に加え、Tree-sitterのパターン構文では、匿名ノード、フィールド名、ワイルドカード、量化、グループ化、選択、アンカー、および述語を表現できます。
匿名ノードは、引用符で囲んでそのまま記述します。パターンマッチング(およびキャプチャ)キーワード return は、
"return" @keyword
パターンにおいて、‘(_)’は任意の名前付きノードに、‘_’は任意の名前付きまたは匿名ノードに一致します。例えば、binary_expressionノードの任意の名前付き子要素をキャプチャする場合、パターンは次のようになります。
(binary_expression (_) @in-biexp)
特定のフィールド名を持つ子ノードをキャプチャすることが可能です。以下のパターンでは、declarator と body
がフィールド名であり、その後に続くコロンで示されています。
(function_definition declarator: (_) @func-declarator body: (_) @func-body)
また、特定のフィールドを持たないノード(例えば、bodyフィールドのないfunction_definition)をキャプチャすることも可能です:
(function_definition !body) @func-no-body
Tree-sitterは、量化演算子 ‘:*’、‘:+’、および ‘:?’ を認識します。これらの意味は正規表現と同様で、‘:*’ は直前のパターンに0回以上、‘:+’ は1回以上、‘:?’ は0回または1回マッチします。
例えば、次のパターンは、0個以上のlongキーワードを持つtype_declarationノードにマッチします。
(type_declaration "long" :*) @long-type
以下のパターンは、long キーワードを含む場合と含まない場合がある型宣言にマッチします:
(type_declaration "long" :?) @long-type
正規表現のグループと同様に、パターンをグループ化し、それらに量指定演算子を適用することができます。 例えば、識別子のカンマ区切りリストを表現するには、次のように記述できます。
(identifier) ("," (identifier)) :*
ここでも、正規表現と同様に、パターン内で「これらのパターンのいずれかにマッチする」ことを表現できます。構文はパターンのベクトルとなります。例えば、C言語のキーワードをいくつかキャプチャする場合、パターンは次のようになります。
[ "return" "break" "if" "else" ] @keyword
アンカー演算子 :anchor
を使用すると、並置(つまり、2つの要素を直接的に隣接させること)を強制できます。この2つの「要素」とは、2つのノード、あるいは子要素とその親要素の末尾を指します。例えば、最初の子要素、最後の子要素、または隣接する2つの子要素をキャプチャする場合:
;; Anchor the child with the end of its parent. (compound_expression (_) @last-child :anchor)
;; Anchor the child with the beginning of its parent. (compound_expression :anchor (_) @first-child)
;; Anchor two adjacent children. (compound_expression (_) @prev-child :anchor (_) @next-child)
なお、並置の適用では匿名ノードは無視される点に注意してください。
パターンに述語制約を追加することが可能です。例えば、次のようなパターンの場合:
( (array :anchor (_) @first (_) @last :anchor) (:eq? @first @last) )
Tree-sitterは、最初の要素が最後の要素と等しい配列にのみマッチします。パターンに述語を紐付けるには、それらをグループ化する必要があります。現在、:eq?、:match?、:pred?の3つの述語があります。
arg1 が arg2
と等しい場合にマッチします。引数には文字列またはキャプチャ名を指定できます。キャプチャ名は、キャプチャされたノードがバッファ内でまたがるテキストを表します。これは
Elisp の equal に近い形式ですが、tree-sitter では eq? という規約が採用されている点に注意してください。以前は :equal 述語もサポートしていましたが、現在は非推奨となっています。
バッファ内でcapture-nameのノードが跨ぐテキストが、文字列リテラルとして指定された正規表現regexpに一致する場合にマッチします。マッチングは大文字小文字を区別します。引数の順序は問いません。以前は:match述語をサポートしていましたが、現在は非推奨となっています。
nodes の各ノードを引数として渡した際に、関数 fn が
nil
以外の値を返す場合にマッチします。この関数は、現在のバッファにクエリ対象のノードのバッファが設定された状態で実行されます。この述語をタイトなループ内で使用するとコストが高くなる可能性があるため、使用には十分ご注意ください。以前は
:pred 述語をサポートしていましたが、現在は非推奨となっています。
なお、述語が参照できるのは、同一のパターン内に現れるキャプチャ名に限られます。実際、他のパターンにあるキャプチャ名を参照することはほとんど意味がありません。
s式に加えて、EmacsではTree-sitter独自のクエリ構文を文字列として記述することで利用できます。これは概ねs式の構文に似ています。例えば、次のようなクエリ
(treesit-query-capture
node '((addition_expression
left: (_) @left
"+" @plus-sign
right: (_) @right) @addition
["return" "break"] @keyword))
に相当する
(treesit-query-capture
node "(addition_expression
left: (_) @left
\"+\" @plus-sign
right: (_) @right) @addition
[\"return\" \"break\"] @keyword")
ほとんどのパターンは、文字列内で直接s式として記述できます。修正が必要なのは、ごく一部だけです:
:anchor は ‘.’ と記述されます。
:eq?、:match?、:pred?は、それぞれ#eq?、#match?、#pred?と表記される。一般に、述語は‘:’を‘#’に変化させる。
例えば、
'(( (compound_expression :anchor (_) @first (_) :* @rest) (:match? "love" @first) ))
文字列形式では次のように記述されます。
"( (compound_expression . (_) @first (_)* @rest) (#match? \"love\" @first) )"
クエリを繰り返し使用する場合、特にタイトなループ内で使用する場合は、そのクエリをコンパイルしておくことが重要です。コンパイル済みのクエリは、未コンパイルのものよりもはるかに高速だからです。コンパイル済みのクエリは、クエリが受け付けられるあらゆる場所で使用できます。
この関数は、language に対する query をコンパイル済みクエリオブジェクトにコンパイルして返します。
この関数は、query の形式が不正な場合に treesit-query-error
エラーを発生させます。シグナルデータには、具体的なエラーの内容が含まれています。treesit-query-validate を使用して、クエリの検証やデバッグを行うことができます。
デフォルトでは、Emacsはqueryを遅延コンパイルするため、queryは実際に使用されるまでコンパイルされません。queryを即座にコンパイルするには、eagerにnil以外を指定してください。
実際にコンパイルされたクエリと、コンパイルされていないクエリを区別するには、treesit-query-eagerly-compiled-p を使用します。
query の形式が不正であるか、あるいは言語をロードできない場合、この関数は treesit-query-error を返します。当然ながら、これは eager が nil
ではない場合にのみ発生します。それ以外の場合、Emacs は実際には query
をコンパイルしないためです。
クエリ用の追加関数がいくつか用意されています:treesit-query-language
はクエリの言語を返し、treesit-query-source
はコンパイル済みクエリの元の文字列または sexp ソースクエリを返し、treesit-query-valid-p はクエリが有効かどうかを判定し、treesit-query-expand は s-expression クエリを文字列形式に変換し、treesit-pattern-expand はパターンを変換します。
Tree-sitterの文法は時間の経過とともに変化します。文法の複数のバージョンに対応するため、Lispプログラムではtreesit-query-first-validを使用して適切なクエリを選択できます。例えば、あるバージョンで文法に(defun)ノードがあり、後にそれが(function_definition)にリネームされた場合、Lispプログラムでは
(treesit-query-first-valid 'lang '((defun) @defun) '((function_definition) @defun))
文法の両バージョンに対応すること。
詳細については、Tree-sitterプロジェクトのパターンマッチングに関するドキュメントをご参照ください。ドキュメントは https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries でご確認いただけます。