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
Теперь мы можем представить функции запроса.
Эта функция сопоставляет образцы в запросе внутри узла. Аргумент запрос может быть 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)
Эта функция анализирует строку как язык, сопоставляет её корневой узел с запросом и возвращает результат.
Помимо типа узла и имени захвата, синтаксис образцов 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.
Соответствует, если арг1 равен арг2. Аргументы могут быть строками или именами захвата. Имена захвата представляют текст, который занимает захваченный узел в буфере.
Соответствует, если текст, который занимает узел имени-захвата в буфере, соответствует регулярному выражению регвыр, заданному как строковый литерал. Сопоставление чувствительно к регистру.
Соответствует, если функция возвращает не 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-выражения внутри строки. Лишь некоторые из них требуют изменения:
:anchor записывается как ‘.’.
:equal, :match и :pred записываются как #equal, #match и #pred соответственно. В общем случае в предикатах ‘:’ заменяется на ‘#’.
Например,
'(( (compound_expression :anchor (_) @first (_) :* @rest) (:match "love" @first) ))
в строковой форме записывается как
"( (compound_expression . (_) @first (_)* @rest) (#match \"love\" @first) )"
Если запрос предполагается использовать многократно, особенно в тесных циклах, важно скомпилировать этот запрос, поскольку скомпилированный запрос работает значительно быстрее нескомпилированного. Скомпилированный запрос можно использовать везде, где принимается запрос.
Эта функция компилирует запрос для языка в скомпилированный объект запроса и возвращает его.
Эта функция вызывает ошибку treesit-query-error, если запрос имеет неверный формат. Данные сигнала содержат описание конкретной ошибки. Вы можете использовать treesit-query-validate для проверки и отладки запроса.
Эта функция возвращает язык запроса.
Эта функция преобразует s-выражение запроса в строковый формат.
Эта функция преобразует s-выражение образца в строковый формат.
Для получения более подробной информации прочтите документацию проекта tree-sitter о сопоставлении с образцом, которая доступна по адресу https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries.