Overview
The custom linter suite (pkg/linters/, registered in cmd/linters/main.go) has an inconsistent suppression story. The shared helper pkg/linters/internal/nolint already exists and provides a clean, reusable BuildLineIndex + HasDirective pair that lets a linter honor //nolint:<name> (and //nolint:all) comments — matching golangci-lint behavior. But only 3 of 21 registered analyzers actually wire it up.
Critically, 9 of the 11 linters that are already enforced in CI (.github/workflows/cgo.yml:1078, make golint-custom LINTER_FLAGS=...) silently ignore //nolint. Today they pass because every flagged site was refactored to zero. But the moment a legitimately intentional violation appears, a contributor has no in-code escape hatch — the only options are an awkward refactor or editing the CI allowlist. That is exactly the kind of friction internal/nolint was built to prevent, and two enforced linters (errstringmatch, jsonmarshalignoredeerror) already rely on it.
Critical finding: suppression parity across enforced linters
| Enforced linter (cgo.yml:1078) |
Honors //nolint? |
| errstringmatch |
✅ yes |
| jsonmarshalignoredeerror |
✅ yes |
| panicinlibrarycode |
❌ no |
| manualmutexunlock |
❌ no |
| osexitinlibrary |
❌ no |
| rawloginlib |
❌ no |
| regexpcompileinfunction |
❌ no |
| fprintlnsprintf |
❌ no |
| strconvparseignorederror |
❌ no |
| uncheckedtypeassertion |
❌ no |
| fmterrorfnoverbs |
❌ no |
2 of 11 enforced linters honor //nolint; 9 do not. (The only other linter wired to internal/nolint is errormessage, which is opt-in / not enforced — so the codebase-wide total is just 3 of 21.)
Why it matters
Evidence — grep + integration pattern
Only three implementation files import the helper:
$ rg -l "internal/nolint" pkg/linters --glob '*.go'
pkg/linters/jsonmarshalignoredeerror/jsonmarshalignoredeerror.go
pkg/linters/errstringmatch/errstringmatch.go
pkg/linters/errormessage/errormessage.go
The integration in errstringmatch.go is ~3 lines:
import "github.com/github/gh-aw/pkg/linters/internal/nolint"
// inside run(pass *analysis.Pass):
noLintLinesByFile := nolint.BuildLineIndex(pass, "errstringmatch")
// ... before each report:
position := pass.Fset.Position(node.Pos())
if nolint.HasDirective(position, noLintLinesByFile) {
return // suppressed
}
pass.ReportRangef(node, "...")
The 9 enforced linters lacking it all already call pass.Report* (verified for manualmutexunlock, uncheckedtypeassertion, fmterrorfnoverbs), so the change is a localized, mechanical insertion of one index build + one guard before each report.
Recommended fix
For each of the 9 enforced linters without suppression support:
- Import
github.com/github/gh-aw/pkg/linters/internal/nolint.
- Build the index once at the top of
run: idx := nolint.BuildLineIndex(pass, "<linterName>").
- Guard each
pass.Report* call: if nolint.HasDirective(pass.Fset.Position(pos), idx) { return/continue }.
- Add a
//nolint:<name> case to each linter's testdata/ so the suppression path is regression-tested.
Optionally extend the same treatment to the registered-but-unenforced precision-gap linters (seenmapbool, tolowerequalfold) so they are enforcement-ready.
Validation checklist
Effort
Moderate, mechanical, low-risk — 9 near-identical 3-line edits plus testdata. No production code changes; no behavior change for code without //nolint annotations.
Scope note (deliberately out of scope)
This issue is only about the suppression-parity gap in the linter framework. The violation-conversion work is already tracked elsewhere and is not duplicated here: strings.EqualFold conversion (#37047), map[string]struct{} conversion (#37048), function-length refactoring (#37046), and spec-test analyzer coverage (#36941).
References:
Generated by 🤖 Sergo - Serena Go Expert · 2.4K AIC · ◷
Overview
The custom linter suite (
pkg/linters/, registered incmd/linters/main.go) has an inconsistent suppression story. The shared helperpkg/linters/internal/nolintalready exists and provides a clean, reusableBuildLineIndex+HasDirectivepair that lets a linter honor//nolint:<name>(and//nolint:all) comments — matching golangci-lint behavior. But only 3 of 21 registered analyzers actually wire it up.Critically, 9 of the 11 linters that are already enforced in CI (
.github/workflows/cgo.yml:1078,make golint-custom LINTER_FLAGS=...) silently ignore//nolint. Today they pass because every flagged site was refactored to zero. But the moment a legitimately intentional violation appears, a contributor has no in-code escape hatch — the only options are an awkward refactor or editing the CI allowlist. That is exactly the kind of frictioninternal/nolintwas built to prevent, and two enforced linters (errstringmatch,jsonmarshalignoredeerror) already rely on it.Critical finding: suppression parity across enforced linters
//nolint?2 of 11 enforced linters honor
//nolint; 9 do not. (The only other linter wired tointernal/nolintiserrormessage, which is opt-in / not enforced — so the codebase-wide total is just 3 of 21.)Why it matters
jsonmarshalignoredeerrorshipped with 13 live//nolint:sites (per [deep-report] Add 6 missing analyzers to pkg/linters/spec_test.go and fix stale count comment #36941) proving real-world need for per-site exceptions. The other 9 enforced linters offer no equivalent — a future justified exception forces a refactor or a CI-config change.uncheckedtypeassertiontwo-valueResultOffix. The framework already owns the solution (internal/nolint); it's just unevenly applied.strings.EqualFoldconversion in [lint-monster] Use strings.EqualFold for Case-Insensitive Comparison #37047, themap[string]struct{}conversion in [lint-monster] Replace map[string]bool with map[string]struct{} for Set Operations #37048) would be safer to enforce if a documented//nolint:path existed for the rare legitimate case, rather than requiring every site to reach zero.Evidence — grep + integration pattern
Only three implementation files import the helper:
The integration in
errstringmatch.gois ~3 lines:The 9 enforced linters lacking it all already call
pass.Report*(verified formanualmutexunlock,uncheckedtypeassertion,fmterrorfnoverbs), so the change is a localized, mechanical insertion of one index build + one guard before each report.Recommended fix
For each of the 9 enforced linters without suppression support:
github.com/github/gh-aw/pkg/linters/internal/nolint.run:idx := nolint.BuildLineIndex(pass, "<linterName>").pass.Report*call:if nolint.HasDirective(pass.Fset.Position(pos), idx) { return/continue }.//nolint:<name>case to each linter'stestdata/so the suppression path is regression-tested.Optionally extend the same treatment to the registered-but-unenforced precision-gap linters (
seenmapbool,tolowerequalfold) so they are enforcement-ready.Validation checklist
nolint.BuildLineIndex+HasDirectivewired into all 9 enforced linters.testdata/includes a//nolint:<name>suppression case (same-line and previous-line).go test ./pkg/linters/...passes.make golint-customstill reports zero violations (no behavioral change for un-annotated code).Effort
Moderate, mechanical, low-risk — 9 near-identical 3-line edits plus testdata. No production code changes; no behavior change for code without
//nolintannotations.Scope note (deliberately out of scope)
This issue is only about the suppression-parity gap in the linter framework. The violation-conversion work is already tracked elsewhere and is not duplicated here:
strings.EqualFoldconversion (#37047),map[string]struct{}conversion (#37048), function-length refactoring (#37046), and spec-test analyzer coverage (#36941).References: