Next: , Previous: , Up: プログラムソースの解析   [Contents][Index]


37.5 tree-sitterノードにたいするパターンマッチング

tree-sitterでは小さな宣言型言語を用いてLispプログラムによるパターンのマッチングができます。このパターンマッチングは2つのステップから構成されています。まずtree-sitterが構文ツリーのノードにたいするパターン(pattern)のマッチを行い、その後にパターンにマッチした特定のノードをキャプチャー(capture)してそのノードをリターンするのです。

まずはもっとも基本的なクエリーパターンを記述してパターン内のノードをキャプチャーする方法、それからパターンマッチング関数、そして最後により上級のパターン構文について説明していきます。

基本的なクエリー構文

クエリー(query: 問い合わせ)は複数のパターン(patterns)によって構成されます。パターンはそれぞれ構文ノード内の特定ノードにマッチするS式です。パターンは(type (child…))という形式をもっています。

たとえば子ノードを含んだノードbinary_expressionにマッチするのは以下のようなパターンでしょう

(binary_expression (number_literal))

上記のクエリーパターンを用いてノードをキャプチャー(capture)するためには、キャプチャーしたいノードパターンの後に@capture-nameを追加します。たとえば、

(binary_expression (number_literal) @number-in-exp)

これはノードbinary_expressionにあるノードnumber_literalをキャプチャー名でキャプチャーします。

同じようにして、たとえばキャプチャー名biexpでノードbinary_expressionをキャプチャーできます:

(binary_expression
 (number_literal) @number-in-exp) @biexp

クエリー関数

これでクエリー関数(query functions)を説明する準備ができました。

Function: treesit-query-capture node query &optional beg end node-only

この関数はqueryのパターンをnodeとマッチする。引数queryはS式、文字列、またはコンパイル済みクエリーオブジェクトのいずれか。ここではS式構文に焦点を当てる。文字列構文およびコンパイル済みクエリーについては、このセクションの最後で説明しよう。

引数nodeはパーサー、あるいは言語シンボルでもよい。パーサーの場合にはそのルートノードを、言語シンボルならまずカレントバッファーでその言語用のパーサーを探すか作成してそのルートノードを使用する。

この関数はキャプチャーしたすべてのノードを、(capture_name . node)という要素をもつalistでリターンする。node-onlyが非nilの場合には、かわりにnodeのリストをリターンする。デフォルトではnodeのテキスト全体を検索するが、begendがいずれも非nilであれば、それはこの関数がノードをマッチするべきバッファーテキストのリージョンを指定する。begendの間のリージョンを跨いで重なるようなノードがマッチすればすべてキャプチャーされる(完全にそのリージョンに含まれている必要はない)。

この関数はqueryが不正な形式であればtreesit-query-errorエラーraiseする。シグナルデータには、その特定。エラーに関する説明が含まれている。クエリーの検証とデバッグにはtreesit-query-validateを使うことができる。

たとえばnodeのテキストが1 + 2で、以下のクエリーを考えてみましょう

(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 @biexp)
Function: treesit-query-string string query language

この関数はlanguageとしてstringをパースして、そのルートノードにたいしてqueryをマッチ、その結果をリターンする。

その他のクエリー構文

ノードのタイプとキャプチャー名以外にも、tree-sitterのパターン構文により無名ノード、フィールド名、ワイルドカード、量化、グルーピング、選択、アンカー、述語を表現することができます。

無名ノード

無名ノードはクォートで括ることで逐語的に記述されます。キーワードreturnにマッチ(してキャプチャー)するパターンは以下のようになるでしょう

"return" @keyword

ワイルドカード

パターンにおいて‘(_)’は任意の名前つきノード、‘_’は名前つきノードや無名ノードのすべてにマッチします。たとえばbinary_expressionノードの名前つきの子をすべてキャプチャーするパターンは以下のようになるでしょう

(binary_expression (_) @in-biexp)

フィールド名

特定のフィールド名をもつ子ノードをキャプチャーすることは可能です。以下のパターンでは、フィールド名であることを示すコロンが後置されているdeclaratorbodyがフィールド名です。

(function_definition
  declarator: (_) @func-declarator
  body: (_) @func-body)

特定のフィールドをもたないノード、たとえばbodyフィールドのないfunction_definitionをキャプチャーすることも可能です:

(function_definition !body) @func-no-body

ノードの量化

