В почти каждой команде рано или поздно вспыхивает спор о точках с запятой. Pull request на четыреста строк логики собирает сорок замечаний, и половина из них про кавычки, отступы и пустые строки между методами. Автор обижен, ревьюер вымотан, а обсуждение самих изменений начинается к вечеру вторника. Попытка переубедить оппонента здесь бесполезна: у вкуса нет объективной правоты, есть только привычка. Проблема решается не силой аргументов, а переносом вопроса из людей в инструменты. Автоматическая проверка забирает себе всё, где ответ известен заранее, и оставляет людям самое интересное: смысл изменений.
Причины по которым споры о стиле кода не заканчиваются никогда
Споры о стиле живучи, потому что маскируются под заботу о качестве. За позицией "нужны точки с запятой" чаще всего стоит привычка из прошлой команды или смутное ощущение собственности на кодовую базу. Разработчик годами писал в одном стиле, перечитывает чужой код глазами и почти физически ощущает "неправильное" как ошибку. Убедить человека в таком состоянии почти невозможно, да и незачем.
Удивительно другое: с точки зрения результата разница между одинарными и двойными кавычками равна нулю. Ни скорость сборки, ни количество инцидентов, ни удобство отладки от неё не зависят. Зависит только единообразие. Когда все файлы выглядят одинаково, diff сводится к содержательным строкам, и глаз цепляется за реальные изменения, а не за переставленные скобки. Каждый, кто открывал pull request с двумя сотнями строк переформатирования и десятью строками настоящей правки, знает это ощущение: нужное прячется как игла в стоге. Спорить имеет смысл о том, влияет ли сравнение через два знака равенства на поведение программы. Первый вопрос проверяется тестами, второй только чьей-то вкусовой палитрой.
Ошибки которые линтер ловит задолго до ревью
Настоящая сила линтера не в пробелах. Правила из базовых наборов находят ошибки, за которые в проде бывает больно. Классика JavaScript:
function findUser(list, id) {
for (var i = 0; i < list.length; i++) {
if (list[i].id == id) {
return list[i];
}
}
return null;
}
Здесь сразу несколько тревожных мест. Объявление через var выносит переменную за пределы цикла, а нестрогое сравнение ловит странности приведения типов:
console.log(0 == ""); // true
console.log(0 == "0"); // false
console.log(null == undefined); // true
Три выражения, и логика в каждом отличается от интуиции новичка. Правило eqeqeq запрещает нестрогие сравнения, no-unused-vars находит потерянные переменные, no-fallthrough ловит провал между ветками switch, prefer-const подсказывает там, где вместо const зря написан let. Эти правила не про красоту, они про поведение программы.
Так выглядит отчёт линтера на живом файле:
src/utils.js:12:5 warning 'retries' is assigned a value but never used no-unused-vars
src/utils.js:18:3 error Expected '===' and instead saw '==' eqeqeq
Формат простой: файл, строка, позиция, серьёзность, текст нарушения, имя правила. По такому отчёту разработчик чинит файл за минуту, до ревью и до тестов, а на конвейере эти строки превращаются в стоп-сигнал без участия человека.
В других экосистемах картина та же. В Python линтер и форматтер Ruff написан на Rust, его авторы заявляют ускорение в десятки и сотни раз против прежних утилит Flake8 и Black, а число встроенных правил перевалило за девятьсот. В Go вопрос закрыт стандартным gofmt: команда gofmt -l . просто перечисляет файлы, отформатированные не по образцу, и спорить в этой экосистеме не о чем. В Rust аналогичную роль играет cargo fmt --check, в Java давно живёт семейство Checkstyle. Гайд по стилю превращается из документа на двадцать страниц в исполняемую проверку.
Отдельный пласт правил связан с безопасностью и надёжностью. Запрет динамической компиляции кода из строк, контроль устаревших интерфейсов, ловля присваивания переменной самой себе: всё это проверяется статически, без запуска программы. Одна включённая группа правил безопасности стоит вечера настройки и экономит расследования, которые иначе начинаются с вопроса "а кто это написал и когда".
Разделение труда между форматтером линтером и проверкой типов
Инструменты стоит разводить по зонам ответственности. Форматтер (Prettier, gofmt, ruff format) перепечатывает файл по единому образцу и почти ничего не позволяет настраивать: его создатели прямо позиционируют инструмент как способ закончить споры о стиле, потому что решения принимает он сам. Линтер (ESLint, Ruff) проверяет паттерны, за которыми стоят ошибки. Проверка типов (tsc, mypy) смотрит на совместимость значений и функций. Конфликта между зонами быть не должно, иначе инструменты начнут бороться за одну и ту же строку.
Работа форматтера выглядит скромно:
const config={port:8080,host:"localhost"}
let retries=3
после запуска превращается в аккуратный вариант с точками с запятой, пробелами и переносами. Вручную такое выравнивание стоило бы автору пяти минут и ревьюеру трёх замечаний, машине оно даётся мгновенно и бесплатно.
Конфиг ESLint с девятой версии описывается файлом eslint.config.js, в котором каждое правило получает уровень строгости:
import tseslint from "typescript-eslint";
export default tseslint.config(
...tseslint.configs.recommended,
{
rules: {
"no-unused-vars": "error",
"eqeqeq": ["error", "smart"],
"prefer-const": "warn"
}
}
);
Уровень error ломает сборку, warn только предупреждает. Это и есть рычаг, который гасит конфликты: спорное правило включается предупреждением, опасное ошибкой, а личные вкусы выносятся в форматтер, с которым не спорят. Чтобы ESLint не сражался с Prettier за одни и те же пробелы, подключают пакет eslint-config-prettier, отключающий стилистические правила. Один инструмент, одна зона ответственности, ноль пересечений.
На практике границу проверяют последовательностью запуска: сначала форматтер, потом линтер с автоисправлением. Форматтер выравнивает файл, линтер подчищает то, что относится к ошибкам, и в отчёте остаётся только то, что требует человеческого решения. Если инструменты начинают править одну и ту же строку по-разному, это сигнал о пересекающихся конфигурациях, и решается он отключением дублирующих правил, а не договорённостью аккуратнее.
Автоматизация проверки до ревью через хуки и конвейер сборки
Даже идеальный конфиг бесполезен, если он запускается вручную по настроению. Базовый уровень автоматизации в JavaScript-проектах выглядит так:
{
"scripts": {
"lint": "eslint . --max-warnings 0",
"format": "prettier --write ."
},
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"]
}
}
Хук husky вызывает lint-staged перед каждым коммитом, и правки вносятся без участия человека. В Python тот же эффект даёт утилита pre-commit с локальными хуками:
repos:
- repo: local
hooks:
- id: ruff-check
name: ruff check
entry: ruff check --fix
language: system
files: \\.py$
- id: ruff-format
name: ruff format
entry: ruff format
language: system
files: \\.py$
Локальный хук обходится флагом --no-verify, и опытные разработчики так и делают в пятницу вечером. Поэтому точка принуждения обязана стоять в конвейере сборки: шаг npm run lint отклоняет ветку независимо от чьих-то привычек. Красная сборка вместо замечания от коллеги меняет психологию: спорить с человеком можно бесконечно, с конвейером бессмысленно. Через пару недель поток стилистических комментариев в ревью иссякает сам, потому что они физически не доходят до ревьюера.
В больших репозиториях полную проверку дерева стоит ускорять кэшем: инструменты помнят, какие файлы уже проверены и не менялись, и повторно смотрят только изменившиеся. Для репозиториев с десятками пакетов работает выборка по затронутым каталогам. Без этих ухищрений шаг линтера съедает минуты конвейера, команда начинает его пропускать, а пропущенный шаг возвращает споры обратно в ревью.
Порядок внедрения линтера в проект с накопленным наследием
В действующий проект линтер нельзя включать рубильником, иначе конвейер останется красным на месяц, а команда начнёт обходить проверку. Рабочая последовательность выглядит так:
- запустить линтер в режиме отчёта без блокировок и оценить реальный масштаб нарушений;
- выбрать базовый набор правил и сразу ослабить спорные до уровня предупреждений;
- зафиксировать текущее состояние в списке исключений, чтобы новые файлы проверялись строго;
- включить проверку в конвейер сборки для изменённых строк, а не для всего дерева;
- закрывать позиции списка исключений постепенно, по мере доработки старых модулей;
- пересматривать конфиг раз в квартал общим решением, а не в пылу спора в ревью.
Шаг со списком исключений самый непопулярный и самый важный: он снимает необходимость в большом переписывании за один вечер и оставляет коду шанс поправиться постепенно. Отдельного упоминания заслуживают лазейки: инструкции вида eslint-disable-next-line и пометки noqa полезны, когда правило явно не подходит конкретной строке, но их число стоит держать на виду. Конфиг с сотней подавленных предупреждений в одном модуле сообщает о проблеме в модуле громче, чем любой отчёт.
Числа здесь работают лучше запретов. Если в проекте четыреста нарушений, команда фиксирует потолок и требует, чтобы каждая неделя его опускала: минус десять, минус двадцать. Через квартал наследие исчезает без единого выделенного спринта, и никто не сможет назвать день, когда именно оно закончилось.
Разумно начинать с пилотного модуля: включить строгий набор на одном небольшом пакете, показать результат на общей встрече и распространить на остальные. Пилот снимает страх: вместо абстрактной угрозы для всего репозитория команда видит аккуратный пример, где после включения правил не сломалось ничего, кроме привычки.
Вопросы которые остаются в ревью после передачи стиля машине
Когда машина забрала кавычки и отступы, у ревьюера освобождается внимание для вопросов, которых не решает ни один конфиг. Насколько точное имя у переменной currentUserSession, если по факту в ней лежит просроченный токен. Что случится с очередью задач при падении воркера посреди батча. Покрывает ли новый тест граничный случай с пустым списком. Не упрётся ли реализация в лимит внешнего API на сто запросов в секунду. Именно за такими замечаниями ревью и существует, а за точкой с запятой не стоит ничего.
Замечание из разряда "поставь пробел" сменяется вопросом "объясни, зачем здесь второй флаг". Разница принципиальная: второе развивает автора, первое только тратит его терпение. Команды, прошедшие автоматизацию, замечают приятный эффект: средняя длина ревью падает, потому что мелочь ловится до отправки изменений, а остаток требует вдумчивого чтения. Ревью перестаёт быть полем брани за эстетику и становится разговором о программе.
Честные границы пользы линтеров и вывод для команды
Линтер не делает код правильным. Он не заменит обсуждение архитектуры, тесты и здравый смысл. Бывает и вред: конфиг из четырёхсот правил, скопированный из чужого проекта без понимания, превращает разработку в спорт по расстановке подавляющих пометок. Правило ценно тогда, когда за ним видна история конкретной ошибки команды, а не ссылка на стиль из модного фреймворка. Полезен и размер: набор, который команда может пересказать своими словами, работает лучше энциклопедии.
Зрелые команды выносят конфиг в отдельный пакет и подключают его во всех репозиториях: обновление правил доезжает до всех проектов одним повышением версии. Это дисциплинирует и авторов правил: изменение, которое ломает сотню сборок сразу, не пройдёт незамеченным и быстро будет откачено.
Смысл автоматической проверки шире красоты кода. Она переводит конфликт из личной плоскости в техническую, где спорить не с кем: сборка либо зелёная, либо нет. Стандарт кода нужен не затем, чтобы все писали одинаково, а затем, чтобы об одинаковости вообще не приходилось говорить. Освобождённое от споров о пробелах ревью возвращает разработке главное: разговор о том, что программа на самом деле делает.