38.5 匹配 tree-sitter 节点的模式

tree-sitter 允许 Lisp 程序使用一门简洁的声明式语言进行模式匹配。该模式匹配分为两步:首先 tree-sitter 将 模式(pattern) 与语法树中的节点进行匹配,然后 捕获(captures) 匹配到的指定节点并返回。

我们首先介绍如何编写最基础的查询模式以及如何在模式中捕获节点,接着介绍模式匹配函数,最后讲解更高级的模式语法。

基础查询语法

一个 查询(query) 由多个 模式(patterns) 组成。每个模式都是一个匹配语法树中特定节点的 S 表达式。模式的格式为 (type (child…))

例如,匹配包含 number_literal 子节点的 binary_expression 节点的模式如下:

(binary_expression (number_literal))

要使用上述查询模式 捕获(capture) 节点,只需在需要捕获的节点模式后添加 @capture-name。例如:

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

该模式会捕获 binary_expression 节点内部的 number_literal 节点,并将捕获名称设为 number-in-exp

我们也可以捕获 binary_expression 节点本身,例如使用捕获名称 biexp

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

查询函数

现在我们来介绍 查询函数(query functions)

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

该函数在 node 范围内匹配 query 中的模式。参数 query 可以是 S 表达式、字符串或编译后的查询对象。目前我们重点介绍 S 表达式语法;字符串语法和编译查询将在本节末尾介绍。

参数 node 也可以是解析器或语言符号。传入解析器时使用其根节点,传入语言符号时会在当前缓冲区中查找或创建对应语言的解析器,并使用其根节点。

函数以 alist 形式返回所有捕获的节点,元素格式为 (capture_name . node)。如果 node-only 为非 nil 值,则直接返回 node 列表。默认情况下会搜索 node 的全部文本,如果 begend 均为非 nil 值,则指定函数匹配节点的缓冲区文本区域。任何覆盖范围与 begend 区域重叠的匹配节点都会被捕获,无需完全包含在该区域内。

如果 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 可以包含多个模式。例如,它可以包含两个顶层模式:

(setq query
      '((binary_expression) @biexp
        (number_literal) @number @biexp)
Function: treesit-query-string string query language

该函数将 string 按照 language 解析,用 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 支持量词操作符 ‘:*’、‘:+’ 和 ‘:?’。它们的含义与正则表达式一致:‘:*’ 匹配前面的模式零次或多次,‘:+’ 匹配一次或多次,‘:?’ 匹配零次或一次。

例如,以下模式匹配包含 零个或多个 long 关键字的 type_declaration 节点:

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

以下模式匹配可能包含也可能不包含 long 关键字的类型声明:

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

分组

与正则表达式中的分组类似,我们可以将多个模式打包为分组,并对分组应用量词操作符。例如,表示以逗号分隔的标识符列表:

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

选择

同样与正则表达式类似,我们可以在模式中表示 “匹配这些模式中的任意一个”。语法为使用向量包裹多个模式。例如,捕获 C 语言中的部分关键字:

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

锚点

锚点操作符 :anchor 用于强制相邻,即强制两个元素直接相邻。这两个“元素(things)” 可以是两个节点,也可以是子节点与父节点的边界。例如,捕获第一个子节点、最后一个子节点或两个相邻子节点:

;; 将子节点与父节点的结束位置锚定
(compound_expression (_) @last-child :anchor)

;; 将子节点与父节点的起始位置锚定
(compound_expression :anchor (_) @first-child)

;; 锚定两个相邻子节点
(compound_expression
 (_) @prev-child
 :anchor
 (_) @next-child)

注意:强制相邻的规则会忽略所有匿名节点。

谓词

可以为模式添加谓词约束。例如,以下模式:

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

tree-sitter 仅匹配第一个元素与最后一个元素相等的数组。要将谓词附加到模式,需要将它们分组。目前支持三种谓词::equal:match:pred

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")

大多数模式可以直接以字符串形式书写,仅少数需要修改:

例如:

'((
   (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 错误。信号数据包含具体错误描述。你可以使用 treesit-query-validate 验证并调试查询语句。

Function: treesit-query-language query

该函数返回 query 对应的语言。

Function: treesit-query-expand query

该函数将 S 表达式格式的 query 转换为字符串格式。

Function: treesit-pattern-expand pattern

该函数将 S 表达式格式的 pattern 转换为字符串格式。

更多细节请阅读 tree-sitter 项目的模式匹配文档,文档地址: https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries