# Линтеры проекта. Перечень правил и их дома — 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 в точке входа
        # `cmd/transcriber` — он кладёт .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
        # Закрытие выборки отложенным вызовом: строки к этому моменту прочитаны,
        # а их отказ уже спрошен у `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