tree-sitterは量化演算子(quantification operator)の‘:*’、‘:+’,‘:?’を認識します。これらの演算子の意味は正規表現の場合と同じです。‘:*’と‘:+’はそれぞれ前にあるパターンの0個以またはは1個以上の繰り返し、‘:?’は前のパターンの0個または1個の繰り返しにマッチします。

たとえば以下はlongというキーワードの0個以上の繰り返しにマッチするパターンです。

(type_declaration "long" :*) @long-type

こちらはlongというキーワードをもつか、あるいはもたないかというタイプ宣言用のパターンです:

(type_declaration "long" :?) @long-type

グルーピング

正規表現におけるグループと同じように、パターンをグループにまとめたり量化演算子を適用することができます。たとえばカンマで区切られた識別子を表現するには、以下のように記述できるでしょう

(identifier) ("," (identifier)) :*

選択

繰り返しになりますが正規表現と同じように、パターンにおいて“これらのパターンのいずれかにマッチ”と表現することができます。この構文はパターンのベクターです。たとえばCのいくつかのキーワードをキャプチャーするパターンは、以下のように記述できるでしょう

[
  "return"
  "break"
  "if"
  "else"
] @keyword

アンカー

たとえば2つのオブジェクトを隣接させるといったように、アンカー演算子:anchorを用いて並置させることができます。2つの“オブジェクト”には2つのノードや子ノードと最後の親などを指定できます。たとえば最初最後の子、あるいは2つの隣接した子をキャプチャーするには:

;; 子と最後で親をアンカー
(compound_expression (_) @last-child :anchor)

;; 子と最初の親をアンカー
(compound_expression :anchor (_) @first-child)

;; 隣接する2つの子をアンカー
(compound_expression
 (_) @prev-child
 :anchor
 (_) @next-child)

並置させる際には無名ノードは無視されることに注意してください。

述語

パターンに述語による制約を追加することができます。たとえば以下のパターンでは:

(
 (array :anchor (_) @first (_) @last :anchor)
 (:equal @first @last)
)

tree-sitterは配列の最初と最後の要素が等しい場合だけマッチします。パターンに述語を付加するためには、それらをグループにまとめる必要があります。現在のところ:equal:match:predという3つの述語があります。

Predicate: :equal arg1 arg2

arg1arg2が等しければマッチ。引数は文字列またはキャプチャー名のいずれか。キャプチャー名とは、バッファーにおいてキャプチャーされたノードが跨ぐテキストを表す。

Predicate: :match regexp capture-name

バッファーにおいてcapture-nameのノードが跨ぐテキストと、文字列リテラルとして与えられた正規表現regexpがマッチすればマッチ。マッチは大文字小文字を区別する。

Predicate: :pred fn &rest nodes

関数fnnodes内のノードそれぞれを渡して非nilがリターンされたらマッチ。

述語が参照できるのは、同じパターン内に出現するキャプチャー名だけであることに注意してください。実際問題として別のパターンからキャプチャー名を参照しても、意味はほとんどありません。

文字列パターン

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式として直接記述できます。変更が必要になるのはごく少数のパターンだけです:

たとえば、

'((
   (compound_expression :anchor (_) @first (_) :* @rest)
   (:match "love" @first)
   ))

文字列形式では以下のように記述します

"(
  (compound_expression . (_) @first (_)* @rest)
  (#match \"love\" @first)
  )"

クエリーのコンパイル

繰り返し使うことを意図したクエリー、とりわけタイトなループ(訳注 :少ない命令を含み多数回実行されるループ)では、クエリーのコンパイルが重要になります。なぜならコンパイル済みのクエリーは、コンパイルされていないものと比較して非常に高速だからです。クエリーの使用が許されている場所ならどこでもコンパイル済みクエリーを使うことができます。

Function: treesit-query-compile language query

この関数はlanguagequeryをコンパイル済みクエリーオブジェクトにコンパイルして、それをリターンする。

この関数はqueryが不正な形式であればtreesit-query-errorエラーraiseする。シグナルデータには、その特定。エラーに関する説明が含まれている。クエリーの検証とデバッグにはtreesit-query-validateを使うことができる。

Function: treesit-query-language query

この関数はqueryの言語をリターンする。

Function: treesit-query-expand query

この関数はS式のqueryを文字列フォーマットに変換する。

Function: treesit-pattern-expand pattern

この関数はS式のpatternを文字列フォーマットに変換する。

パターンマッチングに関する詳細については、https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queriesにあるtree-sitterプロジェクトのドキュメントを参照してください。


Next: 複数言語ののパース, Previous: ノード情報へのアクセス, Up: プログラムソースの解析   [Contents][Index]