Привет, Хабр! Меня зовут Иван, я DevOps‑инженер в ИТ‑холдинге Т1. В современной разработке автоматизация — это давно не «роскошь», а «базовый минимум». Однако многие команды до сих пор ограничиваются лишь простыми возможностями IDE (Integrated Development Environment), упуская потенциал генерации кода. В прошлом году я выступал с этой темой на конференции Импульс Т1. В статье расскажу подробнее, как перейти к генерации кода в масштабах проекта и зачем разбираться в синтаксических деревьях.

Зачем создавать код автоматически

Главная цель генерации кода — не просто сэкономить время разработчика, а обеспечить соответствие кода заданной схеме (например, OpenAPI). Когда генерация настроена правильно, исчезает риск рассинхронизации схемы и реализации, и вносить изменения можно значительно быстрее.

Этот подход применим во многих задачах:

  • создание однотипного кода, часто встречающегося в проекте;

  • создание API‑клиентов из различных схем: OpenAPI, AsyncAPI, gRPC и др.;

  • создание конфигураций (Helm, Ansible) и инфраструктуры;

  • миграция баз данных;

  • стилизация, линтинг, форматирование кодовой базы;

  • автоматический рефакторинг.

Уровни автоматизации: от сниппетов до среды выполнения

Внедрять генерацию кода можно на разных этапах жизненного цикла приложения:

  • IDE и инструменты разработки: самый простой уровень — использование общих сниппетов для генерации повторяющихся методов, файлов, функций и классов. Их поддерживает большинство IDE.

  • Генерация кода при разработке: избавляет от рутинных задач, примеры которых решим в этой статье.

  • Генерация кода для общих модулей: API‑клиенты, схемы сообщений в брокере, схемы баз данных.

  • Генерация кода во время сборки: встраивание URL бэкенда во фронтенд, встраивание ключей для проверки лицензии, Whitelabel‑кастомизация.

  • Генерация кода во время развёртывания: встраивание URL бэкенда во фронтенд, патчинг, создание инфраструктуры.

  • Генерация в среде выполнения: продвинутый уровень, позволяющий менять логику программы на интерпретируемом языке во время выполнения. Также это используют в no‑code и low‑code средах для обфускации или расширения возможностей языка (как, например, в PyTest, который динамически трансформирует код для улучшения отладки).

Как сделать генератор

Первый шаг при создании генератора — выбор источника данных и потребителя кода. В качестве источника данных может быть внешний API, CLI‑аргументы генератора, содержимое базы данных или инспекция существующего кода.

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

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

Воспользуемся конкатенацией. Сначала получим список переводов (получение записано в константу для упрощения примера):

TRANSLATIONS = {
    «hello»: “Привет, {username}!”,
    «welcome»: “Добро пожаловать!”,
    «goodbye»: “Пока, {username}!”,
}

Теперь генерируем код: получаем пары ключ‑значения из словаря, записанного выше. Далее к ключу добавляем префикс MESSAGE и переводим в UPPERCASE, обёртываем значение в кавычки и выводим результат в консоль.

for key, value in TRANSLATIONS.items():
   # MESSAGE_{key.upper()} = "{value}"
   key = "MESSAGE_" + key.upper()
   print(key + ' = "' + value + '"')

Если у вас есть API‑схема с рекурсией, то вы можете расширить метод и добавить рекурсивную генерацию.

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

Масштабируемый подход (шаблонизация)

Если генератор должен поддерживать несколько языков программирования (например, Python и JavaScript) или пользовательские шаблоны, то прямая конкатенация строк становится неудобной. В этом случае правильнее использовать шаблонизаторы (например, Jinja2). Логика работы здесь разделена: генератор отвечает за получение данных из API или файла, а шаблон определяет структуру результирующего кода.

Например, у нас есть такой шаблон:

{%- for key, value in data.items() -%}
MESSAGE_{{ key | upper }} = "{{ value }}"
{% endfor -%}

Загрузим его из константы:

import jinja2

environment = jinja2.Environment()
template = environment.from_string("""
{%- for key, value in data.items() -%}
MESSAGE_{{ key | upper }} = "{{ value }}"
{% endfor -%}
""")

Передадим значения в поле data шаблона и выведем в терминал:

print(template.render(data=TRANSLATIONS))

После запуска генератора получим в терминале результат.

Вы можете хранить шаблоны в отдельных папках или подгружать их из API. Это позволяет использовать один и тот же движок генератора, просто подставляя нужный шаблон в зависимости от того, какой язык требуется пользователю. Такой подход минимизирует дублирование логики и упрощает расширение функциональности. При этом всё также необходимо следить за экранированием, отступами и корректным синтаксисом.

Изменяем уже существующий код

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

Допустим, у нас уже есть некая кодовая база, которую мы хотим автоматически рефакторить, например, заменив все print на logger.info. Если просто найти и заменить поиском, то можно случайно испортить комментарии.

Можно воспользоваться абстрактным синтаксическим деревом (Abstract Syntax Tree, AST). Вот пример на Python:

Можно пройти по дереву и заменить вызовы функции print на logger.info, а затем вывести дерево обратно в терминал.

import ast
from ast import *

code = """
# print result of operation
print("Result:", 1 + 2)
"""
tree = ast.parse(code)

for node in ast.walk(tree):
    if not isinstance(node, Call):
        continue
    if not isinstance(node.func,Name):
        continue
    if node.func.id != "print":
        continue
    node.func = ast.Attribute(
        value=ast.Name(
            id="logger",
            ctx=ast.Load()
        ),
        attr="info",
        ctx=ast.Load(),
    )
print(ast.unparse(ast_obj=tree))

AST позволяет манипулировать кодом, генерировать его с нуля, применять несколько трансформаций подряд, анализировать логику кода и принимать решения. Однако у этих деревьев есть существенный недостаток: они не сохраняют комментарии и специфические особенности форматирования. Код после трансформации становится «чище», но теряет документацию.

Управляем стилем кода

В прошлом примере мы потеряли комментарий из кода. Чтобы этого избежать, можно использовать конкретные синтаксические деревья (Concrete Syntax Tree, CST). Они содержат не только логику, необходимую для запуска кода, но и всю его структуру — от запятых до комментариев. Именно CST позволяют проводить безопасный рефакторинг внутри репозитория без потери читаемости.

Преобразуем код в CST, найдём нужный узел:

и преобразуем дерево:

import libcst as cst
tree = cst.parse_module(                                                                                            
       "# Log result of operation\n"                                                                                   
       "print('Result:', 1 + 2)\n"                                                                                     
)                                                                                                                   
 

class PrintToLogger(cst.CSTTransformer):
    def leave_Name(self, _: cst.Name, node: cst.Name):
        if node.value == "print":
            # Заменяем переменную print на logger.info
            return cst.Attribute(
                value=cst.Name(value="logger"),
                attr=cst.Name(value="info"),
                dot=cst.Dot(),
            )
        return node
transformed_tree = tree.visit(PrintToLogger())
print(transformed_tree.code)

То есть CST обладает всеми возможностями AST, но при этом сохраняет стиль и комментарии.

Будущее: LLM и Model Context Protocol (MCP)

Использование LLM для написания кода выглядит заманчиво, но имеет свои риски: модели могут ошибаться и галлюцинировать, они не имеют прямого доступа к контексту вашего репозитория.

Здесь на помощь приходит Model Context Protocol (MCP). Это протокол, позволяющий моделям взаимодействовать с инструментами разработки: анализировать код проекта, запускать CLI‑команды, получать диагностику от IDE (LSP) и автоматически запускать тесты.

Для того, чтобы сократить количество галлюцинаций LLM в кодовой базе, мы можем предоставить инструменты для генерации кода напрямую языковой модели. Некоторые агенты (например, oh my pi) уже предоставляют инструменты для того, чтобы модель могла менять кодовую базу с помощью AST. Для своей же кодовой базы можно создать скилл агента, прописав логику использования вашего CLI‑инструмента. Альтернативный вариант — реализовать аналогичный функционал через MCP‑сервер, что поможет модели взаимодействовать с кодом и вносить изменения.

С чего начать

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

  • Если у вас есть несколько сервисов, между которыми есть сетевое взаимодействие, то стоит внедрить контракты для этого взаимодействия. В этом поможет OpenAPI‑generator, который создаёт код на множестве языков, позволяет добавлять свои шаблоны и генерировать клиентский и серверный код.

  • Если у вас есть множество микросервисов с аналогичной базой, то поможет Cookiecutter, который создаёт из шаблонов (произвольных) репозитории и проекты (например, микросервисы).

Генерация кода — это мощный инструмент для обеспечения масштабируемости и чистоты кодовой базы. Создание собственных генераторов требует времени и погружения в тему, но вложения окупаются за счёт минимизации рутинных задач и уменьшения количества ошибок. Главное — правильно выбирать инструмент под конкретную задачу, будь то шаблонизаторы, работа с синтаксическими деревьями или современные протоколы для интеграции с ИИ.

Если вы хотите узнать больше о применении генерации кода в реальных проектах, приглашаем вас на конференцию Импульс Т1. 19 ноября в рамках трека «Архитектура и проектирование» мы обсудим, как меняется роль архитектора в эпоху ИИ, как доносить архитектурные решения до разработки и смежных ролей и какие практики лучше работают при построении ИТ‑систем.