# Лінтер

## Що це таке

Лінтер — це програма, яка читає вихідний код і вказує на місця, схожі на
помилки, не збираючи й не запускаючи програму. Кожне зауваження називає правило,
файл і рядок: змінну, якій присвоїли значення й ніде не використали, порівняння,
що завжди істинне, змінну оболонки без лапок, рядок формату, зібраний із
введення користувача.

Назва походить від lint — засобу перевірки програм мовою C, і NetBSD досі
постачає таку утиліту. Її довідка описує завдання як пошук у файлах програм на C
тих особливостей, які ймовірно є помилками, непереносні або марнотратні, і
додає, що типи вона перевіряє суворіше, ніж традиційні компілятори C до C90.
Працює вона у два проходи: спершу кожен файл перевіряється як окрема одиниця,
потім результати звіряються між файлами в пошуках невідповідностей.

Тепер більшість мов мають власні лінтери, часто кілька:

- **ESLint** для JavaScript.
- **Ruff** для Python.
- **ShellCheck** для скриптів оболонки. Його заявлені цілі сягають від
  синтаксичних помилок початківців до тонких пасток, через які робочий скрипт
  може зламатися за майбутніх обставин.
- **Stylelint** для CSS, **markdownlint** для Markdown і **yamllint** для YAML.

Будь-який лінтер складається з правил. Кожне правило має ідентифікатор,
наприклад F841 у Ruff чи SC2059 у ShellCheck, і рівень серйозності. Файл
конфігурації визначає, які правила ввімкнено і наскільки серйозне кожне з них, а
багато правил мають автоматичне виправлення.

Форматувальник переписує оформлення коду й не має думки щодо змісту; засіб
перевірки типів міркує про те, чим є кожне значення. Лінтер стоїть між ними й
дивиться, як код написано: на зловживання можливостями мови, ризиковані шаблони
й угоди, про які домовилися в проєкті.

## Чому це важливо

Лінтер читає кожну гілку коду, зокрема обробник помилок, що спрацьовує раз на
рік і до якого не дістається жоден тест. На весь репозиторій йому вистачає
секунд, тож його можна запускати під час кожного збереження й кожного коміту, а
зауваження найдешевше тоді, коли рядок тільки пишуть.

Правила розв'язують суперечку один раз. Чи прийнятний голий `except:`, чи кожну
змінну оболонки треба брати в лапки — це вирішують у конфігурації, а далі
застосовують однаково до кожного автора й кожного рев'ю. Рецензенти-люди
зберігають увагу для питань, які правило виразити не здатне.

Код виходу — це інтерфейс між лінтером і тим, що його запускає. README
ShellCheck каже, що інструмент використовує коди виходу канонічним способом,
тож збірка може викликати його як ще одну команду. Ruff і ESLint документують
ту саму триступеневу угоду: 0, коли перевірка чиста, 1, коли знайдено
порушення, і 2, коли збій стався в самому інструменті, наприклад через хибну
конфігурацію. Тож ворота якості можуть відрізнити «код неправильний» від
«перевірка зламалася», і обидва випадки мають їх зупинити.

Рівень серйозності визначає, які зауваження впливають на код виходу. ESLint
завершується успішно, коли знаходить лише попередження, якщо `--max-warnings`
не задає межу, а в lint(1) є `-w`, щоб вважати попередження помилками. Проєкт,
який хоче, щоб лінтери блокували помилки, а не лише описували їх, записує цей
вибір туди, де його використовує кожен запуск:

```sh
# кожне зауваження валить запуск, попередження теж
lint -w -abcFghprSxz source.c
eslint --max-warnings 0 src/
ruff check src/
shellcheck scripts/*.sh
```

Цей сайт обрав найсуворіший варіант. Його власні ворота якості вважають кожну
діагностику помилкою й забороняють вбудовані придушення на кшталт `# noqa`, тож
зауваження виправляють у його причині.

## Як це виглядає на практиці

Конфігурація лінтера часто починається з тих правил, які випадково мовчали того
дня, коли її налаштовували. Одна сторінка робить навпаки:
[усі правила лінтера Python увімкнено як помилки](/uk/portfolio/enabled-every-python-linter-rule-as-an-error-147/).
Перший прогін дав 1 815 зауважень. Їх опрацьовували за категоріями, а не за
файлами, бо категорії самі дещо кажуть: невикористані аргументи й затінені
вбудовані імена виявилися шумом, а категорії безпеки, змінюваних значень за
замовчуванням і обробки винятків кожна вказували на реальну поведінку.
Нечисленні винятки мають письмове обґрунтування просто в рядку, а два з них
підкріплені тестом, який перевіряє ту властивість, що її забезпечувало б
вимкнене правило. Поряд у суворому режимі працювала перевірка типів і ловила
інший різновид дефектів: про те, чим є значення, а не як його записано.
Названо й ціну: комусь доводиться ухвалювати рішення 1 815 разів.

