From 35bde75b1f54d7c3cac92167c375c464e5e1cb53 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Wed, 12 Aug 2026 22:04:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=B5=D0=B9=D1=82=20=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D1=8F=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D1=83,=20=D1=81=D1=83=D0=B4=D1=8F=D1=89=D1=83=D1=8E=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=20=D0=BF=D0=BE=20=D0=B6=D0=B8=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=BA=D0=B0=D1=80=D1=82=D0=B5=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - forbidigo с analyze-types запрещает в файлах проверок обращение к httptest.ResponseRecorder.Header и .HeaderMap: правило судит по типу приёмника, поэтому ловит и цепочку, и переменную, и индекс, и обход - класс стоил трёх зелёных гейтов при неработающем коде; правило записано строкой в перечне механизированного, прозой не дублируется - поправлены два утверждения «механизировано: ничего», разошедшиеся с включённым линтером и со сверкой шага схемы --- .golangci.yml | 42 +++++++++++++++++++++++++++++++++++++ docs/conventions/README.md | 6 ++++-- docs/conventions/logging.md | 6 ++++-- docs/review.md | 12 +++++++++-- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d14d02a..bbf669e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,7 +4,41 @@ linters: default: standard enable: - errorlint + - 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: true + forbid: + - pattern: '^httptest\.ResponseRecorder\.Header$' + msg: 'проверка судит ответ по живой карте заголовков: читай w.Result().Header' + - pattern: '^httptest\.ResponseRecorder\.HeaderMap$' + msg: 'проверка судит ответ по живой карте заголовков: читай w.Result().Header' errcheck: # Без этого `_ = x.Close()` снимает замечание, и критерий «отказ не # теряется молча» принимается реализацией, которая его теряет. Отказ, @@ -20,6 +54,14 @@ linters: # Метод сам логирует ошибку отправки, вызывающему она не нужна - (*git.vakhrushev.me/av/transcriber/internal/controller/tg.TelegramController).send + exclusions: + rules: + # `path-except` оставляет находки только в файлах проверок: в рабочем коде + # чтение своей карты заголовков законно. + - linters: + - forbidigo + path-except: '_test\.go$' + formatters: enable: - gofmt diff --git a/docs/conventions/README.md b/docs/conventions/README.md index cf78c7b..fe6e266 100644 --- a/docs/conventions/README.md +++ b/docs/conventions/README.md @@ -56,6 +56,7 @@ htmx, а здесь решено делать SPA — и перенесённы | --- | --- | | Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` | | Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck`, включая присваивание в `_` (`check-blank`). Отказ, который решено не проверять, объявляют в `exclude-functions` поимённо — там сегодня `defer Close` и `send` | +| Проверка судит ответ по готовому ответу (`Result()`), а не по живой карте заголовков обработчика | `.golangci.yml` → `forbidigo` с `analyze-types`, находки только в `*_test.go`. Судит по типу приёмника (`httptest.ResponseRecorder`), поэтому ловит любую форму: цепочкой, через переменную, по индексу карты, обходом, полем `HeaderMap`. Остаётся ревью проверка, идущая мимо recorder — через свой `http.ResponseWriter` | | Форматирование исходников | `.golangci.yml` → `gofmt` | | Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` | | Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` | @@ -68,6 +69,7 @@ htmx, а здесь решено делать SPA — и перенесённы **Из перечисленного в записях правилом выражено одно** — сравнение ошибок через `errors.Is` и `errors.As` (`errorlint`, строка таблицы выше). Прозой остаётся всё прочее: ни константный `msg` лога (`sloglint`), ни запрет `fmt.Print*` и -`os.Getenv` (`forbidigo`), ни запрет сторонних пакетов ошибок (`depguard`), ни -архитектурные тесты-сканеры. Это следующий шаг переноса в правило: свойство, +`os.Getenv` (`forbidigo` заведён, но одним правилом — о том, чем судят ответ в +проверках; этих двух запретов в нём нет), ни запрет сторонних пакетов ошибок +(`depguard`), ни архитектурные тесты-сканеры. Это следующий шаг переноса в правило: свойство, оставшееся прозой, проверяет человек на каждом ревью заново. diff --git a/docs/conventions/logging.md b/docs/conventions/logging.md index 59ec8e2..991eeda 100644 --- a/docs/conventions/logging.md +++ b/docs/conventions/logging.md @@ -11,8 +11,10 @@ OpenSpec. категория; шаг конвейера логирует и себя, и свой исход, и при этом возвращает ошибку выше, где её логируют снова. -**Механизировано:** ничего. Ни `sloglint`, ни `forbidigo` в `.golangci.yml` не -включены, поэтому правилами не выражено ни одно из перечисленного ниже. +**Механизировано:** ничего из перечисленного ниже. `forbidigo` в `.golangci.yml` +включён, но правило у него одно и о другом — чем судят ответ в проверках +([README.md](README.md), «Механизировано»); `sloglint` не заведён, и ни один +пункт этой записи правилом не выражен. ## Принципы diff --git a/docs/review.md b/docs/review.md index 3105841..fab748c 100644 --- a/docs/review.md +++ b/docs/review.md @@ -281,8 +281,16 @@ API и имя не откатываются обратной правкой по - **Что меняем:** уборка перенесена до записи ответа; все проверки этого файла судят по `Result()`. Класс всплывает **третий раз** (2026-08-10 «тесты http-обработчика ни разу не были зелёными», 2026-08-11 «проверка приёма не - могла упасть»), поэтому он же уходит кандидатом в конвенции: проверка ответа - судит по готовому ответу, а не по изменяемому состоянию обработчика + могла упасть»), поэтому он же ушёл в конвенции правилом: проверка ответа + судит по готовому ответу, а не по изменяемому состоянию обработчика. + Механизировано 2026-08-12 задачей `response-assertions-judge-result` — + `forbidigo` в `.golangci.yml` роняет гейт на чтении живой карты заголовков в + файле проверок. Правило судит по **типу приёмника**, а не по тексту вызова, и + потому ловит любую форму чтения живой карты — цепочкой, через переменную, по + индексу, обходом, полем `HeaderMap`. Текстовый запрет ловил только прямую + цепочку и обходился одной лишней строкой — это назвал прогон ревью этой же + задачи. Проходу ревью остаётся проверка, идущая мимо recorder, через свой + `http.ResponseWriter` ## 2026-08-12 — каждый анонимный запрос навсегда замедлял запись в хранилище [пойман ревью]