- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
201 lines
15 KiB
YAML
201 lines
15 KiB
YAML
# Линтеры проекта. Перечень правил и их дома — 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`
|
||
# одной правкой. Окружение читает только 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)'
|
||
|
||
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
|
||
|
||
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
|