Линтеры и сканеры взяли на себя то, что было прозой конвенций
- включены sloglint, misspell, depguard; forbidigo получил запреты на чтение времени, окружения и вывод в stdout — каждый по свойству, а не по одному имени: запрет на os.Getenv без соседей обходится os.LookupEnv - заведён internal/archrules — тесты-сканеры: направление зависимостей между ядром, транспортами и адаптерами, узнавание ошибки по тексту в шести формах, и согласованность колонок очереди во всех четырёх местах плюс шаг схемы - каждое правило проверено мутацией, каждое исключение объявлено с причиной
This commit is contained in:
+112
-28
@@ -1,44 +1,106 @@
|
||||
# Линтеры проекта. Перечень правил и их дома — 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:
|
||||
# Проверка ответа судит по **готовому ответу**, а не по изменяемому
|
||||
# состоянию обработчика. `httptest` устроен зеркально настоящему серверу:
|
||||
# `Header()` отдаёт живую карту, доступную и после записи ответа, а снимок,
|
||||
# который получит клиент, лежит отдельно и читается через `Result()`.
|
||||
# Проверка, читающая живую карту, зелена при неработающем коде — класс
|
||||
# всплывал трижды (docs/review.md, записи 2026-08-10, 2026-08-11 и
|
||||
# 2026-08-12) и трижды стоил зелёного гейта.
|
||||
#
|
||||
# Правило действует только в `*_test.go` — исключение ниже: в самом
|
||||
# обработчике `Header()` и есть способ отдать заголовок.
|
||||
#
|
||||
# Судит оно **по типу приёмника**, а не по печатному тексту вызова
|
||||
# (`analyze-types`), и в этом весь смысл: запрет на цепочку
|
||||
# `w.Header().Get` обходится одной лишней строкой — `h := w.Header()`, —
|
||||
# а также чтением по индексу карты и обходом `range`. По типу под правило
|
||||
# попадают все эти формы разом. Записать его текстом нельзя ещё и потому,
|
||||
# что `.Header` носят и запрос (`req.Header.Set` в проверках законен), и
|
||||
# снимок ответа (`w.Result().Header` — как раз то, к чему правило ведёт).
|
||||
#
|
||||
# Приёмник назван поимённо: подставной сервер в проверках отдаёт заголовок
|
||||
# через `w.Header().Set`, но у него приёмник — `http.ResponseWriter`, и под
|
||||
# правило он не попадает.
|
||||
#
|
||||
# `HeaderMap` — тот же живой снимок прежним именем поля; правило второе,
|
||||
# потому что о его устарелости говорит `staticcheck` (SA1019), а о том,
|
||||
# почему по нему не судят ответ, — только это сообщение.
|
||||
# `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()` снимает замечание, и критерий «отказ не
|
||||
# теряется молча» принимается реализацией, которая его теряет. Отказ,
|
||||
@@ -56,11 +118,33 @@ linters:
|
||||
|
||||
exclusions:
|
||||
rules:
|
||||
# `path-except` оставляет находки только в файлах проверок: в рабочем коде
|
||||
# чтение своей карты заголовков законно.
|
||||
# Правило о заголовках живёт только в файлах проверок: в рабочем коде
|
||||
# `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:
|
||||
|
||||
Reference in New Issue
Block a user