Парсинг с Tree-sitter
Парсинг исходного кода в синтаксические деревья с помощью Tree-sitter. Модуль основан на биндингах go-tree-sitter.
Возможности Tree-sitter:
- Полное представление структуры исходного кода
- Инкрементальное обновление при изменениях
- Устойчивость к синтаксическим ошибкам (частичный парсинг)
- Поиск по шаблонам с помощью S-выражений
Подключение
local treesitter = require("treesitter")
Поддерживаемые языки
| Язык | Псевдонимы | Корневой узел |
|---|---|---|
| Go | go, golang |
source_file |
| JavaScript | js, javascript |
program |
| TypeScript | ts, typescript |
program |
| TSX | tsx |
program |
| Python | python, py |
module |
| Lua | lua |
chunk |
| PHP | php |
program |
| C# | csharp, cs, c# |
compilation_unit |
| HTML | html, html5 |
document |
| Markdown | markdown, md |
document |
| SQL | sql |
- |
local langs = treesitter.supported_languages()
-- {go = true, javascript = true, python = true, ...}
Быстрый старт
Парсинг кода
local code = [[
func hello() {
return "Hello!"
}
]]
local tree, err = treesitter.parse("go", code)
if err then
return nil, err
end
local root = tree:root_node()
print(root:kind()) -- "source_file"
print(root:child_count()) -- количество объявлений верхнего уровня
Поиск по дереву
local code = [[
func hello() {}
func world() {}
]]
local tree = treesitter.parse("go", code)
local root = tree:root_node()
-- Найти все имена функций
local query = treesitter.query("go", [[
(function_declaration name: (identifier) @func_name)
]])
local captures = query:captures(root, code)
for _, capture in ipairs(captures) do
print(capture.name, capture.text)
end
-- "func_name" "hello"
-- "func_name" "world"
Парсинг
Простой парсинг
Парсит исходный код и возвращает синтаксическое дерево. Парсер создаётся автоматически.
local tree, err = treesitter.parse("go", code)
| Параметр | Тип | Описание |
|---|---|---|
language |
string | Название языка или псевдоним |
code |
string | Исходный код |
Возвращает: Tree, error
Переиспользуемый парсер
Создание парсера для многократного использования или инкрементальных обновлений:
local parser = treesitter.parser()
parser:set_language("go")
local tree1 = parser:parse("package main")
-- Инкрементальный парсинг с предыдущим деревом
local tree2 = parser:parse("package main\nfunc foo() {}", tree1)
parser:close()
Возвращает: Parser
Методы парсера
| Метод | Описание |
|---|---|
set_language(lang) |
Установка языка, возвращает boolean, error |
get_language() |
Текущий язык |
parse(code, old_tree?) |
Парсинг, опционально с предыдущим деревом |
set_timeout(duration) |
Тайм-аут (строка типа "1s" или наносекунды) |
set_ranges(ranges) |
Диапазоны байт для парсинга |
reset() |
Сброс состояния |
close() |
Освобождение ресурсов |
Деревья
Корневой узел
local tree = treesitter.parse("go", "package main")
local root = tree:root_node()
print(root:kind()) -- "source_file"
print(root:text()) -- "package main"
Методы дерева
| Метод | Описание |
|---|---|
root_node() |
Корневой узел |
root_node_with_offset(bytes, point) |
Корневой узел со смещением |
language() |
Объект языка дерева |
copy() |
Глубокая копия дерева |
walk() |
Курсор для обхода |
edit(edit_table) |
Применение инкрементальной правки |
changed_ranges(other_tree) |
Изменённые диапазоны |
included_ranges() |
Диапазоны, включённые при парсинге |
dot_graph() |
Представление в формате DOT |
close() |
Освобождение ресурсов |
Инкрементальное редактирование
Обновление дерева при изменении исходного кода:
local code = "func main() { x := 1 }"
local tree = treesitter.parse("go", code)
-- Отметить правку: "1" заменено на "100" в позиции 19
tree:edit({
start_byte = 19,
old_end_byte = 20,
new_end_byte = 22,
start_row = 0,
start_column = 19,
old_end_row = 0,
old_end_column = 20,
new_end_row = 0,
new_end_column = 22
})
-- Повторный парсинг с отредактированным деревом (быстрее полного)
local parser = treesitter.parser()
parser:set_language("go")
local new_tree = parser:parse("func main() { x := 100 }", tree)
Узлы
Узлы представляют элементы синтаксического дерева.
Тип узла
local node = root:child(0)
-- Информация о типе
print(node:kind()) -- "package_clause"
print(node:type()) -- то же, что kind()
print(node:is_named()) -- true для значимых узлов
print(node:grammar_name()) -- имя правила грамматики
Навигация
-- Дочерние узлы
local child = node:child(0) -- по индексу (с 0)
local named = node:named_child(0) -- только именованные
local count = node:child_count()
local named_count = node:named_child_count()
-- Соседние узлы
local next = node:next_sibling()
local prev = node:prev_sibling()
local next_named = node:next_named_sibling()
local prev_named = node:prev_named_sibling()
-- Родитель
local parent = node:parent()
-- По имени поля
local name_node = func_decl:child_by_field_name("name")
local field = node:field_name_for_child(0)
Позиция в коде
-- Смещение в байтах
local start = node:start_byte()
local end_ = node:end_byte()
-- Строка и столбец (с 0)
local start_pt = node:start_point() -- {row = 0, column = 0}
local end_pt = node:end_point() -- {row = 0, column = 12}
-- Исходный текст
local text = node:text()
Обнаружение ошибок
if root:has_error() then
-- В дереве есть синтаксические ошибки
end
if node:is_error() then
-- Этот узел — ошибка
end
if node:is_missing() then
-- Узел добавлен парсером для восстановления после ошибки
end
S-выражение
local sexp = node:to_sexp()
-- "(source_file (package_clause (package_identifier)))"
Запросы
Поиск по шаблонам с использованием языка запросов Tree-sitter (S-выражения).
Создание запроса
local query, err = treesitter.query("go", [[
(function_declaration
name: (identifier) @func_name
parameters: (parameter_list) @params
)
]])
| Параметр | Тип | Описание |
|---|---|---|
language |
string | Название языка |
pattern |
string | Шаблон в виде S-выражения |
Возвращает: Query, error
Выполнение запроса
-- Получить все захваты (плоский список)
local captures = query:captures(root, source_code)
for _, capture in ipairs(captures) do
print(capture.name) -- "@func_name"
print(capture.text) -- исходный текст
print(capture.index) -- индекс захвата
-- capture.node — объект узла
end
-- Получить совпадения (сгруппированные по шаблону)
local matches = query:matches(root, source_code)
for _, match in ipairs(matches) do
print(match.id, match.pattern)
for _, capture in ipairs(match.captures) do
print(capture.name, capture.node:text())
end
end
Управление запросом
-- Ограничение области поиска
query:set_byte_range(0, 1000)
query:set_point_range({row = 0, column = 0}, {row = 10, column = 0})
-- Ограничение количества совпадений
query:set_match_limit(100)
if query:did_exceed_match_limit() then
-- Есть ещё совпадения
end
-- Тайм-аут (строка или наносекунды)
query:set_timeout("500ms")
query:set_timeout(1000000000) -- 1 секунда
-- Отключение шаблонов и захватов
query:disable_pattern(0)
query:disable_capture("func_name")
Информация о запросе
local pattern_count = query:pattern_count()
local capture_count = query:capture_count()
local name = query:capture_name_for_id(0)
local id = query:capture_index_for_name("func_name")
Курсор
Эффективный обход дерева без создания объектов узлов на каждом шаге.
Базовый обход
local cursor = tree:walk()
-- Начинаем с корня
print(cursor:current_node():kind()) -- "source_file"
print(cursor:current_depth()) -- 0
-- Навигация
if cursor:goto_first_child() then
print(cursor:current_node():kind())
print(cursor:current_depth()) -- 1
end
if cursor:goto_next_sibling() then
-- перешли к следующему соседу
end
cursor:goto_parent() -- назад к родителю
cursor:close()
Методы курсора
| Метод | Возвращает | Описание |
|---|---|---|
current_node() |
Node |
Текущий узел |
current_depth() |
integer |
Глубина (0 = корень) |
current_field_name() |
string? |
Имя поля, если есть |
goto_parent() |
boolean |
Перейти к родителю |
goto_first_child() |
boolean |
Перейти к первому потомку |
goto_last_child() |
boolean |
Перейти к последнему потомку |
goto_next_sibling() |
boolean |
Перейти к следующему соседу |
goto_previous_sibling() |
boolean |
Перейти к предыдущему соседу |
goto_first_child_for_byte(n) |
integer? |
Перейти к потомку, содержащему байт |
goto_first_child_for_point(pt) |
integer? |
Перейти к потомку, содержащему точку |
reset(node) |
- | Сбросить курсор на узел |
copy() |
Cursor |
Создать копию курсора |
close() |
- | Освободить ресурсы |
Метаданные языка
local lang = treesitter.language("go")
print(lang:version()) -- версия ABI
print(lang:node_kind_count()) -- количество типов узлов
print(lang:field_count()) -- количество полей
-- Поиск типов узлов
local kind = lang:node_kind_for_id(1)
local id = lang:id_for_node_kind("identifier", true)
local is_named = lang:node_kind_is_named(1)
-- Поиск полей
local field_name = lang:field_name_for_id(1)
local field_id = lang:field_id_for_name("name")
Ошибки
| Ситуация | Тип | Повтор |
|---|---|---|
| Язык не поддерживается | errors.INVALID |
нет |
| Нет биндинга для языка | errors.INVALID |
нет |
| Некорректный шаблон запроса | errors.INVALID |
нет |
| Некорректные позиции | errors.INVALID |
нет |
| Ошибка парсинга | errors.INTERNAL |
нет |
Подробнее см. Обработка ошибок.
Синтаксис запросов
Запросы Tree-sitter используют S-выражения:
; Совпадение с типом узла
(identifier)
; Совпадение с указанием полей
(function_declaration name: (identifier))
; Захват с именем @name
(function_declaration name: (identifier) @func_name)
; Несколько шаблонов
[
(function_declaration)
(method_declaration)
] @declaration
; Подстановочные знаки
(_) ; любой узел
(identifier)+ ; один или более
(identifier)* ; ноль или более
(identifier)? ; опционально
; Предикаты
((identifier) @var
(#match? @var "^_")) ; совпадение по регулярке
Полная документация: Tree-sitter Query Syntax.