Как Tree-sitter превращает код в структуру и факты. Пишем рекурсивный обход дерева на Python. Узнайте, как анализировать код глубже текста.
Вы когда-нибудь искали строку @Controller в большом проекте, но не могли понять, это декоратор, комментарий или часть строки? Обычный текстовый поиск не различает контекст. А вот Tree-sitter — парсер, который строит синтаксическое дерево (AST) и превращает код в структурированные факты. В этой статье мы разберём, как использовать Tree-sitter на Python, чтобы извлекать из кода не просто строки, а осмысленные данные.
Для начала установим Python-библиотеку Tree-sitter и языковой пакет для TypeScript:
pip install tree-sitter tree-sitter-typescriptТеперь напишем скрипт, который парсит файл и выводит дерево разбора. Возьмём небольшой NestJS-контроллер, чтобы было наглядно:
from tree_sitter import Language, Parser
from tree_sitter_languages import get_parser
# Загружаем парсер для TypeScript
parser = get_parser('typescript')
# Исходный код контроллера
code = '''
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { OrdersService } from './orders.service';
@Controller('orders')
@UseGuards(AuthGuard)
export class OrdersController {
constructor(private readonly service: OrdersService) {}
@Get(':id')
findOne() {
return this.service.findOne();
}
}
'''
# Парсим код в дерево
tree = parser.parse(bytes(code, 'utf8'))
root = tree.root_node
# Рекурсивно печатаем дерево
def print_tree(node, indent=0):
print(' ' * indent + node.type, end='')
if node.child_count == 0 and node.type != 'comment':
print(f' = {code[node.start_byte:node.end_byte]}', end='')
print()
for child in node.children:
print_tree(child, indent + 2)
print_tree(root)Этот код выведет огромное вложенное дерево, в котором будут узлы import_statement, class_declaration, decorator и другие. Но читать это неудобно. Давайте напишем более компактный обходчик, который покажет только ключевые узлы.
Вместо печати всего дерева, мы напишем функцию, которая рекурсивно обходит узлы и собирает информацию о структуре кода. Нам интересны классы, декораторы и их аргументы.
def extract_facts(node, facts):
# Если узел — объявление класса
if node.type == 'class_declaration':
class_name = None
for child in node.children:
if child.type == 'type_identifier':
class_name = code[child.start_byte:child.end_byte]
break
facts.append({'type': 'class', 'name': class_name})
# Если узел — декоратор
if node.type == 'decorator':
decorator_info = extract_decorator(node)
facts.append(decorator_info)
# Рекурсивно обходим детей
for child in node.children:
extract_facts(child, facts)
def extract_decorator(node):
# Ищем имя функции-декоратора (например, Controller)
name = None
args = []
for child in node.children:
if child.type == 'call_expression':
# Внутри call_expression ищем identifier и аргументы
for sub in child.children:
if sub.type == 'identifier':
name = code[sub.start_byte:sub.end_byte]
elif sub.type == 'arguments':
for arg in sub.children:
if arg.type == 'string':
args.append(code[arg.start_byte:arg.end_byte])
return {'type': 'decorator', 'name': name, 'arguments': args}
facts = []
extract_facts(root, facts)
for fact in facts:
print(fact)Вывод будет примерно таким:
{'type': 'class', 'name': 'OrdersController'}
{'type': 'decorator', 'name': 'Controller', 'arguments': ["'orders'"]}
{'type': 'decorator', 'name': 'UseGuards', 'arguments': ['AuthGuard']}Теперь мы имеем не просто текст, а структурированные данные. Это уже можно использовать для анализа.
Текстовый поиск ищет подстроку, но не понимает её роль. Например, строка @Controller может быть декоратором, а может встретиться в комментарии или строке. Анализ с помощью AST даёт контекст: мы точно знаем, что это узел decorator с именем Controller. Это ключевое отличие.
Предположим, мы хотим найти все классы, помеченные как @Controller, и их маршруты @Get. Мы можем расширить наш анализатор:
def analyze_controller(node, controller_info):
if node.type == 'class_declaration':
# Проверяем, есть ли декоратор Controller
for child in node.children:
if child.type == 'decorator':
deco = extract_decorator(child)
if deco['name'] == 'Controller':
controller_name = None
for sub in node.children:
if sub.type == 'type_identifier':
controller_name = code[sub.start_byte:sub.end_byte]
controller_info.append({'name': controller_name, 'route': deco['arguments']})
for child in node.children:
analyze_controller(child, controller_info)
controller_info = []
analyze_controller(root, controller_info)
print(controller_info)Вывод:
[{'name': 'OrdersController', 'route': ["'orders'"]}]Теперь мы можем автоматически построить список маршрутов приложения.
Важно понимать: Tree-sitter понимает только синтаксис, но не семантику. Если вы напишете @SomethingThatDoesNotExist(), Tree-sitter всё равно распознает это как декоратор. Он не знает, существует ли такой декоратор в NestJS или скомпилируется ли код. Это разделение полезно: парсинг говорит нам, что код структурно собой представляет, а другие инструменты (компилятор TypeScript, правила статического анализа) определяют, что эта структура означает.
Попробуйте запустить приведённый код на своём проекте. Начните с малого: извлеките все классы и декораторы из одного файла. Затем попробуйте расширить анализ: найдите все методы с декоратором @Get и постройте карту маршрутов. Это первый шаг к созданию собственного анализатора кода.
Tree-sitter — это фундамент для редакторов, навигации по коду и статических анализаторов. Освоив его, вы сможете создавать инструменты, которые понимают код глубже, чем простой текстовый поиск.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →