Зачем писать свой линтер, когда есть готовые?
На первый взгляд идея написать линтер с нуля может показаться избыточной. Современные инструменты вроде 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, может сэкономить десятки часов ревью и багфиксов. А через год вы уже будете поддерживать полноценный инструмент, который знает ваш код лучше любого разработчика.



