38.5 Сопоставление с образцом узлов Tree-sitter

Tree-sitter позволяет программам на Lisp сопоставлять шаблоны с использованием небольшого декларативного языка. Это сопоставление с образцом состоит из двух этапов: сначала tree-sitter сопоставляет образец с узлами в синтаксическом дереве, затем он захватывает конкретные узлы, которые соответствуют образцу, и возвращает захваченные узлы.

Сначала мы опишем, как написать самый простой образец запроса и как захватывать узлы в образце, затем функцию сопоставления с образцом и, наконец, более продвинутый синтаксис образцов.

Базовый синтаксис запросов

Запрос состоит из нескольких образцов. Каждый образец представляет собой s-выражение, которое соответствует определённому узлу в синтаксическом узле. Образец имеет форму (тип (дочерний_узел…)).

Например, образец, который соответствует узлу binary_expression, содержащему дочерние узлы number_literal, будет выглядеть так:

(binary_expression (number_literal))

Чтобы захватить узел с помощью приведённого выше образца запроса, добавьте @имя-захвата после образца узла, который вы хотите захватить. Например,

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

захватывает узлы number_literal, которые находятся внутри узла binary_expression, с именем захвата number-in-exp.

Мы также можем захватить сам узел binary_expression, например, с именем захвата biexp:

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

Функция запроса

Теперь мы можем представить функции запроса.

Функция: treesit-query-capture узел запрос &optional начало конец только-узлы

Эта функция сопоставляет образцы в запросе внутри узла. Аргумент запрос может быть s-выражением, строкой или скомпилированным объектом запроса. Пока мы сосредоточимся на синтаксисе s-выражений; строковый синтаксис и скомпилированные запросы описаны в конце раздела.

Аргумент узел также может быть парсером или символом языка. Парсер означает использование его корневого узла, символ языка означает поиск или создание парсера для этого языка в текущем буфере и использование корневого узла.

Функция возвращает все захваченные узлы в виде ассоциативного списка с элементами вида (имя_захвата . узел). Если только-узлы не равен nil, она возвращает список узлов вместо этого. По умолчанию поиск ведётся по всему тексту узла, но если начало и конец оба не равны nil, они задают область текста буфера, в которой эта функция должна сопоставлять узлы. Захватывается любой соответствующий узел, чей диапазон пересекается с областью между началом и концом; он не обязан полностью находиться внутри этой области.

Эта функция вызывает ошибку treesit-query-error, если запрос имеет неверный формат. Данные сигнала содержат описание конкретной ошибки. Вы можете использовать treesit-query-validate для проверки и отладки запроса.

Например, предположим, что текст узла — 1 + 2, а запрос — это

(setq query
      '((binary_expression
         (number_literal) @number-in-exp) @biexp)

Сопоставление с этим запросом вернёт

(treesit-query-capture node query)
    ⇒ ((biexp . <узел для "1 + 2">)
       (number-in-exp . <узел для "1">)
       (number-in-exp . <узел для "2">))

Как упоминалось ранее, запрос может содержать несколько образцов. Например, он может содержать два образца верхнего уровня:

(setq query
      '((binary_expression) @biexp
        (number_literal) @number @biexp)
Функция: treesit-query-string строка запрос язык

Эта функция анализирует строку как язык, сопоставляет её корневой узел с запросом и возвращает результат.

Дополнительный синтаксис запросов

Помимо типа узла и имени захвата, синтаксис образцов tree-sitter позволяет выражать анонимный узел, имя поля, шаблон, квантификацию, группировку, альтернативу, якорь и предикат.

Анонимный узел

Анонимный узел записывается дословно, в кавычках. Образец, сопоставляющий (и захватывающий) ключевое слово return, будет таким:

"return" @keyword

Шаблон

В образце ‘(_)’ соответствует любому именованному узлу, а ‘_’ соответствует любому именованному или анонимному узлу. Например, чтобы захватить любой именованный дочерний элемент узла binary_expression, образец будет таким:

(binary_expression (_) @in-biexp)

Имя поля

Можно захватывать дочерние узлы, имеющие конкретные имена полей. В образце ниже declarator и body — это имена полей, на что указывает двоеточие после них.

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

Также можно захватить узел, у которого отсутствует определённое поле, скажем, function_definition без поля body:

(function_definition !body) @func-no-body

Квантификация узла

Tree-sitter распознаёт операторы квантификации ‘:*’, ‘:+’ и ‘:?’. Их значения такие же, как в регулярных выражениях: ‘:*’ соответствует предшествующему образцу ноль или более раз, ‘:+’ — один или более раз, а ‘:?’ — ноль или один раз.

Например, следующий образец соответствует узлам type_declaration, которые имеют ноль или более ключевых слов long.

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

Следующий образец соответствует объявлению типа, которое может как иметь, так и не иметь ключевое слово long:

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

Группировка

Аналогично группам в регулярных выражениях, мы можем объединять образцы в группы и применять к ним операторы квантификации. Например, чтобы выразить разделённый запятыми список идентификаторов, можно написать:

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

Альтернатива

Опять же, аналогично регулярным выражениям, мы можем выразить «соответствие любому из этих образцов» в образце. Синтаксис — это вектор образцов. Например, чтобы захватить некоторые ключевые слова в C, образец будет таким:

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

Якорь

Оператор якоря :anchor можно использовать для принудительного соположения, то есть для требования, чтобы две сущности находились непосредственно рядом друг с другом. Этими двумя «сущностями» могут быть два узла, или дочерний узел и конец его родителя. Например, чтобы захватить первый дочерний элемент, последний дочерний элемент или два соседних дочерних элемента:

;; Привязка дочернего элемента к концу его родителя.
(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.

Предикат: :equal арг1 арг2

Соответствует, если арг1 равен арг2. Аргументы могут быть строками или именами захвата. Имена захвата представляют текст, который занимает захваченный узел в буфере.

Предикат: :match регвыр имя-захвата

Соответствует, если текст, который занимает узел имени-захвата в буфере, соответствует регулярному выражению регвыр, заданному как строковый литерал. Сопоставление чувствительно к регистру.

Предикат: :pred функция &rest узлы

Соответствует, если функция возвращает не 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)
  )"

Компиляция запросов

Если запрос предполагается использовать многократно, особенно в тесных циклах, важно скомпилировать этот запрос, поскольку скомпилированный запрос работает значительно быстрее нескомпилированного. Скомпилированный запрос можно использовать везде, где принимается запрос.

Функция: treesit-query-compile язык запрос

Эта функция компилирует запрос для языка в скомпилированный объект запроса и возвращает его.

Эта функция вызывает ошибку treesit-query-error, если запрос имеет неверный формат. Данные сигнала содержат описание конкретной ошибки. Вы можете использовать treesit-query-validate для проверки и отладки запроса.

Функция: treesit-query-language запрос

Эта функция возвращает язык запроса.

Функция: treesit-query-expand запрос

Эта функция преобразует s-выражение запроса в строковый формат.

Функция: treesit-pattern-expand образец

Эта функция преобразует s-выражение образца в строковый формат.

Для получения более подробной информации прочтите документацию проекта tree-sitter о сопоставлении с образцом, которая доступна по адресу https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries.