Как написать линтер для кода с нуля и автоматизировать проверку стиля программирования

Зачем писать свой линтер, когда есть готовые?

На первый взгляд идея написать линтер с нуля может показаться избыточной. Современные инструменты вроде ESLint, Pylint или RuboCop покрывают подавляющее большинство сценариев. Однако в реальном проекте часто возникают ситуации, когда стандартные проверки не учитывают специфики архитектуры, бизнес-логики или командных соглашений. Именно в таких случаях создание линтера для кода становится не капризом, а необходимостью.

Например, в одном из проектов с микросервисной архитектурой мы столкнулись с проблемой: разработчики обращались к внутренним API напрямую, минуя шлюзы. Ни один стандартный линтер не мог это отследить. Решение — разработка собственного линтера, проверяющего импорты и зависимости между модулями на уровне AST (Abstract Syntax Tree).

Первый шаг — определить цель линтинга

Перед тем как сделать линтер для кода, важно задать себе конкретный вопрос: что именно мы хотим контролировать? Это может быть любой аспект — от стиля форматирования до архитектурных ограничений. Ошибка многих начинающих разработчиков линтеров — пытаться обобщить всё сразу. Лучше начать с одной-двух специфичных проверок, глубоко относящихся к вашему проекту.

В нашей практике мы начали с простой проверки: запрет на использование определённых методов ORM вне слоя репозиториев. Это позволило избежать размытия бизнес-логики и упростило сопровождение кода. Сначала проверка была реализована как скрипт на Python, позже мы оформили её как полноценный плагин для Flake8.

Техническая реализация: разбираем AST

Линтеры, работающие на уровне синтаксического дерева, дают большую гибкость. Вместо поверхностного анализа строк можно точно определить, какие конструкции использует программист. Например, в Python модуль `ast` позволяет разбирать код в дерево, проходить его и находить нужные узлы.

```python
import ast

class ForbiddenCallChecker(ast.NodeVisitor):
def visit_Call(self, node):
if isinstance(node.func, ast.Attribute) and node.func.attr == 'raw_sql':
print(f"Запрещённый вызов raw_sql на строке {node.lineno}")
self.generic_visit(node)

tree = ast.parse(open("target_file.py").read())
ForbiddenCallChecker().visit(tree)
```

Такой подход позволяет реализовать даже сложные проверки, например, запрет на использование глобальных переменных или обращение к неразрешённым сервисам.

Интеграция в CI/CD: зачем линтер без автоматизации?

Разработка собственного линтера — лишь половина дела. Чтобы он приносил пользу, его нужно встроить в процесс разработки. В идеале — интегрировать в пайплайн CI/CD. Мы используем GitHub Actions и проверяем код перед мержем в основную ветку. Это позволяет не просто находить ошибки, а предотвращать их попадание в продакшн.

Для этого линтер оформляется как CLI-инструмент с кодом завершения. Например, `exit(1)` при ошибке. Такой подход позволяет линтеру участвовать в автоматических проверках наравне с тестами и сборкой.

Нестандартные решения: за пределами синтаксиса

Если вы решили написать линтер с нуля, не ограничивайтесь только синтаксисом. Например, можно анализировать логику зависимостей между модулями. В одном проекте мы заметили, что микросервисы начинали зависеть друг от друга циклически. Это приводило к ужасным багам при билде.

Мы реализовали линтер, который строил граф зависимостей между модулями и проверял его на наличие циклов. Использовали NetworkX для построения графа и DFS для обнаружения циклических путей. Такой линтер не просто проверял стиль, а предотвращал архитектурное гниение.

Инструменты для разработки линтера: что использовать на старте

Как написать свой линтер для кода - иллюстрация

В зависимости от языка, набор инструментов может отличаться. В Python есть модули `ast`, `lib2to3`, `typed_ast`. Для JavaScript — `eslint` и `babel-parser`. В Java — `Error Prone` и `Checkstyle`. Однако не стоит ограничиваться только стандартными инструментами для разработки линтера. Например, в TypeScript мы использовали собственный транслятор на базе `ts-morph`, который позволял анализировать типы и связи между файлами.

Вот ещё один пример нестандартного инструмента — `tree-sitter`. Это универсальный парсер с поддержкой многих языков, который позволяет строить синтаксические деревья и использовать их для анализа. Отличный выбор, если вы хотите сделать кросс-языковой линтер.

Масштабирование: от одной проверки к платформе

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

Мы рекомендовали бы использовать архитектуру плагинов. Каждый новый тип проверки оформляется как отдельный модуль с единым интерфейсом. Это позволяет подключать и отключать проверки по мере необходимости, не трогая основное ядро линтера. Подобный подход используется в Flake8, ESLint и других зрелых инструментах.

Заключение: писать свой линтер — это больше, чем просто проверка

Как написать свой линтер для кода - иллюстрация

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

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

Прокрутить вверх