Інша сторінка починається з незручного відкриття. Ворота якості сайту,
тридцять два скрипти й 10 242 рядки JavaScript, були найбільшим масивом коду в
репозиторії і єдиним, який не читали ні форматувальник, ні лінтер, ні засіб
перевірки типів. Щоб
[підключити код самих воріт до лінтера](/uk/portfolio/brought-the-quality-gate-code-under-a-linter-143/),
почали з форматувальника й налаштували для цих файлів відступ у два пробіли,
тож змінилися 38 рядків замість 2 173. Далі лінтер дав тринадцять реальних
зауважень, усі виправлені й жодне не придушене, а засіб перевірки типів знайшов
два дефекти, які лінтер пропустив. Пояснення, навіщо це, теж є: дефект у
перевірці проявляється як перевірка, що проходить тоді, коли мала б упасти.

Ідея працює й для тексту. Третя сторінка описує
[лінтер для власної документації репозиторію](/uk/portfolio/put-the-documentation-under-its-own-linter-142/)
з межами, які спричиняють збій, а не лише дають пораду. Серед його восьми
механічних політик — бюджет рядків для кожного файлу, який може лише
зменшуватися, правило, що заявлений статус плану має збігатися з його власними
прапорцями, і перевірка, що кожен шлях і символ коду, згаданий у документі,
існує в дереві. Перший запуск виявив шість планів, позначених виконаними, з
невідміченими прапорцями, чотирнадцять завдань, що перевіряли кожен коміт, але
не згадувалися в жодному документі, три биті посилання, один документ-сироту й
три мертві шляхи.

## Де виникають проблеми

Лінтер рідко ламається гучно. Звичайна вада — запуск, що завершується з кодом 0,
хоча мав би щось зупинити.

**Зауваження, які ніколи не змінюють код виходу.** `--max-warnings` в ESLint за
замовчуванням дорівнює -1, тобто межу вимкнено, а за довідкою lint(1) прохід
перехресної перевірки між файлами завжди завершується успішно. Зауваження, яке
не може провалити запуск, читають один раз, а потім прокручують. Вирішуйте для
кожного правила: або помилка, що валить запуск, або вимкнено, а будь-який
перемикач «попередження як помилки» тримайте в спільній конфігурації.

**Виправлення, яке звітує про успіх.** Ruff завершується з 0, коли кожне
знайдене порушення виправлено автоматично. За робочим столом це зручно, але
ворота, що працюють з увімкненими виправленнями, лагодять лише власну робочу
копію, а коміт, який надійшов, і далі містить кожне порушення. Запускайте
перевірку без виправлень або додайте `--exit-non-zero-on-fix`, щоб виправлення
вважалося збоєм. Пошукайте також `--exit-zero`: з ним Ruff завершується з 0,
навіть коли порушення є, а прапорець, доданий на час одного переходу з купою
порушень, має звичку залишатися.

**Шаблон, що охоплює нуль файлів.** Перейменований каталог чи перенесена
конфігурація можуть залишити лінтер націленим на порожню множину, а перевірка
нуля файлів — чистий результат. ESLint за замовчуванням вважає помилкою glob у
лапках, під який не потрапляє жоден файл. `--no-error-on-unmatched-pattern`
перетворює це на тихий успіх, а `--pass-on-no-patterns` завершується з 0, коли
шаблонів файлів не передано зовсім. Залиште поведінку за замовчуванням і
тримайте один файл із відомим порушенням, про яке ворота мусять повідомити.

**Набір правил, обраний за тим, що мовчало.** Конфігурація, зібрана вимиканням
усього, що спрацювало першого дня, фіксує звички коду, а не стандарт. Почніть
зі свідомого вибору: або все одразу, або запропонована Ruff невелика база з `E`
і `F`, до якої щоразу додають по одній групі, — і запишіть, чому вимкнено кожне
вимкнене правило. Ruff попереджає, що `ALL` неявно вмикає нові правила після
кожного оновлення інструмента, тож зафіксуйте версію лінтера й оновлюйте її
окремою зміною.

**Придушення поширюються по одному рядку.** Кожен лінтер має запасний вихід:
`/* LINTED */` у lint(1), `# noqa: F841` у Ruff,
`# shellcheck disable=SC2059` у ShellCheck і `// eslint-disable-next-line` в
ESLint. Кожен із них фіксує, що саме заглушили, але не чому. Вимагайте
письмового обґрунтування біля кожного винятку, а для важливих вимкніть правило й
додайте тест на властивість, яку воно захищало. Допомагає й тримати винятки в
одному місці: `--no-inline-config` в ESLint робить коментарі вимкнення
недієвими, а ShellCheck читає рядки `disable` для всього проєкту з файлу
`.shellcheckrc`.

## Що почитати далі

- [lint(1)](https://man.netbsd.org/lint.1) — довідка NetBSD до засобу перевірки
  C, від якого походить слово: два проходи, `-w` і коментарі для придушення.
- [The Ruff Linter](https://docs.astral.sh/ruff/linter/) — вибір правил,
  безпечні й небезпечні виправлення, коментарі придушення й коди виходу.
- [Command Line Interface Reference](https://eslint.org/docs/latest/use/command-line-interface)
  — `--max-warnings` в ESLint, вбудована конфігурація, шаблони без збігів і
  коди виходу.
- [ShellCheck](https://github.com/koalaman/shellcheck) — README проєкту: що
  інструмент шукає і як він вписується у збірку.
- [Ignore](https://github.com/koalaman/shellcheck/wiki/Ignore) — способи
  ShellCheck ігнорувати зауваження для рядка, запуску чи цілого проєкту.

<https://engineer.company/uk/glossary/linter/>
