Files
transcriber/.golangci.yml
T
av b01bdabc37 снят мёртвый godotenv.Load() вместе с зависимостью
- переменных окружения наш рабочий код больше не читает нигде: ушли вызов, импорт и предупреждение на старте
- у оставленной строки .env в .gitignore и у запрета forbidigo переписаны причины: прежние называли живым снесённого читателя
- в документы записано, что барьеры против .env стоят без читателя, и правило парности .gitignore с .dockerignore
2026-08-23 21:06:30 +03:00

214 lines
16 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Линтеры проекта. Перечень правил и их дома — 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
# Отмена доходит до внешнего вызова: запрос и внешний процесс заводятся с
# контекстом. Инвариант «принятая запись не теряется молча» держится
# остановкой на середине, а не только записью в лог: `ffmpeg`, заведённый
# без контекста, переживает остановку воркера и дожёвывает чужую запись.
- noctx
# Контекст приезжает сверху, а не заводится по месту. `context.Background()`
# внутри адаптера обрывает цепочку отмены ровно на границе с платным
# внешним сервисом — там, где отмена и нужна.
- contextcheck
# Тело ответа закрывается. `errcheck` его не видит: `(io.ReadCloser).Close`
# объявлен в `exclude-functions` ниже, и незакрытое тело от невыясненного
# `Close` этим списком не отличается.
- bodyclose
# `return nil` после проверенной ошибки — это молчаливая потеря отказа,
# прямо запрещённая инвариантом об очереди (CLAUDE.md, major).
- nilerr
# Отказ выборки не теряется: неспрошенный `rows.Err()` превращает оборванное
# чтение в пустой результат.
- rowserrcheck
# `Rows` и `Stmt` закрываются: незакрытая выборка держит соединение.
- sqlclosecheck
# Форма утверждений в проверках: перепутанные местами «ожидалось/получено»,
# `assert` там, где после провала продолжать нельзя, `require` из горутины.
- testifylint
# Подавление — это решение: строчное `//nolint` обязано называть линтер и
# причину, а протухшее подавление обязано краснеть. Тот же порядок, что у
# подавлений в этом файле, но применённый к комментариям в коде.
- nolintlint
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`
# одной правкой. Наш рабочий код окружение не читает вовсе — это
# правило и держит.
#
# Чего правило не ловит: `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)'
nolintlint:
# Подавление без причины снимают при первом же неудобстве: снимающий не
# знает, что оно ловило. Те же два требования, что у подавлений в этом
# файле, — имя линтера и причина строкой.
require-explanation: true
require-specific: true
# Подавление, которому нечего подавлять, — след починенного места, и
# краснеть оно обязано: иначе перечень подавлений врёт.
allow-unused: false
errcheck:
# Без этого `_ = x.Close()` снимает замечание, и критерий «отказ не
# теряется молча» принимается реализацией, которая его теряет. Отказ,
# который решено не проверять, теперь объявляют ниже поимённо — заметно.
check-blank: true
# Непроверенное приведение типа паникует, а не отдаёт ошибку, поэтому
# `check-blank` его не ловит: `v := x.(T)` вовсе не про присваивание в `_`.
check-type-assertions: true
exclude-functions:
# Закрытие через defer и лучшая-попытка уборки файла — осознанно без проверки
- (io.Closer).Close
- (*database/sql.DB).Close
- (*os.File).Close
- (io.ReadCloser).Close
- os.Remove
# Закрытие выборки отложенным вызовом: строки к этому моменту прочитаны,
# а их отказ уже спрошен у `rows.Err()` — отдельного смысла у отказа
# закрытия нет.
- (*database/sql.Rows).Close
# Откат транзакции отложенным вызовом. Успешно завершённая транзакция
# отвечает на него «уже закончена», и проверка этого отказа означала бы
# разбор штатного исхода.
- (*database/sql.Tx).Rollback
# Запись тела ответа. Отказ здесь значит оборванное соединение, и
# сказать о нём некому: код ответа уже ушёл, а строка о каждом закрытом
# браузере наполняла бы журнал ничем.
- (*encoding/json.Encoder).Encode
- (net/http.ResponseWriter).Write
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'
# `httptest.NewRequest` строит фикстуру для обработчика в том же процессе:
# внешнего собеседника за ней нет, и отменять у неё нечего — правило здесь
# говорит не о том, что мы имели в виду. Изъятие названо по имени этой
# функции, а не выключением `noctx` на проверках целиком: настоящий внешний
# вызов из проверки — `http.Get`, `exec.Command` — правилу по-прежнему
# подсуден.
- linters:
- noctx
path: '_test\.go$'
text: 'httptest\.NewRequest'
formatters:
enable:
- gofmt