- включены sloglint, misspell, depguard; forbidigo получил запреты на чтение времени, окружения и вывод в stdout — каждый по свойству, а не по одному имени: запрет на os.Getenv без соседей обходится os.LookupEnv - заведён internal/archrules — тесты-сканеры: направление зависимостей между ядром, транспортами и адаптерами, узнавание ошибки по тексту в шести формах, и согласованность колонок очереди во всех четырёх местах плюс шаг схемы - каждое правило проверено мутацией, каждое исключение объявлено с причиной
152 lines
11 KiB
YAML
152 lines
11 KiB
YAML
# Линтеры проекта. Перечень правил и их дома — docs/autotests.md,
|
||
# «Механизировано»; здесь только настройка и «почему именно так».
|
||
#
|
||
# Базовый набор v2 (`default: standard`) — errcheck, govet, ineffassign,
|
||
# staticcheck, unused. Сверх него включено то, что механизирует конвенции: то,
|
||
# что проверяет правило, прозой в конвенциях не остаётся.
|
||
version: "2"
|
||
|
||
linters:
|
||
default: standard
|
||
enable:
|
||
# docs/conventions/errors.md: сравнение ошибок через errors.Is и errors.As.
|
||
- errorlint
|
||
# docs/conventions/errors.md: ошибки — только stdlib.
|
||
- depguard
|
||
# docs/conventions/logging.md: форма вызова slog.
|
||
- sloglint
|
||
# Опечатка в комментарии и в тексте ошибки читается как термин проекта.
|
||
- misspell
|
||
# Запреты по месту: чем судят ответ в проверках, чем читают время, откуда
|
||
# берут конфигурацию, куда пишут вывод. Подробности у каждого правила ниже.
|
||
- forbidigo
|
||
settings:
|
||
forbidigo:
|
||
# `analyze-types` включает суждение по типу приёмника, а не по печатному
|
||
# тексту вызова. Правилу о заголовках это необходимо (см. ниже), прочим
|
||
# правилам не мешает: имена пакетов в шаблонах те же.
|
||
analyze-types: true
|
||
forbid:
|
||
# Вывод идёт в журнал: строка в stdout мимо slog не имеет ни уровня, ни
|
||
# полей, и в разборе постфактум её не найти. Встроенные `print`/`println`
|
||
# названы тем же правилом: запрет на одно имя обходится соседним.
|
||
- pattern: '^fmt\.Print.*$'
|
||
msg: 'пишем через slog, а не в stdout напрямую (docs/conventions/logging.md)'
|
||
- pattern: '^print(ln)?$'
|
||
msg: 'пишем через slog, а не в stdout напрямую (docs/conventions/logging.md)'
|
||
# Конфигурация приезжает из TOML. Перечислены все способы прочитать
|
||
# окружение, а не один: `os.Getenv` без соседей обходится `os.LookupEnv`
|
||
# одной правкой. Окружение читает только godotenv в main.go — он кладёт
|
||
# .env в окружение процесса, а не в настройки.
|
||
#
|
||
# Чего правило не ловит: `fmt.Fprintln(os.Stdout, …)` и
|
||
# `os.Stdout.WriteString` — первый аргумент по имени функции не судится.
|
||
# Этот остаток назван прозой в docs/conventions/logging.md.
|
||
- pattern: '^os\.(Getenv|LookupEnv|Environ|ExpandEnv)$'
|
||
msg: 'конфигурация только из TOML (docs/conventions/config.md)'
|
||
# Единая точка чтения времени — internal/clock: метка времени в UTC
|
||
# (`clock.Now`), измерение длительности с монотонными часами
|
||
# (`clock.Start`). Прежде время брали по месту, и хранилище сравнивало
|
||
# строками времена из разных зон.
|
||
- pattern: '^time\.Now$'
|
||
msg: 'время читают clock.Now (метка) и clock.Start (длительность) — docs/conventions/database.md'
|
||
# Проверка ответа судит по **готовому ответу**, а не по изменяемому
|
||
# состоянию обработчика. `httptest` устроен зеркально настоящему серверу:
|
||
# `Header()` отдаёт живую карту, доступную и после записи ответа, а
|
||
# снимок, который получит клиент, лежит отдельно и читается через
|
||
# `Result()`. Проверка, читающая живую карту, зелена при неработающем
|
||
# коде — класс всплывал трижды (docs/review.md, записи 2026-08-10,
|
||
# 2026-08-11 и 2026-08-12) и трижды стоил зелёного гейта.
|
||
#
|
||
# Правило судит по типу приёмника, и в этом весь смысл: запрет на
|
||
# цепочку `w.Header().Get` обходится одной лишней строкой —
|
||
# `h := w.Header()`, — а также чтением по индексу карты и обходом
|
||
# `range`. По типу под правило попадают все эти формы разом. Текстом его
|
||
# записать нельзя ещё и потому, что `.Header` носят и запрос
|
||
# (`req.Header.Set` в проверках законен), и снимок ответа
|
||
# (`w.Result().Header` — как раз то, к чему правило ведёт).
|
||
#
|
||
# Приёмник назван поимённо: подставной сервер в проверках отдаёт
|
||
# заголовок через `w.Header().Set`, но у него приёмник —
|
||
# `http.ResponseWriter`, и под правило он не попадает.
|
||
- pattern: '^httptest\.ResponseRecorder\.Header$'
|
||
msg: 'проверка судит ответ по живой карте заголовков: читай w.Result().Header'
|
||
# `HeaderMap` — тот же живой снимок прежним именем поля. Правило второе,
|
||
# потому что об устарелости поля говорит `staticcheck` (SA1019), а о том,
|
||
# почему по нему не судят ответ, — только это сообщение.
|
||
- pattern: '^httptest\.ResponseRecorder\.HeaderMap$'
|
||
msg: 'проверка судит ответ по живой карте заголовков: читай w.Result().Header'
|
||
|
||
sloglint:
|
||
# Стиль вызова один — пары «ключ-значение». `kv-only` запрещает атрибуты
|
||
# (`slog.String` и прочие) **целиком**, а не только смешение с парами:
|
||
# смешение и так запрещено умолчанием `no-mixed-args`. Решение осознанное —
|
||
# один стиль на весь код, — и записано строкой в
|
||
# docs/conventions/logging.md, «Сообщение».
|
||
no-mixed-args: true
|
||
kv-only: true
|
||
# `msg` — константа: сообщение с подставленным значением не сгруппировать
|
||
# отбором, а данные для этого и кладут в поля.
|
||
static-msg: true
|
||
# `key-naming-case` не включаем: словарь полей намеренно смешанный —
|
||
# доменные поля `snake_case`, системные домены с точкой (`http.method`,
|
||
# `ext.service`). См. docs/conventions/logging.md, «Поля: словарь имён».
|
||
|
||
depguard:
|
||
rules:
|
||
main:
|
||
deny:
|
||
- pkg: github.com/pkg/errors
|
||
desc: 'ошибки — только stdlib errors и fmt.Errorf (docs/conventions/errors.md)'
|
||
- pkg: github.com/cockroachdb/errors
|
||
desc: 'стек-трейс избыточен, контекст несёт цепочка %w (docs/conventions/errors.md)'
|
||
|
||
errcheck:
|
||
# Без этого `_ = x.Close()` снимает замечание, и критерий «отказ не
|
||
# теряется молча» принимается реализацией, которая его теряет. Отказ,
|
||
# который решено не проверять, теперь объявляют ниже поимённо — заметно.
|
||
check-blank: true
|
||
exclude-functions:
|
||
# Закрытие через defer и лучшая-попытка уборки файла — осознанно без проверки
|
||
- (io.Closer).Close
|
||
- (*database/sql.DB).Close
|
||
- (*os.File).Close
|
||
- (io.ReadCloser).Close
|
||
- os.Remove
|
||
# Метод сам логирует ошибку отправки, вызывающему она не нужна
|
||
- (*git.vakhrushev.me/av/transcriber/internal/controller/tg.TelegramController).send
|
||
|
||
exclusions:
|
||
rules:
|
||
# Правило о заголовках живёт только в файлах проверок: в рабочем коде
|
||
# `Header()` и есть способ отдать заголовок.
|
||
- linters:
|
||
- forbidigo
|
||
path-except: '_test\.go$'
|
||
text: 'живой карте заголовков'
|
||
# Единая точка чтения времени сама читает время — иначе ей нечем.
|
||
- linters:
|
||
- forbidigo
|
||
path: 'internal/clock/'
|
||
text: 'time.Now'
|
||
# Проверка читает окружение **своего прогона** — `PATH`, чтобы убрать из
|
||
# него каталог с `go`, и `os.Environ()`, чтобы передать окружение дочернему
|
||
# процессу. Настройками приложения это не является. Исключение объявлено по
|
||
# тексту сообщения, а не по имени функции: правило называет четыре имени, и
|
||
# исключение обязано покрывать те же четыре.
|
||
- linters:
|
||
- forbidigo
|
||
path: '_test\.go$'
|
||
text: 'конфигурация только из TOML'
|
||
# Проверки строят время фикстур, а не метку домена: `time.Now` в них не
|
||
# обходит единую точку, а задаёт вход. Запрет здесь стоил бы обязательного
|
||
# обряда на каждый срок захвата в фикстуре и не поймал бы ничего.
|
||
- linters:
|
||
- forbidigo
|
||
path: '_test\.go$'
|
||
text: 'time.Now'
|
||
|
||
formatters:
|
||
enable:
|
||
- gofmt
|