Spec-Zone.ru › Elisp

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

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

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

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

Запрос состоит из нескольких образцов. Каждый образец — это s-выражение, соответствующее определённому узлу синтаксического дерева. Образец имеет вид (type (child…)).

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

(binary_expression (number_literal))

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

(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 node query &optional beg end node-only grouped

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

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

Функция возвращает все захваченные узлы в виде alist со значениями вида (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 может содержать несколько образцов. Например, он может состоять из двух образцов верхнего уровня:

(setq query
      '((binary_expression) @biexp
        (number_literal) @number)
Функция: treesit-query-string string query language

Эта функция разбирает string как language, сопоставляет корневой узел с query и возвращает результат.

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

Помимо типа узла и имени захвата, синтаксис образцов 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 позволяет задать смежность, то есть указать, что два элемента должны находиться непосредственно рядом друг с другом. Этими двумя «элементами» могут быть два узла либо дочерний узел и конец его родительского узла. Например, чтобы захватить первый дочерний узел, последний дочерний узел или два соседних дочерних узла:

;; 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?.

Предикат: :eq? arg1 arg2

Сопоставляется, если arg1 равен arg2. Аргументами могут быть строки или имена захватов. Имена захватов обозначают текст буфера, занимаемый захваченным узлом. Обратите внимание, что это больше похоже на equal в Elisp, но tree-sitter использует соглашение eq?. Ранее поддерживался предикат :equal, но теперь он считается устаревшим.

Предикат: :match? capture-name regexp

Сопоставляется, если текст буфера, занимаемый узлом capture-name, соответствует регулярному выражению regexp, заданному строковым литералом. Сопоставление учитывает регистр. Порядок аргументов не имеет значения. Ранее поддерживался предикат :match, но теперь он считается устаревшим.

Предикат: :pred? fn &rest nodes

Сопоставляется, если функция fn, получив в качестве аргументов каждый узел из nodes, возвращает значение, не равное 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)
  )"

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

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

Функция: treesit-query-compile language query &optional eager

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

Если 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 возвращает исходный запрос с исходным текстом в виде строки или s-выражения для скомпилированного запроса; treesit-query-valid-p проверяет корректность запроса; treesit-query-expand преобразует запрос в виде s-выражения в строковый формат; 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.

Copyright © 1990-1996, 1998-2026 Free Software Foundation, Inc.
Licensed under the GNU GPL license.
https://www.gnu.org/software/emacs/manual/html_node/elisp/Pattern-Matching.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API