В почти каждой команде рано или поздно вспыхивает спор о точках с запятой. 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 отклоняет ветку независимо от чьих-то привычек. Красная сборка вместо замечания от коллеги меняет психологию: спорить с человеком можно бесконечно, с конвейером бессмысленно. Через пару недель поток стилистических комментариев в ревью иссякает сам, потому что они физически не доходят до ревьюера.

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

Порядок внедрения линтера в проект с накопленным наследием

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

  1. запустить линтер в режиме отчёта без блокировок и оценить реальный масштаб нарушений;
  2. выбрать базовый набор правил и сразу ослабить спорные до уровня предупреждений;
  3. зафиксировать текущее состояние в списке исключений, чтобы новые файлы проверялись строго;
  4. включить проверку в конвейер сборки для изменённых строк, а не для всего дерева;
  5. закрывать позиции списка исключений постепенно, по мере доработки старых модулей;
  6. пересматривать конфиг раз в квартал общим решением, а не в пылу спора в ревью.

Шаг со списком исключений самый непопулярный и самый важный: он снимает необходимость в большом переписывании за один вечер и оставляет коду шанс поправиться постепенно. Отдельного упоминания заслуживают лазейки: инструкции вида eslint-disable-next-line и пометки noqa полезны, когда правило явно не подходит конкретной строке, но их число стоит держать на виду. Конфиг с сотней подавленных предупреждений в одном модуле сообщает о проблеме в модуле громче, чем любой отчёт.

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

Разумно начинать с пилотного модуля: включить строгий набор на одном небольшом пакете, показать результат на общей встрече и распространить на остальные. Пилот снимает страх: вместо абстрактной угрозы для всего репозитория команда видит аккуратный пример, где после включения правил не сломалось ничего, кроме привычки.

Вопросы которые остаются в ревью после передачи стиля машине

Когда машина забрала кавычки и отступы, у ревьюера освобождается внимание для вопросов, которых не решает ни один конфиг. Насколько точное имя у переменной currentUserSession, если по факту в ней лежит просроченный токен. Что случится с очередью задач при падении воркера посреди батча. Покрывает ли новый тест граничный случай с пустым списком. Не упрётся ли реализация в лимит внешнего API на сто запросов в секунду. Именно за такими замечаниями ревью и существует, а за точкой с запятой не стоит ничего.

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

Честные границы пользы линтеров и вывод для команды

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

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

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