# Линтеры проекта. Перечень правил и их дома — docs/conventions/go-linters.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