From 30ea0f020414ffed0b2b838361b7f16c187a6189 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 09:25:23 +0000
Subject: [PATCH 01/10] Initial plan


From 99104c4ce755a850f252e235d0a5dc0ef5dc9728 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 09:45:30 +0000
Subject: [PATCH 02/10] lint: share nolint and generated file indexes

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
 .../appendbytestring/appendbytestring.go      |  15 +-
 .../bytesbufferstring/bytesbufferstring.go    |  15 +-
 .../bytescomparestring/bytescomparestring.go  |  15 +-
 .../contextcancelnotdeferred.go               |  25 ++--
 pkg/linters/ctxbackground/ctxbackground.go    |  15 +-
 pkg/linters/deferinloop/deferinloop.go        |  15 +-
 pkg/linters/errorfwrapv/errorfwrapv.go        |  15 +-
 pkg/linters/errormessage/errormessage.go      |  15 +-
 .../errortypeassertion/errortypeassertion.go  |  23 +--
 pkg/linters/errstringmatch/errstringmatch.go  |  15 +-
 .../excessivefuncparams.go                    |  15 +-
 .../execcommandwithoutcontext.go              |  15 +-
 .../fileclosenotdeferred.go                   |  29 ++--
 .../fmterrorfnoverbs/fmterrorfnoverbs.go      |   9 +-
 .../fprintlnsprintf/fprintlnsprintf.go        |  15 +-
 .../hardcodedfilepath/hardcodedfilepath.go    |  15 +-
 pkg/linters/httpnoctx/httpnoctx.go            |  15 +-
 .../httprespbodyclose/httprespbodyclose.go    |  29 ++--
 pkg/linters/httpstatuscode/httpstatuscode.go  |  21 ++-
 pkg/linters/internal/filecheck/filecheck.go   |  59 ++++++++
 .../internal/filecheck/filecheck_test.go      |  56 ++++++++
 pkg/linters/internal/nolint/nolint.go         | 133 ++++++++++++++----
 pkg/linters/internal/nolint/nolint_test.go    |  11 ++
 .../ioutildeprecated/ioutildeprecated.go      |  19 ++-
 .../jsonmarshalignoredeerror.go               |  27 ++--
 pkg/linters/largefunc/largefunc.go            |  15 +-
 pkg/linters/lenstringsplit/lenstringsplit.go  |  15 +-
 pkg/linters/lenstringzero/lenstringzero.go    |  15 +-
 .../logfatallibrary/logfatallibrary.go        |  15 +-
 .../manualmutexunlock/manualmutexunlock.go    |  25 ++--
 pkg/linters/mapdeletecheck/mapdeletecheck.go  |  15 +-
 .../osexitinlibrary/osexitinlibrary.go        |  15 +-
 .../osgetenvlibrary/osgetenvlibrary.go        |  15 +-
 .../ossetenvlibrary/ossetenvlibrary.go        |  15 +-
 .../panic-in-library-code.go                  |  15 +-
 pkg/linters/rawloginlib/rawloginlib.go        |  15 +-
 .../regexpcompileinfunction.go                |  15 +-
 pkg/linters/seenmapbool/seenmapbool.go        |  19 ++-
 pkg/linters/sortslice/sortslice.go            |  15 +-
 pkg/linters/sprintferrdot/sprintferrdot.go    |  15 +-
 .../sprintferrorsnew/sprintferrorsnew.go      |  15 +-
 pkg/linters/sprintfint/sprintfint.go          |  15 +-
 .../strconvparseignorederror.go               |   9 +-
 .../stringreplaceminusone.go                  |  15 +-
 .../stringscountcontains.go                   |  15 +-
 .../stringsindexcontains.go                   |  15 +-
 .../src/stringsindexcontains/generated.go     |   9 ++
 pkg/linters/timeafterleak/timeafterleak.go    |  15 +-
 .../timesleepnocontext/timesleepnocontext.go  |  15 +-
 .../tolowerequalfold/tolowerequalfold.go      |  15 +-
 .../uncheckedtypeassertion.go                 |  19 ++-
 .../wgdonenotdeferred/wgdonenotdeferred.go    |  19 ++-
 .../writebytestring/writebytestring.go        |  15 +-
 53 files changed, 798 insertions(+), 268 deletions(-)
 create mode 100644 pkg/linters/internal/filecheck/filecheck_test.go
 create mode 100644 pkg/linters/stringsindexcontains/testdata/src/stringsindexcontains/generated.go

diff --git a/pkg/linters/appendbytestring/appendbytestring.go b/pkg/linters/appendbytestring/appendbytestring.go
index d8b874d4043..e0c6f4c303c 100644
--- a/pkg/linters/appendbytestring/appendbytestring.go
+++ b/pkg/linters/appendbytestring/appendbytestring.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "appendbytestring",
 	Doc:      "reports append(b, []byte(s)...) calls where s is a string that can be simplified to append(b, s...)",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/appendbytestring",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "appendbytestring")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -52,10 +59,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "appendbytestring") {
 			return
 		}
 
diff --git a/pkg/linters/bytesbufferstring/bytesbufferstring.go b/pkg/linters/bytesbufferstring/bytesbufferstring.go
index ee723e2602c..f59288ef115 100644
--- a/pkg/linters/bytesbufferstring/bytesbufferstring.go
+++ b/pkg/linters/bytesbufferstring/bytesbufferstring.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "bytesbufferstring",
 	Doc:      "reports string(buf.Bytes()) calls where buf is a bytes.Buffer value and suggests buf.String() instead",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/bytesbufferstring",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "bytesbufferstring")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -57,10 +64,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "bytesbufferstring") {
 			return
 		}
 
diff --git a/pkg/linters/bytescomparestring/bytescomparestring.go b/pkg/linters/bytescomparestring/bytescomparestring.go
index 3b95a42b2fe..b17f7b48bae 100644
--- a/pkg/linters/bytescomparestring/bytescomparestring.go
+++ b/pkg/linters/bytescomparestring/bytescomparestring.go
@@ -25,7 +25,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "bytescomparestring",
 	Doc:      "flags string(a) == string(b) and string(a) != string(b) as []byte comparisons written the long way; use bytes.Equal for clearer intent",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/bytescomparestring",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -34,7 +34,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "bytescomparestring")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	// seenImportFiles tracks files that have already received a bytes import
 	// TextEdit in this pass, preventing duplicate overlapping edits when a
@@ -57,10 +64,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(bin.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "bytescomparestring") {
 			return
 		}
 
diff --git a/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go b/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go
index 8af6ac0ee03..97c35107724 100644
--- a/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go
+++ b/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go
@@ -20,7 +20,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "contextcancelnotdeferred",
 	Doc:      "reports context cancel functions that are called directly instead of deferred",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/contextcancelnotdeferred",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -29,38 +29,45 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "contextcancelnotdeferred")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
 	}
 
 	insp.Preorder(nodeFilter, func(n ast.Node) {
-		inspectCancelFuncDecl(pass, n, noLintLinesByFile)
+		inspectCancelFuncDecl(pass, n, noLintIndex, generatedFiles)
 	})
 
 	return nil, nil
 }
 
-func inspectCancelFuncDecl(pass *analysis.Pass, n ast.Node, noLintLinesByFile map[string]map[int]struct{}) {
+func inspectCancelFuncDecl(pass *analysis.Pass, n ast.Node, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex) {
 	fn, ok := n.(*ast.FuncDecl)
 	if !ok || fn.Body == nil {
 		return
 	}
 
 	pos := pass.Fset.PositionFor(fn.Pos(), false)
-	if filecheck.IsTestFile(pos.Filename) {
+	if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 		return
 	}
 
 	cancelVars := make(map[types.Object]*cancelVarState)
 
 	ast.Inspect(fn.Body, func(node ast.Node) bool {
-		return inspectCancelNode(pass, cancelVars, node, noLintLinesByFile)
+		return inspectCancelNode(pass, cancelVars, node, noLintIndex)
 	})
 
 	for _, state := range cancelVars {
-		if state.hasDirectCancel && !state.hasDeferCancel && !nolint.HasDirective(pass.Fset.PositionFor(state.createPos, false), noLintLinesByFile) {
+		if state.hasDirectCancel && !state.hasDeferCancel && !nolint.HasDirectiveForLinter(pass.Fset.PositionFor(state.createPos, false), noLintIndex, "contextcancelnotdeferred") {
 			pass.Report(analysis.Diagnostic{
 				Pos:     state.createPos,
 				Message: "context cancel function should be deferred immediately after context.WithCancel/WithTimeout/WithDeadline",
@@ -69,7 +76,7 @@ func inspectCancelFuncDecl(pass *analysis.Pass, n ast.Node, noLintLinesByFile ma
 	}
 }
 
-func inspectCancelNode(pass *analysis.Pass, cancelVars map[types.Object]*cancelVarState, node ast.Node, noLintLinesByFile map[string]map[int]struct{}) bool {
+func inspectCancelNode(pass *analysis.Pass, cancelVars map[types.Object]*cancelVarState, node ast.Node, noLintIndex nolint.DirectiveIndex) bool {
 	if node == nil {
 		return false
 	}
@@ -93,7 +100,7 @@ func inspectCancelNode(pass *analysis.Pass, cancelVars map[types.Object]*cancelV
 				if obj == nil {
 					continue
 				}
-				if prev, exists := cancelVars[obj]; exists && prev.hasDirectCancel && !prev.hasDeferCancel && !nolint.HasDirective(pass.Fset.PositionFor(prev.createPos, false), noLintLinesByFile) {
+				if prev, exists := cancelVars[obj]; exists && prev.hasDirectCancel && !prev.hasDeferCancel && !nolint.HasDirectiveForLinter(pass.Fset.PositionFor(prev.createPos, false), noLintIndex, "contextcancelnotdeferred") {
 					pass.Report(analysis.Diagnostic{
 						Pos:     prev.createPos,
 						Message: "context cancel function should be deferred immediately after context.WithCancel/WithTimeout/WithDeadline",
diff --git a/pkg/linters/ctxbackground/ctxbackground.go b/pkg/linters/ctxbackground/ctxbackground.go
index 65802084c33..99b91150682 100644
--- a/pkg/linters/ctxbackground/ctxbackground.go
+++ b/pkg/linters/ctxbackground/ctxbackground.go
@@ -20,7 +20,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "ctxbackground",
 	Doc:      "reports calls to context.Background() inside functions that already receive a context.Context parameter",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/ctxbackground",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -29,7 +29,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "ctxbackground")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cur.Node().(*ast.CallExpr)
@@ -38,10 +45,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "ctxbackground") {
 			continue
 		}
 
diff --git a/pkg/linters/deferinloop/deferinloop.go b/pkg/linters/deferinloop/deferinloop.go
index 243f5640ba6..00722a29742 100644
--- a/pkg/linters/deferinloop/deferinloop.go
+++ b/pkg/linters/deferinloop/deferinloop.go
@@ -25,7 +25,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "deferinloop",
 	Doc:      "reports defer statements enclosed anywhere within a for or range loop body; a function literal between a defer and an enclosing loop is treated as a new scope boundary, making the defer exempt; test files are not checked",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/deferinloop",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -36,7 +36,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "deferinloop")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range insp.Root().Preorder((*ast.DeferStmt)(nil)) {
 		deferStmt, ok := cur.Node().(*ast.DeferStmt)
@@ -45,10 +52,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(deferStmt.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "deferinloop") {
 			continue
 		}
 
diff --git a/pkg/linters/errorfwrapv/errorfwrapv.go b/pkg/linters/errorfwrapv/errorfwrapv.go
index f1d2ac1799f..1c427526314 100644
--- a/pkg/linters/errorfwrapv/errorfwrapv.go
+++ b/pkg/linters/errorfwrapv/errorfwrapv.go
@@ -46,7 +46,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "errorfwrapv",
 	Doc:      "reports fmt.Errorf calls that format error arguments with %v instead of %w",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/errorfwrapv",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -59,7 +59,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "errorfwrapv")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -72,7 +79,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		position := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(position.Filename) {
+		if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
 			return
 		}
 
@@ -105,7 +112,7 @@ func run(pass *analysis.Pass) (any, error) {
 			if !types.Implements(tv.Type, errorIface) {
 				continue
 			}
-			if nolint.HasDirective(position, noLintLinesByFile) {
+			if nolint.HasDirectiveForLinter(position, noLintIndex, "errorfwrapv") {
 				return
 			}
 			pass.ReportRangef(call, "fmt.Errorf formats an error argument with %%v; use %%w to preserve the error chain")
diff --git a/pkg/linters/errormessage/errormessage.go b/pkg/linters/errormessage/errormessage.go
index 8e99502b4ae..48e31e6cb1e 100644
--- a/pkg/linters/errormessage/errormessage.go
+++ b/pkg/linters/errormessage/errormessage.go
@@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "errormessage",
 	Doc:      "reports non-actionable error message patterns in changed files",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/errormessage",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -46,7 +46,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "errormessage")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
 	insp.Preorder(nodeFilter, func(n ast.Node) {
@@ -56,10 +63,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if !shouldCheckFile(pos.Filename, changed) || filecheck.IsTestFile(pos.Filename) {
+		if !shouldCheckFile(pos.Filename, changed) || filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "errormessage") {
 			return
 		}
 
diff --git a/pkg/linters/errortypeassertion/errortypeassertion.go b/pkg/linters/errortypeassertion/errortypeassertion.go
index d73d3391238..e4c267c2840 100644
--- a/pkg/linters/errortypeassertion/errortypeassertion.go
+++ b/pkg/linters/errortypeassertion/errortypeassertion.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "errortypeassertion",
 	Doc:      "reports type assertions from error to concrete types; use errors.As for wrapped errors",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/errortypeassertion",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "errortypeassertion")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	builtinErrorObj := types.Universe.Lookup("error")
 	if builtinErrorObj == nil {
@@ -43,9 +50,9 @@ func run(pass *analysis.Pass) (any, error) {
 	insp.Preorder([]ast.Node{(*ast.TypeAssertExpr)(nil), (*ast.TypeSwitchStmt)(nil)}, func(n ast.Node) {
 		switch node := n.(type) {
 		case *ast.TypeAssertExpr:
-			checkTypeAssertExpr(pass, noLintLinesByFile, builtinErrorType, node)
+			checkTypeAssertExpr(pass, noLintIndex, generatedFiles, builtinErrorType, node)
 		case *ast.TypeSwitchStmt:
-			checkTypeSwitchStmt(pass, noLintLinesByFile, builtinErrorType, node)
+			checkTypeSwitchStmt(pass, noLintIndex, generatedFiles, builtinErrorType, node)
 		}
 	})
 
@@ -53,14 +60,14 @@ func run(pass *analysis.Pass) (any, error) {
 }
 
 // checkTypeAssertExpr flags direct type assertions of the form err.(ConcreteType).
-func checkTypeAssertExpr(pass *analysis.Pass, noLintLinesByFile map[string]map[int]struct{}, builtinErrorType types.Type, typeAssert *ast.TypeAssertExpr) {
+func checkTypeAssertExpr(pass *analysis.Pass, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex, builtinErrorType types.Type, typeAssert *ast.TypeAssertExpr) {
 	// Type-switch guards have nil Type; skip them (handled by checkTypeSwitchStmt).
 	if typeAssert.Type == nil {
 		return
 	}
 
 	pos := pass.Fset.PositionFor(typeAssert.Pos(), false)
-	if filecheck.IsTestFile(pos.Filename) || nolint.HasDirective(pos, noLintLinesByFile) {
+	if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) || nolint.HasDirectiveForLinter(pos, noLintIndex, "errortypeassertion") {
 		return
 	}
 
@@ -90,7 +97,7 @@ func checkTypeAssertExpr(pass *analysis.Pass, noLintLinesByFile map[string]map[i
 
 // checkTypeSwitchStmt flags concrete-type case arms in type switches on error,
 // e.g. "case *os.PathError:" inside "switch err.(type)".
-func checkTypeSwitchStmt(pass *analysis.Pass, noLintLinesByFile map[string]map[int]struct{}, builtinErrorType types.Type, stmt *ast.TypeSwitchStmt) {
+func checkTypeSwitchStmt(pass *analysis.Pass, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex, builtinErrorType types.Type, stmt *ast.TypeSwitchStmt) {
 	x := typeSwitchX(stmt)
 	if x == nil {
 		return
@@ -110,7 +117,7 @@ func checkTypeSwitchStmt(pass *analysis.Pass, noLintLinesByFile map[string]map[i
 		}
 		for _, typeExpr := range cc.List {
 			pos := pass.Fset.PositionFor(typeExpr.Pos(), false)
-			if filecheck.IsTestFile(pos.Filename) || nolint.HasDirective(pos, noLintLinesByFile) {
+			if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) || nolint.HasDirectiveForLinter(pos, noLintIndex, "errortypeassertion") {
 				continue
 			}
 
diff --git a/pkg/linters/errstringmatch/errstringmatch.go b/pkg/linters/errstringmatch/errstringmatch.go
index 0c3ba7354d3..a5a1c83e904 100644
--- a/pkg/linters/errstringmatch/errstringmatch.go
+++ b/pkg/linters/errstringmatch/errstringmatch.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "errstringmatch",
 	Doc:      "reports strings.Contains/HasPrefix/HasSuffix/EqualFold/Index/LastIndex/Compare(err.Error(), \"...\") calls that perform brittle substring matching on error messages",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/errstringmatch",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -42,7 +42,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "errstringmatch")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -54,7 +61,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 		position := pass.Fset.PositionFor(outer.Pos(), false)
-		if filecheck.IsTestFile(position.Filename) {
+		if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
 			return
 		}
 
@@ -76,7 +83,7 @@ func run(pass *analysis.Pass) (any, error) {
 		if !isStringLiteral(pass, outer.Args[1]) {
 			return
 		}
-		if nolint.HasDirective(position, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(position, noLintIndex, "errstringmatch") {
 			return
 		}
 
diff --git a/pkg/linters/excessivefuncparams/excessivefuncparams.go b/pkg/linters/excessivefuncparams/excessivefuncparams.go
index 1da8c100952..ba8d94a8ef1 100644
--- a/pkg/linters/excessivefuncparams/excessivefuncparams.go
+++ b/pkg/linters/excessivefuncparams/excessivefuncparams.go
@@ -24,7 +24,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "excessivefuncparams",
 	Doc:      "reports functions whose parameter count exceeds the limit (default 8 params)",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/excessivefuncparams",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -43,7 +43,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "excessivefuncparams")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
@@ -58,7 +65,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 		position := pass.Fset.PositionFor(fn.Name.Pos(), false)
-		if filecheck.IsTestFile(position.Filename) {
+		if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
 			return
 		}
 
@@ -72,7 +79,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		if params > maxParams {
-			if nolint.HasDirective(position, noLintLinesByFile) {
+			if nolint.HasDirectiveForLinter(position, noLintIndex, "excessivefuncparams") {
 				return
 			}
 			pkgLog.Printf("flagging %s: %d parameters exceeds limit %d", fn.Name.Name, params, maxParams)
diff --git a/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go b/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go
index c5b7be3f887..990011dd9eb 100644
--- a/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go
+++ b/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go
@@ -22,7 +22,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "execcommandwithoutcontext",
 	Doc:      "reports exec.Command calls inside context-receiving functions where exec.CommandContext should be used to propagate cancellation",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/execcommandwithoutcontext",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -31,7 +31,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "execcommandwithoutcontext")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cur.Node().(*ast.CallExpr)
@@ -44,10 +51,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "execcommandwithoutcontext") {
 			continue
 		}
 
diff --git a/pkg/linters/fileclosenotdeferred/fileclosenotdeferred.go b/pkg/linters/fileclosenotdeferred/fileclosenotdeferred.go
index 5de6731afeb..b1b327e8b15 100644
--- a/pkg/linters/fileclosenotdeferred/fileclosenotdeferred.go
+++ b/pkg/linters/fileclosenotdeferred/fileclosenotdeferred.go
@@ -20,7 +20,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "fileclosenotdeferred",
 	Doc:      "reports file operations where Close() is not immediately deferred, which can lead to resource leaks",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/fileclosenotdeferred",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -29,27 +29,34 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "fileclosenotdeferred")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
 	}
 
 	insp.Preorder(nodeFilter, func(n ast.Node) {
-		inspectFileFuncDecl(pass, n, noLintLinesByFile)
+		inspectFileFuncDecl(pass, n, noLintIndex, generatedFiles)
 	})
 
 	return nil, nil
 }
 
-func inspectFileFuncDecl(pass *analysis.Pass, n ast.Node, noLintLinesByFile map[string]map[int]struct{}) {
+func inspectFileFuncDecl(pass *analysis.Pass, n ast.Node, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex) {
 	fn, ok := n.(*ast.FuncDecl)
 	if !ok || fn.Body == nil {
 		return
 	}
 
 	pos := pass.Fset.PositionFor(fn.Pos(), false)
-	if filecheck.IsTestFile(pos.Filename) {
+	if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 		return
 	}
 
@@ -60,12 +67,12 @@ func inspectFileFuncDecl(pass *analysis.Pass, n ast.Node, noLintLinesByFile map[
 	// Walk all statements in the function body, including nested blocks,
 	// but stop at function literals so closures are analysed independently.
 	ast.Inspect(fn.Body, func(node ast.Node) bool {
-		return analyzeASTNodeForFileClosePatterns(pass, fileVars, node, noLintLinesByFile)
+		return analyzeASTNodeForFileClosePatterns(pass, fileVars, node, noLintIndex)
 	})
 
 	// Report files with manual close but no defer
 	for _, state := range fileVars {
-		if state.hasManualClose && !state.hasDefer && !nolint.HasDirective(pass.Fset.PositionFor(state.openPos, false), noLintLinesByFile) {
+		if state.hasManualClose && !state.hasDefer && !nolint.HasDirectiveForLinter(pass.Fset.PositionFor(state.openPos, false), noLintIndex, "fileclosenotdeferred") {
 			pass.Report(analysis.Diagnostic{
 				Pos:     state.openPos,
 				Message: "file Close() should be deferred immediately after successful open to prevent resource leaks",
@@ -74,7 +81,7 @@ func inspectFileFuncDecl(pass *analysis.Pass, n ast.Node, noLintLinesByFile map[
 	}
 }
 
-func analyzeASTNodeForFileClosePatterns(pass *analysis.Pass, fileVars map[types.Object]*fileVarState, node ast.Node, noLintLinesByFile map[string]map[int]struct{}) bool {
+func analyzeASTNodeForFileClosePatterns(pass *analysis.Pass, fileVars map[types.Object]*fileVarState, node ast.Node, noLintIndex nolint.DirectiveIndex) bool {
 	if node == nil {
 		return false
 	}
@@ -87,7 +94,7 @@ func analyzeASTNodeForFileClosePatterns(pass *analysis.Pass, fileVars map[types.
 
 	// Look for assignments like: file, err := os.Open(...)
 	if assign, ok := node.(*ast.AssignStmt); ok {
-		trackFileOpenAssignment(pass, fileVars, assign, noLintLinesByFile)
+		trackFileOpenAssignment(pass, fileVars, assign, noLintIndex)
 	}
 
 	// Look for defer file.Close()
@@ -118,7 +125,7 @@ func analyzeASTNodeForFileClosePatterns(pass *analysis.Pass, fileVars map[types.
 	return true
 }
 
-func trackFileOpenAssignment(pass *analysis.Pass, fileVars map[types.Object]*fileVarState, assign *ast.AssignStmt, noLintLinesByFile map[string]map[int]struct{}) {
+func trackFileOpenAssignment(pass *analysis.Pass, fileVars map[types.Object]*fileVarState, assign *ast.AssignStmt, noLintIndex nolint.DirectiveIndex) {
 	for i, rhs := range assign.Rhs {
 		call, ok := rhs.(*ast.CallExpr)
 		if !ok || !isFileOpenCall(pass, call) {
@@ -138,7 +145,7 @@ func trackFileOpenAssignment(pass *analysis.Pass, fileVars map[types.Object]*fil
 		// If this object was already tracked from a prior open on the
 		// same binding (plain = reassignment), report any unresolved
 		// violation immediately before overwriting the state.
-		if prev, exists := fileVars[obj]; exists && prev.hasManualClose && !prev.hasDefer && !nolint.HasDirective(pass.Fset.PositionFor(prev.openPos, false), noLintLinesByFile) {
+		if prev, exists := fileVars[obj]; exists && prev.hasManualClose && !prev.hasDefer && !nolint.HasDirectiveForLinter(pass.Fset.PositionFor(prev.openPos, false), noLintIndex, "fileclosenotdeferred") {
 			pass.Report(analysis.Diagnostic{
 				Pos:     prev.openPos,
 				Message: "file Close() should be deferred immediately after successful open to prevent resource leaks",
diff --git a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
index 1d362a9723c..dabce447d8b 100644
--- a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
+++ b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
@@ -19,7 +19,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "fmterrorfnoverbs",
 	Doc:      "reports fmt.Errorf calls whose format string contains no verbs, preferring errors.New",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/fmterrorfnoverbs",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer},
 	Run:      run,
 }
 
@@ -28,7 +28,10 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "fmterrorfnoverbs")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -61,7 +64,7 @@ func run(pass *analysis.Pass) (any, error) {
 
 		if !hasRealFormatVerb(val) {
 			position := pass.Fset.PositionFor(call.Pos(), false)
-			if nolint.HasDirective(position, noLintLinesByFile) {
+			if nolint.HasDirectiveForLinter(position, noLintIndex, "fmterrorfnoverbs") {
 				return
 			}
 			pass.ReportRangef(call, "fmt.Errorf called with no format verbs; use errors.New(%s) instead", lit.Value)
diff --git a/pkg/linters/fprintlnsprintf/fprintlnsprintf.go b/pkg/linters/fprintlnsprintf/fprintlnsprintf.go
index 36846ee34cf..519f8c82f97 100644
--- a/pkg/linters/fprintlnsprintf/fprintlnsprintf.go
+++ b/pkg/linters/fprintlnsprintf/fprintlnsprintf.go
@@ -19,7 +19,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "fprintlnsprintf",
 	Doc:      "reports fmt.Fprintln(w, fmt.Sprintf(...)) calls that should be rewritten as fmt.Fprintf(w, ...)",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/fprintlnsprintf",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -28,7 +28,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "fprintlnsprintf")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -50,7 +57,7 @@ func run(pass *analysis.Pass) (any, error) {
 
 		// Skip test files.
 		pos := pass.Fset.Position(call.Pos())
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
 
@@ -62,7 +69,7 @@ func run(pass *analysis.Pass) (any, error) {
 		if !isFmtFunc(pass, printedArg, "Sprintf") {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "fprintlnsprintf") {
 			return
 		}
 
diff --git a/pkg/linters/hardcodedfilepath/hardcodedfilepath.go b/pkg/linters/hardcodedfilepath/hardcodedfilepath.go
index 098afac3728..cd9add4e6c8 100644
--- a/pkg/linters/hardcodedfilepath/hardcodedfilepath.go
+++ b/pkg/linters/hardcodedfilepath/hardcodedfilepath.go
@@ -33,7 +33,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "hardcodedfilepath",
 	Doc:      "reports hard-coded file path string literals that should be replaced with named constants",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/hardcodedfilepath",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -238,7 +238,14 @@ func run(pass *analysis.Pass) (any, error) {
 		return nil, err
 	}
 
-	noLintLines := nolint.BuildLineIndex(pass, "hardcodedfilepath")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 	knownConsts := collectKnownPathConsts(pass)
 
 	for cur := range insp.Root().Preorder((*ast.BasicLit)(nil)) {
@@ -248,10 +255,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(lit.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLines) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "hardcodedfilepath") {
 			continue
 		}
 
diff --git a/pkg/linters/httpnoctx/httpnoctx.go b/pkg/linters/httpnoctx/httpnoctx.go
index bdd166f4ed3..dcd087f7f47 100644
--- a/pkg/linters/httpnoctx/httpnoctx.go
+++ b/pkg/linters/httpnoctx/httpnoctx.go
@@ -25,7 +25,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "httpnoctx",
 	Doc:      "reports context-free net/http request paths: http.Client/http package helpers without context, http.NewRequest in context-aware functions, and http.DefaultClient.Do",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/httpnoctx",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -43,7 +43,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "httpnoctx")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cursor := range insp.Root().Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cursor.Node().(*ast.CallExpr)
@@ -52,10 +59,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "httpnoctx") {
 			continue
 		}
 
diff --git a/pkg/linters/httprespbodyclose/httprespbodyclose.go b/pkg/linters/httprespbodyclose/httprespbodyclose.go
index af72ff49c68..e8add3c44bd 100644
--- a/pkg/linters/httprespbodyclose/httprespbodyclose.go
+++ b/pkg/linters/httprespbodyclose/httprespbodyclose.go
@@ -23,7 +23,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "httprespbodyclose",
 	Doc:      "reports HTTP response Body.Close() calls that are not deferred, which risks resource leaks on early return",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/httprespbodyclose",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -34,7 +34,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "httprespbodyclose")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
@@ -42,13 +49,13 @@ func run(pass *analysis.Pass) (any, error) {
 	}
 
 	insp.Preorder(nodeFilter, func(n ast.Node) {
-		inspectFunc(pass, n, noLintLinesByFile)
+		inspectFunc(pass, n, noLintIndex, generatedFiles)
 	})
 
 	return nil, nil
 }
 
-func inspectFunc(pass *analysis.Pass, n ast.Node, noLintLinesByFile map[string]map[int]struct{}) {
+func inspectFunc(pass *analysis.Pass, n ast.Node, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex) {
 	var body *ast.BlockStmt
 	switch fn := n.(type) {
 	case *ast.FuncDecl:
@@ -64,26 +71,26 @@ func inspectFunc(pass *analysis.Pass, n ast.Node, noLintLinesByFile map[string]m
 	}
 
 	pos := pass.Fset.PositionFor(n.Pos(), false)
-	if filecheck.IsTestFile(pos.Filename) {
+	if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 		return
 	}
 
 	respVars := make(map[types.Object]*respVarState)
 
 	ast.Inspect(body, func(node ast.Node) bool {
-		return inspectNode(pass, respVars, node, noLintLinesByFile)
+		return inspectNode(pass, respVars, node, noLintIndex)
 	})
 
 	for _, state := range respVars {
 		if state.hasManualClose && !state.hasDeferClose {
-			if !nolint.HasDirective(pass.Fset.PositionFor(state.assignPos, false), noLintLinesByFile) {
+			if !nolint.HasDirectiveForLinter(pass.Fset.PositionFor(state.assignPos, false), noLintIndex, "httprespbodyclose") {
 				reportMissingDefer(pass, state)
 			}
 		}
 	}
 }
 
-func inspectNode(pass *analysis.Pass, respVars map[types.Object]*respVarState, node ast.Node, noLintLinesByFile map[string]map[int]struct{}) bool {
+func inspectNode(pass *analysis.Pass, respVars map[types.Object]*respVarState, node ast.Node, noLintIndex nolint.DirectiveIndex) bool {
 	if node == nil {
 		return false
 	}
@@ -92,7 +99,7 @@ func inspectNode(pass *analysis.Pass, respVars map[types.Object]*respVarState, n
 	}
 
 	if assign, ok := node.(*ast.AssignStmt); ok {
-		trackHTTPAssignment(pass, respVars, assign, noLintLinesByFile)
+		trackHTTPAssignment(pass, respVars, assign, noLintIndex)
 		// Also check RHS for manual Body.Close() in assignments like: err := resp.Body.Close()
 		for _, rhs := range assign.Rhs {
 			if call, ok := rhs.(*ast.CallExpr); ok {
@@ -131,7 +138,7 @@ func markBodyClose(pass *analysis.Pass, respVars map[types.Object]*respVarState,
 	}
 }
 
-func trackHTTPAssignment(pass *analysis.Pass, respVars map[types.Object]*respVarState, assign *ast.AssignStmt, noLintLinesByFile map[string]map[int]struct{}) {
+func trackHTTPAssignment(pass *analysis.Pass, respVars map[types.Object]*respVarState, assign *ast.AssignStmt, noLintIndex nolint.DirectiveIndex) {
 	for i, lhs := range assign.Lhs {
 		ident, ok := lhs.(*ast.Ident)
 		if !ok || ident.Name == "_" {
@@ -161,7 +168,7 @@ func trackHTTPAssignment(pass *analysis.Pass, respVars map[types.Object]*respVar
 
 		// Report any prior unresolved violation before overwriting state.
 		if prev, exists := respVars[obj]; exists && prev.hasManualClose && !prev.hasDeferClose {
-			if !nolint.HasDirective(pass.Fset.PositionFor(prev.assignPos, false), noLintLinesByFile) {
+			if !nolint.HasDirectiveForLinter(pass.Fset.PositionFor(prev.assignPos, false), noLintIndex, "httprespbodyclose") {
 				reportMissingDefer(pass, prev)
 			}
 		}
diff --git a/pkg/linters/httpstatuscode/httpstatuscode.go b/pkg/linters/httpstatuscode/httpstatuscode.go
index 8902217b2a4..a24c69177a5 100644
--- a/pkg/linters/httpstatuscode/httpstatuscode.go
+++ b/pkg/linters/httpstatuscode/httpstatuscode.go
@@ -25,7 +25,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "httpstatuscode",
 	Doc:      "reports integer HTTP status code literals used in comparisons that should use http.Status* named constants",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/httpstatuscode",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -100,7 +100,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "httpstatuscode")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range root.Preorder((*ast.BinaryExpr)(nil), (*ast.SwitchStmt)(nil)) {
 		switch node := cur.Node().(type) {
@@ -115,7 +122,7 @@ func run(pass *analysis.Pass) (any, error) {
 			if !isHTTPStatusContext(pass, other) {
 				continue
 			}
-			checkAndReport(pass, lit, noLintLinesByFile)
+			checkAndReport(pass, lit, noLintIndex, generatedFiles)
 		case *ast.SwitchStmt:
 			if node.Tag == nil || !isHTTPStatusContext(pass, node.Tag) {
 				continue
@@ -130,7 +137,7 @@ func run(pass *analysis.Pass) (any, error) {
 					if !ok || lit.Kind != token.INT {
 						continue
 					}
-					checkAndReport(pass, lit, noLintLinesByFile)
+					checkAndReport(pass, lit, noLintIndex, generatedFiles)
 				}
 			}
 		}
@@ -139,7 +146,7 @@ func run(pass *analysis.Pass) (any, error) {
 	return nil, nil
 }
 
-func checkAndReport(pass *analysis.Pass, lit *ast.BasicLit, noLintLinesByFile map[string]map[int]struct{}) {
+func checkAndReport(pass *analysis.Pass, lit *ast.BasicLit, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex) {
 	code64, err := strconv.ParseInt(lit.Value, 0, 64)
 	if err != nil || code64 < 100 || code64 > 599 {
 		return
@@ -147,10 +154,10 @@ func checkAndReport(pass *analysis.Pass, lit *ast.BasicLit, noLintLinesByFile ma
 	code := int(code64)
 
 	pos := pass.Fset.PositionFor(lit.Pos(), false)
-	if filecheck.IsTestFile(pos.Filename) {
+	if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 		return
 	}
-	if nolint.HasDirective(pos, noLintLinesByFile) {
+	if nolint.HasDirectiveForLinter(pos, noLintIndex, "httpstatuscode") {
 		return
 	}
 
diff --git a/pkg/linters/internal/filecheck/filecheck.go b/pkg/linters/internal/filecheck/filecheck.go
index 5ae7f651f77..5dcbf566416 100644
--- a/pkg/linters/internal/filecheck/filecheck.go
+++ b/pkg/linters/internal/filecheck/filecheck.go
@@ -1,11 +1,70 @@
 package filecheck
 
 import (
+	"fmt"
+	"go/ast"
 	"path/filepath"
+	"reflect"
 	"strings"
+
+	"golang.org/x/tools/go/analysis"
 )
 
+// GeneratedIndex records generated Go source files by filename.
+type GeneratedIndex map[string]struct{}
+
+// Analyzer builds a shared generated-file index once per package so analyzers
+// can reuse it via pass.ResultOf.
+var Analyzer = &analysis.Analyzer{
+	Name:             "generatedfileindex",
+	Doc:              "indexes generated Go source files for gh-aw custom linters",
+	ResultType:       reflect.TypeFor[GeneratedIndex](),
+	RunDespiteErrors: true,
+	Run: func(pass *analysis.Pass) (any, error) {
+		return BuildGeneratedIndex(pass), nil
+	},
+}
+
+// Index returns the shared generated-file index for pass.
+func Index(pass *analysis.Pass) (GeneratedIndex, error) {
+	idx, ok := pass.ResultOf[Analyzer].(GeneratedIndex)
+	if !ok {
+		return nil, fmt.Errorf("generated file analyzer result has unexpected type %T", pass.ResultOf[Analyzer])
+	}
+	return idx, nil
+}
+
+// BuildGeneratedIndex returns the set of generated Go source files in pass.
+func BuildGeneratedIndex(pass *analysis.Pass) GeneratedIndex {
+	generated := make(GeneratedIndex)
+	for _, file := range pass.Files {
+		if !ast.IsGenerated(file) {
+			continue
+		}
+		filename := pass.Fset.PositionFor(file.Pos(), false).Filename
+		if filename == "" {
+			continue
+		}
+		generated[filename] = struct{}{}
+	}
+	return generated
+}
+
 // IsTestFile reports whether filename is a Go test file path.
 func IsTestFile(filename string) bool {
 	return strings.HasSuffix(filepath.Base(filename), "_test.go")
 }
+
+// IsGeneratedFile reports whether filename is marked as generated.
+func IsGeneratedFile(filename string, generated GeneratedIndex) bool {
+	if filename == "" {
+		return false
+	}
+	_, ok := generated[filename]
+	return ok
+}
+
+// ShouldSkipFilename reports whether filename should be skipped by linters.
+func ShouldSkipFilename(filename string, generated GeneratedIndex) bool {
+	return IsTestFile(filename) || IsGeneratedFile(filename, generated)
+}
diff --git a/pkg/linters/internal/filecheck/filecheck_test.go b/pkg/linters/internal/filecheck/filecheck_test.go
new file mode 100644
index 00000000000..83cae62f2cd
--- /dev/null
+++ b/pkg/linters/internal/filecheck/filecheck_test.go
@@ -0,0 +1,56 @@
+package filecheck
+
+import (
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"testing"
+
+	"golang.org/x/tools/go/analysis"
+)
+
+func TestBuildGeneratedIndex(t *testing.T) {
+	const generatedFilename = "generated.go"
+	const regularFilename = "regular.go"
+
+	fset := token.NewFileSet()
+	generatedFile, err := parser.ParseFile(fset, generatedFilename, `// Code generated by test. DO NOT EDIT.
+package p
+
+func generated() {}
+`, parser.ParseComments)
+	if err != nil {
+		t.Fatalf("ParseFile(generated) error = %v", err)
+	}
+	regularFile, err := parser.ParseFile(fset, regularFilename, `package p
+
+func regular() {}
+`, parser.ParseComments)
+	if err != nil {
+		t.Fatalf("ParseFile(regular) error = %v", err)
+	}
+
+	idx := BuildGeneratedIndex(&analysis.Pass{Fset: fset, Files: []*ast.File{generatedFile, regularFile}})
+	if !IsGeneratedFile(generatedFilename, idx) {
+		t.Fatalf("expected %q to be marked generated", generatedFilename)
+	}
+	if IsGeneratedFile(regularFilename, idx) {
+		t.Fatalf("did not expect %q to be marked generated", regularFilename)
+	}
+}
+
+func TestShouldSkipFilename(t *testing.T) {
+	idx := GeneratedIndex{
+		"generated.go": {},
+	}
+
+	if !ShouldSkipFilename("example_test.go", idx) {
+		t.Fatalf("expected test file to be skipped")
+	}
+	if !ShouldSkipFilename("generated.go", idx) {
+		t.Fatalf("expected generated file to be skipped")
+	}
+	if ShouldSkipFilename("regular.go", idx) {
+		t.Fatalf("did not expect regular file to be skipped")
+	}
+}
diff --git a/pkg/linters/internal/nolint/nolint.go b/pkg/linters/internal/nolint/nolint.go
index b60fb828eb5..286933d5836 100644
--- a/pkg/linters/internal/nolint/nolint.go
+++ b/pkg/linters/internal/nolint/nolint.go
@@ -3,36 +3,43 @@
 package nolint
 
 import (
+	"fmt"
 	"go/token"
 	"go/types"
+	"reflect"
 	"strings"
 
 	"golang.org/x/tools/go/analysis"
 )
 
-// HasDirective reports whether the given source position is covered by a
-// suppression directive for linterName (or "nolint:all").  Both same-line and
-// previous-line directives are recognised, matching golangci-lint behaviour.
-func HasDirective(position token.Position, idx map[string]map[int]struct{}) bool {
-	if position.Filename == "" {
-		return false
-	}
+// DirectiveIndex records nolint directives by filename, line, and linter name.
+type DirectiveIndex map[string]map[int]map[string]struct{}
 
-	noLintLines := idx[position.Filename]
-	if noLintLines == nil {
-		return false
-	}
+// Analyzer builds a shared nolint directive index once per package so analyzers
+// can reuse it via pass.ResultOf.
+var Analyzer = &analysis.Analyzer{
+	Name:             "nolintindex",
+	Doc:              "indexes nolint directives for gh-aw custom linters",
+	ResultType:       reflect.TypeFor[DirectiveIndex](),
+	RunDespiteErrors: true,
+	Run: func(pass *analysis.Pass) (any, error) {
+		return BuildDirectiveIndex(pass), nil
+	},
+}
 
-	_, sameLine := noLintLines[position.Line]
-	_, previousLine := noLintLines[position.Line-1]
-	return sameLine || previousLine
+// Index returns the shared nolint directive index for pass.
+func Index(pass *analysis.Pass) (DirectiveIndex, error) {
+	idx, ok := pass.ResultOf[Analyzer].(DirectiveIndex)
+	if !ok {
+		return nil, fmt.Errorf("nolint analyzer result has unexpected type %T", pass.ResultOf[Analyzer])
+	}
+	return idx, nil
 }
 
-// BuildLineIndex scans all comments in the analysis pass and returns a map
-// from filename → set of line numbers that carry a nolint directive for
-// linterName (e.g. "errstringmatch") or "all".
-func BuildLineIndex(pass *analysis.Pass, linterName string) map[string]map[int]struct{} {
-	noLintLinesByFile := make(map[string]map[int]struct{}, len(pass.Files))
+// BuildDirectiveIndex scans all comments in the analysis pass and returns a map
+// from filename → line → set of linter names that carry a nolint directive.
+func BuildDirectiveIndex(pass *analysis.Pass) DirectiveIndex {
+	noLintLinesByFile := make(DirectiveIndex, len(pass.Files))
 	for _, file := range pass.Files {
 		filename := pass.Fset.PositionFor(file.Pos(), false).Filename
 		if filename == "" {
@@ -51,28 +58,92 @@ func BuildLineIndex(pass *analysis.Pass, linterName string) map[string]map[int]s
 				if i := strings.IndexAny(payload, " \t"); i >= 0 {
 					payload = payload[:i]
 				}
-				matched := false
+				line := pass.Fset.PositionFor(comment.Slash, false).Line
+				if noLintLinesByFile[filename] == nil {
+					noLintLinesByFile[filename] = make(map[int]map[string]struct{})
+				}
+				if noLintLinesByFile[filename][line] == nil {
+					noLintLinesByFile[filename][line] = make(map[string]struct{})
+				}
 				for token := range strings.SplitSeq(payload, ",") {
 					name := strings.TrimSpace(token)
-					if name == linterName || name == "all" {
-						matched = true
-						break
+					if name == "" {
+						continue
 					}
+					noLintLinesByFile[filename][line][name] = struct{}{}
 				}
-				if !matched {
-					continue
-				}
-				line := pass.Fset.PositionFor(comment.Slash, false).Line
-				if noLintLinesByFile[filename] == nil {
-					noLintLinesByFile[filename] = make(map[int]struct{})
-				}
-				noLintLinesByFile[filename][line] = struct{}{}
 			}
 		}
 	}
 	return noLintLinesByFile
 }
 
+// HasDirective reports whether the given source position is covered by a
+// suppression directive for linterName (or "nolint:all").  Both same-line and
+// previous-line directives are recognised, matching golangci-lint behaviour.
+func HasDirective(position token.Position, idx map[string]map[int]struct{}) bool {
+	if position.Filename == "" {
+		return false
+	}
+
+	noLintLines := idx[position.Filename]
+	if noLintLines == nil {
+		return false
+	}
+
+	_, sameLine := noLintLines[position.Line]
+	_, previousLine := noLintLines[position.Line-1]
+	return sameLine || previousLine
+}
+
+// HasDirectiveForLinter reports whether the given source position is covered by
+// a suppression directive for linterName (or "nolint:all"). Both same-line and
+// previous-line directives are recognised, matching golangci-lint behaviour.
+func HasDirectiveForLinter(position token.Position, idx DirectiveIndex, linterName string) bool {
+	if position.Filename == "" {
+		return false
+	}
+	return hasDirectiveForLine(position.Line, idx[position.Filename], linterName) ||
+		hasDirectiveForLine(position.Line-1, idx[position.Filename], linterName)
+}
+
+// BuildLineIndex scans all comments in the analysis pass and returns a map
+// from filename → set of line numbers that carry a nolint directive for
+// linterName (e.g. "errstringmatch") or "all".
+func BuildLineIndex(pass *analysis.Pass, linterName string) map[string]map[int]struct{} {
+	noLintLinesByFile := make(map[string]map[int]struct{}, len(pass.Files))
+	for filename, lines := range BuildDirectiveIndex(pass) {
+		for line, names := range lines {
+			if !hasDirectiveName(names, linterName) {
+				continue
+			}
+			if noLintLinesByFile[filename] == nil {
+				noLintLinesByFile[filename] = make(map[int]struct{})
+			}
+			noLintLinesByFile[filename][line] = struct{}{}
+		}
+	}
+	return noLintLinesByFile
+}
+
+func hasDirectiveForLine(line int, lines map[int]map[string]struct{}, linterName string) bool {
+	if lines == nil {
+		return false
+	}
+	return hasDirectiveName(lines[line], linterName)
+}
+
+func hasDirectiveName(names map[string]struct{}, linterName string) bool {
+	if names == nil {
+		return false
+	}
+	if _, ok := names[linterName]; ok {
+		return true
+	}
+	_, ok := names["all"]
+	return ok
+}
+
 // ImplementsError reports whether t implements the built-in error interface.
 func ImplementsError(t types.Type) bool {
 	obj := types.Universe.Lookup("error")
diff --git a/pkg/linters/internal/nolint/nolint_test.go b/pkg/linters/internal/nolint/nolint_test.go
index 2191cf71abb..5b609912e9f 100644
--- a/pkg/linters/internal/nolint/nolint_test.go
+++ b/pkg/linters/internal/nolint/nolint_test.go
@@ -46,6 +46,17 @@ var _ = 5
 	if _, ok := lines[9]; ok {
 		t.Fatalf("line 9 unexpectedly matched prefix-only directive")
 	}
+
+	shared := BuildDirectiveIndex(&analysis.Pass{Fset: fset, Files: []*ast.File{file}})
+	if !HasDirectiveForLinter(token.Position{Filename: filename, Line: 4}, shared, "tolowerequalfold") {
+		t.Fatalf("expected previous-line shared directive match")
+	}
+	if !HasDirectiveForLinter(token.Position{Filename: filename, Line: 13}, shared, "differentlinter") {
+		t.Fatalf("expected nolint:all shared directive match")
+	}
+	if HasDirectiveForLinter(token.Position{Filename: filename, Line: 10}, shared, "tolowerequalfold") {
+		t.Fatalf("unexpected shared directive match for prefix-only directive")
+	}
 }
 
 func TestHasDirective_SameLineAndPreviousLine(t *testing.T) {
diff --git a/pkg/linters/ioutildeprecated/ioutildeprecated.go b/pkg/linters/ioutildeprecated/ioutildeprecated.go
index 99ab41252b1..d9a48ab7d75 100644
--- a/pkg/linters/ioutildeprecated/ioutildeprecated.go
+++ b/pkg/linters/ioutildeprecated/ioutildeprecated.go
@@ -32,7 +32,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "ioutildeprecated",
 	Doc:      "reports uses of deprecated io/ioutil functions that should be replaced with io or os package equivalents",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/ioutildeprecated",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -41,7 +41,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "ioutildeprecated")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	if pass.TypesInfo == nil {
 		return nil, nil
@@ -55,10 +62,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(sel.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "ioutildeprecated") {
 			continue
 		}
 
@@ -97,10 +104,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(ident.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "ioutildeprecated") {
 			continue
 		}
 
diff --git a/pkg/linters/jsonmarshalignoredeerror/jsonmarshalignoredeerror.go b/pkg/linters/jsonmarshalignoredeerror/jsonmarshalignoredeerror.go
index 9fb52057ac0..9935ae603e0 100644
--- a/pkg/linters/jsonmarshalignoredeerror/jsonmarshalignoredeerror.go
+++ b/pkg/linters/jsonmarshalignoredeerror/jsonmarshalignoredeerror.go
@@ -18,7 +18,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "jsonmarshalignoredeerror",
 	Doc:      "reports json.Marshal and json.Unmarshal calls where the error return is discarded",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/jsonmarshalignoredeerror",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer},
 	Run:      run,
 }
 
@@ -27,27 +27,30 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "jsonmarshalignoredeerror")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 	nodeFilter := []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)}
 	insp.Preorder(nodeFilter, func(n ast.Node) {
 		switch stmt := n.(type) {
 		case *ast.AssignStmt:
-			checkDiscardedJSONAssign(pass, stmt, noLintLinesByFile)
+			checkDiscardedJSONAssign(pass, stmt, noLintIndex)
 		case *ast.ExprStmt:
-			checkDiscardedJSONExpr(pass, stmt, noLintLinesByFile)
+			checkDiscardedJSONExpr(pass, stmt, noLintIndex)
 		}
 	})
 	return nil, nil
 }
 
-func checkDiscardedJSONAssign(pass *analysis.Pass, assign *ast.AssignStmt, noLintLinesByFile map[string]map[int]struct{}) {
+func checkDiscardedJSONAssign(pass *analysis.Pass, assign *ast.AssignStmt, noLintIndex nolint.DirectiveIndex) {
 	// Pattern: val, _ := json.Marshal(x)  — 2 lhs, 1 rhs, Lhs[1] is blank
 	if len(assign.Lhs) == 2 && len(assign.Rhs) == 1 {
 		blank, ok := assign.Lhs[1].(*ast.Ident)
 		if ok && blank.Name == "_" {
 			call, ok := assign.Rhs[0].(*ast.CallExpr)
 			if ok && isJSONFunc(pass, call, "Marshal") {
-				reportDiscardedJSONCall(pass, call, noLintLinesByFile, "error return from json.Marshal is discarded; marshal failures produce nil bytes silently")
+				reportDiscardedJSONCall(pass, call, noLintIndex, "error return from json.Marshal is discarded; marshal failures produce nil bytes silently")
 			}
 		}
 	}
@@ -58,29 +61,29 @@ func checkDiscardedJSONAssign(pass *analysis.Pass, assign *ast.AssignStmt, noLin
 		if ok && blank.Name == "_" {
 			call, ok := assign.Rhs[0].(*ast.CallExpr)
 			if ok && isJSONFunc(pass, call, "Unmarshal") {
-				reportDiscardedJSONCall(pass, call, noLintLinesByFile, "error return from json.Unmarshal is discarded; unmarshal failures leave the target value in a partial state")
+				reportDiscardedJSONCall(pass, call, noLintIndex, "error return from json.Unmarshal is discarded; unmarshal failures leave the target value in a partial state")
 			}
 		}
 	}
 }
 
-func checkDiscardedJSONExpr(pass *analysis.Pass, stmt *ast.ExprStmt, noLintLinesByFile map[string]map[int]struct{}) {
+func checkDiscardedJSONExpr(pass *analysis.Pass, stmt *ast.ExprStmt, noLintIndex nolint.DirectiveIndex) {
 	call, ok := stmt.X.(*ast.CallExpr)
 	if !ok {
 		return
 	}
 	if isJSONFunc(pass, call, "Marshal") {
-		reportDiscardedJSONCall(pass, call, noLintLinesByFile, "error return from json.Marshal is discarded; marshal failures produce nil bytes silently")
+		reportDiscardedJSONCall(pass, call, noLintIndex, "error return from json.Marshal is discarded; marshal failures produce nil bytes silently")
 		return
 	}
 	if isJSONFunc(pass, call, "Unmarshal") {
-		reportDiscardedJSONCall(pass, call, noLintLinesByFile, "error return from json.Unmarshal is discarded; unmarshal failures leave the target value in a partial state")
+		reportDiscardedJSONCall(pass, call, noLintIndex, "error return from json.Unmarshal is discarded; unmarshal failures leave the target value in a partial state")
 	}
 }
 
-func reportDiscardedJSONCall(pass *analysis.Pass, call *ast.CallExpr, noLintLinesByFile map[string]map[int]struct{}, message string) {
+func reportDiscardedJSONCall(pass *analysis.Pass, call *ast.CallExpr, noLintIndex nolint.DirectiveIndex, message string) {
 	position := pass.Fset.PositionFor(call.Pos(), false)
-	if nolint.HasDirective(position, noLintLinesByFile) {
+	if nolint.HasDirectiveForLinter(position, noLintIndex, "jsonmarshalignoredeerror") {
 		return
 	}
 	pass.ReportRangef(call, "%s", message)
diff --git a/pkg/linters/largefunc/largefunc.go b/pkg/linters/largefunc/largefunc.go
index 1fbf005cc48..bff3f3c7ba7 100644
--- a/pkg/linters/largefunc/largefunc.go
+++ b/pkg/linters/largefunc/largefunc.go
@@ -24,7 +24,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "largefunc",
 	Doc:      "reports functions whose body exceeds the line limit (default 60 lines)",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/largefunc",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -43,7 +43,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "largefunc")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
@@ -71,7 +78,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		position := pass.Fset.PositionFor(reportNode.Pos(), false)
-		if filecheck.IsTestFile(position.Filename) {
+		if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
 			return
 		}
 
@@ -81,7 +88,7 @@ func run(pass *analysis.Pass) (any, error) {
 		lines := end.Line - start.Line - 1
 
 		if lines > maxLines {
-			if nolint.HasDirective(position, noLintLinesByFile) {
+			if nolint.HasDirectiveForLinter(position, noLintIndex, "largefunc") {
 				return
 			}
 			pkgLog.Printf("flagging %s: %d lines exceeds limit %d", name, lines, maxLines)
diff --git a/pkg/linters/lenstringsplit/lenstringsplit.go b/pkg/linters/lenstringsplit/lenstringsplit.go
index 961f054e123..29d5a14005e 100644
--- a/pkg/linters/lenstringsplit/lenstringsplit.go
+++ b/pkg/linters/lenstringsplit/lenstringsplit.go
@@ -24,7 +24,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "lenstringsplit",
 	Doc:      "reports len(strings.Split(s, sep)) expressions with a provably non-empty separator that allocate a []string just to count substrings; use strings.Count(s, sep)+1 instead",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/lenstringsplit",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -33,7 +33,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "lenstringsplit")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
 
@@ -62,10 +69,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(outer.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "lenstringsplit") {
 			return
 		}
 
diff --git a/pkg/linters/lenstringzero/lenstringzero.go b/pkg/linters/lenstringzero/lenstringzero.go
index d3272ceb336..16ff2e04c75 100644
--- a/pkg/linters/lenstringzero/lenstringzero.go
+++ b/pkg/linters/lenstringzero/lenstringzero.go
@@ -23,7 +23,7 @@ var Analyzer = &analysis.Analyzer{
 		"(len(s) > 0, len(s) >= 1, len(s) < 1, len(s) <= 0) on string values " +
 		"that should use == \"\" or != \"\" instead",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/lenstringzero",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -32,7 +32,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "lenstringzero")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 	lenStringAliases := collectLenStringAliases(pass)
 
 	nodeFilter := []ast.Node{(*ast.BinaryExpr)(nil)}
@@ -49,10 +56,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(expr.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "lenstringzero") {
 			return
 		}
 
diff --git a/pkg/linters/logfatallibrary/logfatallibrary.go b/pkg/linters/logfatallibrary/logfatallibrary.go
index 75477f81998..3b05f2ad4fd 100644
--- a/pkg/linters/logfatallibrary/logfatallibrary.go
+++ b/pkg/linters/logfatallibrary/logfatallibrary.go
@@ -28,7 +28,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "logfatallibrary",
 	Doc:      "reports log.Fatal, log.Fatalf, and log.Fatalln calls inside library packages where they implicitly call os.Exit and bypass deferred cleanup",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/logfatallibrary",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -43,7 +43,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "logfatallibrary")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -54,7 +61,7 @@ func run(pass *analysis.Pass) (any, error) {
 		if !ok {
 			return
 		}
-		if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {
+		if strings.HasSuffix(pkgPath, ".test") || filecheck.ShouldSkipFilename(pass.Fset.Position(call.Pos()).Filename, generatedFiles) {
 			return
 		}
 		sel, ok := call.Fun.(*ast.SelectorExpr)
@@ -68,7 +75,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 		position := pass.Fset.PositionFor(call.Pos(), false)
-		if nolint.HasDirective(position, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(position, noLintIndex, "logfatallibrary") {
 			return
 		}
 		pass.ReportRangef(call, "log.%s called in library package %s; use error returns instead to avoid implicit os.Exit", sel.Sel.Name, pkgPath)
diff --git a/pkg/linters/manualmutexunlock/manualmutexunlock.go b/pkg/linters/manualmutexunlock/manualmutexunlock.go
index ff6dc38ce44..798c875df60 100644
--- a/pkg/linters/manualmutexunlock/manualmutexunlock.go
+++ b/pkg/linters/manualmutexunlock/manualmutexunlock.go
@@ -40,7 +40,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "manualmutexunlock",
 	Doc:      "reports mutex Unlock() calls that are not deferred",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/manualmutexunlock",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -49,27 +49,34 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "manualmutexunlock")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
 	}
 
 	insp.Preorder(nodeFilter, func(n ast.Node) {
-		inspectMutexFuncDecl(pass, noLintLinesByFile, n)
+		inspectMutexFuncDecl(pass, noLintIndex, generatedFiles, n)
 	})
 
 	return nil, nil
 }
 
-func inspectMutexFuncDecl(pass *analysis.Pass, noLintLinesByFile map[string]map[int]struct{}, n ast.Node) {
+func inspectMutexFuncDecl(pass *analysis.Pass, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex, n ast.Node) {
 	fn, ok := n.(*ast.FuncDecl)
 	if !ok || fn.Body == nil {
 		return
 	}
 
 	pos := pass.Fset.PositionFor(fn.Pos(), false)
-	if filecheck.IsTestFile(pos.Filename) {
+	if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 		return
 	}
 
@@ -78,14 +85,14 @@ func inspectMutexFuncDecl(pass *analysis.Pass, noLintLinesByFile map[string]map[
 
 	// Walk all statements in the function body
 	ast.Inspect(fn.Body, func(node ast.Node) bool {
-		return inspectMutexNode(pass, noLintLinesByFile, mutexVars, node)
+		return inspectMutexNode(pass, noLintIndex, mutexVars, node)
 	})
 
 	// Report mutexes with manual unlock but no defer
 	for _, state := range mutexVars {
 		if state.hasManualUnlock && !state.hasDefer {
 			position := pass.Fset.PositionFor(state.lockPos, false)
-			if nolint.HasDirective(position, noLintLinesByFile) {
+			if nolint.HasDirectiveForLinter(position, noLintIndex, "manualmutexunlock") {
 				continue
 			}
 			pass.Report(analysis.Diagnostic{
@@ -96,7 +103,7 @@ func inspectMutexFuncDecl(pass *analysis.Pass, noLintLinesByFile map[string]map[
 	}
 }
 
-func inspectMutexNode(pass *analysis.Pass, noLintLinesByFile map[string]map[int]struct{}, mutexVars map[mutexKey]*mutexVarState, node ast.Node) bool {
+func inspectMutexNode(pass *analysis.Pass, noLintIndex nolint.DirectiveIndex, mutexVars map[mutexKey]*mutexVarState, node ast.Node) bool {
 	if node == nil {
 		return false
 	}
@@ -114,7 +121,7 @@ func inspectMutexNode(pass *analysis.Pass, noLintLinesByFile map[string]map[int]
 				// binding, report any unresolved violation before overwriting state.
 				if prev, exists := mutexVars[key]; exists && prev.hasManualUnlock && !prev.hasDefer {
 					position := pass.Fset.PositionFor(prev.lockPos, false)
-					if nolint.HasDirective(position, noLintLinesByFile) {
+					if nolint.HasDirectiveForLinter(position, noLintIndex, "manualmutexunlock") {
 						mutexVars[key] = &mutexVarState{
 							lockPos: call.Pos(),
 						}
diff --git a/pkg/linters/mapdeletecheck/mapdeletecheck.go b/pkg/linters/mapdeletecheck/mapdeletecheck.go
index 490d6b249bf..f4a0994eee4 100644
--- a/pkg/linters/mapdeletecheck/mapdeletecheck.go
+++ b/pkg/linters/mapdeletecheck/mapdeletecheck.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "mapdeletecheck",
 	Doc:      "reports redundant map membership checks before delete(m, k) calls since delete is already a no-op for missing keys",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/mapdeletecheck",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "mapdeletecheck")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{(*ast.IfStmt)(nil)}
 
@@ -41,10 +48,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(ifStmt.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "mapdeletecheck") {
 			return
 		}
 
diff --git a/pkg/linters/osexitinlibrary/osexitinlibrary.go b/pkg/linters/osexitinlibrary/osexitinlibrary.go
index b705386f03e..0f635ab80bb 100644
--- a/pkg/linters/osexitinlibrary/osexitinlibrary.go
+++ b/pkg/linters/osexitinlibrary/osexitinlibrary.go
@@ -19,7 +19,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "osexitinlibrary",
 	Doc:      "reports os.Exit calls inside library packages where they bypass deferred cleanup and prevent testing",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/osexitinlibrary",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -34,7 +34,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "osexitinlibrary")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -45,7 +52,7 @@ func run(pass *analysis.Pass) (any, error) {
 		if !ok {
 			return
 		}
-		if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {
+		if strings.HasSuffix(pkgPath, ".test") || filecheck.ShouldSkipFilename(pass.Fset.Position(call.Pos()).Filename, generatedFiles) {
 			return
 		}
 		sel, ok := call.Fun.(*ast.SelectorExpr)
@@ -54,7 +61,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 		if astutil.IsPkgSelector(pass, sel, "os") && sel.Sel.Name == "Exit" {
 			position := pass.Fset.PositionFor(call.Pos(), false)
-			if nolint.HasDirective(position, noLintLinesByFile) {
+			if nolint.HasDirectiveForLinter(position, noLintIndex, "osexitinlibrary") {
 				return
 			}
 			pass.ReportRangef(call, "os.Exit called in library package %s; move process termination to a cmd/ entry-point", pkgPath)
diff --git a/pkg/linters/osgetenvlibrary/osgetenvlibrary.go b/pkg/linters/osgetenvlibrary/osgetenvlibrary.go
index 84f6b6204dc..ad8a42e4b4a 100644
--- a/pkg/linters/osgetenvlibrary/osgetenvlibrary.go
+++ b/pkg/linters/osgetenvlibrary/osgetenvlibrary.go
@@ -19,7 +19,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "osgetenvlibrary",
 	Doc:      "reports calls to os.Getenv or os.LookupEnv in non-main, non-test packages",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/osgetenvlibrary",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -33,7 +33,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "osgetenvlibrary")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -45,7 +52,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 
-		if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.PositionFor(call.Pos(), false).Filename) {
+		if strings.HasSuffix(pkgPath, ".test") || filecheck.ShouldSkipFilename(pass.Fset.PositionFor(call.Pos(), false).Filename, generatedFiles) {
 			return
 		}
 
@@ -54,7 +61,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 		position := pass.Fset.PositionFor(call.Pos(), false)
-		if nolint.HasDirective(position, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(position, noLintIndex, "osgetenvlibrary") {
 			return
 		}
 		switch fn.Name() {
diff --git a/pkg/linters/ossetenvlibrary/ossetenvlibrary.go b/pkg/linters/ossetenvlibrary/ossetenvlibrary.go
index e7ce83937a1..21809e465e1 100644
--- a/pkg/linters/ossetenvlibrary/ossetenvlibrary.go
+++ b/pkg/linters/ossetenvlibrary/ossetenvlibrary.go
@@ -19,7 +19,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "ossetenvlibrary",
 	Doc:      "reports calls to os.Setenv or os.Unsetenv in non-main, non-test packages",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/ossetenvlibrary",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -33,7 +33,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "ossetenvlibrary")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -45,7 +52,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 
-		if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.PositionFor(call.Pos(), false).Filename) {
+		if strings.HasSuffix(pkgPath, ".test") || filecheck.ShouldSkipFilename(pass.Fset.PositionFor(call.Pos(), false).Filename, generatedFiles) {
 			return
 		}
 
@@ -54,7 +61,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 		position := pass.Fset.PositionFor(call.Pos(), false)
-		if nolint.HasDirective(position, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(position, noLintIndex, "ossetenvlibrary") {
 			return
 		}
 		switch fn.Name() {
diff --git a/pkg/linters/panic-in-library-code/panic-in-library-code.go b/pkg/linters/panic-in-library-code/panic-in-library-code.go
index f92eefe4e5c..b83091baf8f 100644
--- a/pkg/linters/panic-in-library-code/panic-in-library-code.go
+++ b/pkg/linters/panic-in-library-code/panic-in-library-code.go
@@ -24,7 +24,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "panicinlibrarycode",
 	Doc:      "reports panic() calls in library code under pkg/ that should return errors instead",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/panic-in-library-code",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -39,7 +39,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "panicinlibrarycode")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cur.Node().(*ast.CallExpr)
@@ -47,7 +54,7 @@ func run(pass *analysis.Pass) (any, error) {
 			continue
 		}
 		// Skip test files
-		if strings.HasSuffix(pkgPath, ".test") || filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {
+		if strings.HasSuffix(pkgPath, ".test") || filecheck.ShouldSkipFilename(pass.Fset.Position(call.Pos()).Filename, generatedFiles) {
 			continue
 		}
 
@@ -68,7 +75,7 @@ func run(pass *analysis.Pass) (any, error) {
 			continue
 		}
 		position := pass.Fset.PositionFor(call.Pos(), false)
-		if nolint.HasDirective(position, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(position, noLintIndex, "panicinlibrarycode") {
 			continue
 		}
 
diff --git a/pkg/linters/rawloginlib/rawloginlib.go b/pkg/linters/rawloginlib/rawloginlib.go
index 5324e680916..7b896a546e2 100644
--- a/pkg/linters/rawloginlib/rawloginlib.go
+++ b/pkg/linters/rawloginlib/rawloginlib.go
@@ -18,7 +18,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "rawloginlib",
 	Doc:      "reports use of the standard log package in library packages where pkg/logger should be used instead",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/rawloginlib",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -38,7 +38,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "rawloginlib")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
 
@@ -47,7 +54,7 @@ func run(pass *analysis.Pass) (any, error) {
 		if !ok {
 			return
 		}
-		if filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {
+		if filecheck.ShouldSkipFilename(pass.Fset.Position(call.Pos()).Filename, generatedFiles) {
 			return
 		}
 		sel, ok := call.Fun.(*ast.SelectorExpr)
@@ -61,7 +68,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 		position := pass.Fset.PositionFor(call.Pos(), false)
-		if nolint.HasDirective(position, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(position, noLintIndex, "rawloginlib") {
 			return
 		}
 		pass.ReportRangef(call, "log.%s called in library package %s; use pkg/logger instead", sel.Sel.Name, pkgPath)
diff --git a/pkg/linters/regexpcompileinfunction/regexpcompileinfunction.go b/pkg/linters/regexpcompileinfunction/regexpcompileinfunction.go
index 01362d544dc..bfa2f759265 100644
--- a/pkg/linters/regexpcompileinfunction/regexpcompileinfunction.go
+++ b/pkg/linters/regexpcompileinfunction/regexpcompileinfunction.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "regexpcompileinfunction",
 	Doc:      "reports regexp.MustCompile and regexp.Compile calls inside function bodies that should be moved to package-level variables",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/regexpcompileinfunction",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "regexpcompileinfunction")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cur.Node().(*ast.CallExpr)
@@ -42,7 +49,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
 
@@ -55,7 +62,7 @@ func run(pass *analysis.Pass) (any, error) {
 		if !inside {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "regexpcompileinfunction") {
 			continue
 		}
 		pass.Report(analysis.Diagnostic{
diff --git a/pkg/linters/seenmapbool/seenmapbool.go b/pkg/linters/seenmapbool/seenmapbool.go
index 2da5836e009..a2e41e6413a 100644
--- a/pkg/linters/seenmapbool/seenmapbool.go
+++ b/pkg/linters/seenmapbool/seenmapbool.go
@@ -20,7 +20,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "seenmapbool",
 	Doc:      "reports map[string]bool used as a set (values always true) where map[string]struct{} should be used instead",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/seenmapbool",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -29,7 +29,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "seenmapbool")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
@@ -44,7 +51,7 @@ func run(pass *analysis.Pass) (any, error) {
 				return
 			}
 			pos := pass.Fset.PositionFor(fn.Pos(), false)
-			if filecheck.IsTestFile(pos.Filename) {
+			if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 				return
 			}
 			body = fn.Body
@@ -54,7 +61,7 @@ func run(pass *analysis.Pass) (any, error) {
 			}
 			body = fn.Body
 		}
-		inspectBody(pass, body, noLintLinesByFile)
+		inspectBody(pass, body, noLintIndex)
 	})
 
 	return nil, nil
@@ -62,7 +69,7 @@ func run(pass *analysis.Pass) (any, error) {
 
 // inspectBody walks a function body and reports map[string]bool variables
 // that are only ever assigned the literal true (i.e., used as a set).
-func inspectBody(pass *analysis.Pass, body *ast.BlockStmt, noLintLinesByFile map[string]map[int]struct{}) {
+func inspectBody(pass *analysis.Pass, body *ast.BlockStmt, noLintIndex nolint.DirectiveIndex) {
 	// Collect map[string]bool local variables defined in this scope.
 	candidates := make(map[types.Object]ast.Node) // object -> declaration node for reporting
 
@@ -170,7 +177,7 @@ func inspectBody(pass *analysis.Pass, body *ast.BlockStmt, noLintLinesByFile map
 		if nonSetMaps[obj] {
 			continue
 		}
-		if nolint.HasDirective(pass.Fset.PositionFor(declNode.Pos(), false), noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pass.Fset.PositionFor(declNode.Pos(), false), noLintIndex, "seenmapbool") {
 			continue
 		}
 		pass.ReportRangef(
diff --git a/pkg/linters/sortslice/sortslice.go b/pkg/linters/sortslice/sortslice.go
index 70c60e48ecf..c8c6a14c963 100644
--- a/pkg/linters/sortslice/sortslice.go
+++ b/pkg/linters/sortslice/sortslice.go
@@ -20,7 +20,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "sortslice",
 	Doc:      "reports sort.Slice and sort.SliceStable calls that should use the type-safe slices.SortFunc or slices.SortStableFunc",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/sortslice",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -29,7 +29,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "sortslice")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range root.Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cur.Node().(*ast.CallExpr)
@@ -38,10 +45,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "sortslice") {
 			continue
 		}
 
diff --git a/pkg/linters/sprintferrdot/sprintferrdot.go b/pkg/linters/sprintferrdot/sprintferrdot.go
index 928ec079a01..92fa8be5f57 100644
--- a/pkg/linters/sprintferrdot/sprintferrdot.go
+++ b/pkg/linters/sprintferrdot/sprintferrdot.go
@@ -39,7 +39,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "sprintferrdot",
 	Doc:      "reports redundant .Error() calls on error arguments passed to fmt format functions",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/sprintferrdot",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -48,7 +48,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "sprintferrdot")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -61,7 +68,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
 
@@ -92,7 +99,7 @@ func run(pass *analysis.Pass) (any, error) {
 				continue
 			}
 			if isErrorDotCall(pass, arg) {
-				if nolint.HasDirective(pass.Fset.PositionFor(arg.Pos(), false), noLintLinesByFile) {
+				if nolint.HasDirectiveForLinter(pass.Fset.PositionFor(arg.Pos(), false), noLintIndex, "sprintferrdot") {
 					continue
 				}
 				pass.Reportf(arg.Pos(),
diff --git a/pkg/linters/sprintferrorsnew/sprintferrorsnew.go b/pkg/linters/sprintferrorsnew/sprintferrorsnew.go
index d049fadd27f..1ffe7da557c 100644
--- a/pkg/linters/sprintferrorsnew/sprintferrorsnew.go
+++ b/pkg/linters/sprintferrorsnew/sprintferrorsnew.go
@@ -18,7 +18,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "sprintferrorsnew",
 	Doc:      "reports errors.New(fmt.Sprintf(...)) calls that should use fmt.Errorf instead",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/sprintferrorsnew",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -27,7 +27,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "sprintferrorsnew")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -39,7 +46,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 
-		if filecheck.IsTestFile(pass.Fset.Position(call.Pos()).Filename) {
+		if filecheck.ShouldSkipFilename(pass.Fset.Position(call.Pos()).Filename, generatedFiles) {
 			return
 		}
 
@@ -69,7 +76,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		position := pass.Fset.PositionFor(call.Pos(), false)
-		if nolint.HasDirective(position, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(position, noLintIndex, "sprintferrorsnew") {
 			return
 		}
 		pass.Reportf(call.Pos(), "use fmt.Errorf instead of errors.New(fmt.Sprintf(...))")
diff --git a/pkg/linters/sprintfint/sprintfint.go b/pkg/linters/sprintfint/sprintfint.go
index 1ef47bd6d5b..99b53ce0f79 100644
--- a/pkg/linters/sprintfint/sprintfint.go
+++ b/pkg/linters/sprintfint/sprintfint.go
@@ -26,7 +26,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "sprintfint",
 	Doc:      `reports fmt.Sprintf("%d", x) calls where x is a single int value; use strconv.Itoa(x) instead`,
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/sprintfint",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -35,7 +35,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "sprintfint")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	// seenImportFiles tracks files that have already received an import edit in
 	// this pass, preventing duplicate overlapping edits when a single file
@@ -53,10 +60,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "sprintfint") {
 			return
 		}
 
diff --git a/pkg/linters/strconvparseignorederror/strconvparseignorederror.go b/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
index a1740ac4924..ed83ff106b0 100644
--- a/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
+++ b/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
@@ -19,7 +19,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "strconvparseignorederror",
 	Doc:      "reports strconv parsing calls where the error return is discarded with _",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/strconvparseignorederror",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer},
 	Run:      run,
 }
 
@@ -37,7 +37,10 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "strconvparseignorederror")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.AssignStmt)(nil),
@@ -75,7 +78,7 @@ func run(pass *analysis.Pass) (any, error) {
 			if pkgName, ok := obj.(*types.PkgName); ok {
 				if pkgName.Imported().Path() == "strconv" {
 					position := pass.Fset.PositionFor(call.Pos(), false)
-					if nolint.HasDirective(position, noLintLinesByFile) {
+					if nolint.HasDirectiveForLinter(position, noLintIndex, "strconvparseignorederror") {
 						return
 					}
 					pass.ReportRangef(call, "error return from strconv.%s is discarded; parse failures produce zero values silently", sel.Sel.Name)
diff --git a/pkg/linters/stringreplaceminusone/stringreplaceminusone.go b/pkg/linters/stringreplaceminusone/stringreplaceminusone.go
index 32581490a95..77bbc016f10 100644
--- a/pkg/linters/stringreplaceminusone/stringreplaceminusone.go
+++ b/pkg/linters/stringreplaceminusone/stringreplaceminusone.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "stringreplaceminusone",
 	Doc:      "reports strings.Replace calls with n=-1 that should use strings.ReplaceAll",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/stringreplaceminusone",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "stringreplaceminusone")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
 
@@ -41,10 +48,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "stringreplaceminusone") {
 			return
 		}
 
diff --git a/pkg/linters/stringscountcontains/stringscountcontains.go b/pkg/linters/stringscountcontains/stringscountcontains.go
index a67cfe85d5c..48011e733bd 100644
--- a/pkg/linters/stringscountcontains/stringscountcontains.go
+++ b/pkg/linters/stringscountcontains/stringscountcontains.go
@@ -22,7 +22,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "stringscountcontains",
 	Doc:      "reports strings.Count(s, sub) comparisons with 0 or 1 (e.g. > 0, >= 1, == 0, != 0, < 1, <= 0) and their yoda-order variants that should use strings.Contains(s, sub) or !strings.Contains(s, sub)",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/stringscountcontains",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -31,7 +31,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "stringscountcontains")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{(*ast.BinaryExpr)(nil)}
 
@@ -42,10 +49,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(expr.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "stringscountcontains") {
 			return
 		}
 
diff --git a/pkg/linters/stringsindexcontains/stringsindexcontains.go b/pkg/linters/stringsindexcontains/stringsindexcontains.go
index fe6119213e8..c92f71e9870 100644
--- a/pkg/linters/stringsindexcontains/stringsindexcontains.go
+++ b/pkg/linters/stringsindexcontains/stringsindexcontains.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "stringsindexcontains",
 	Doc:      "reports strings.Index(s, substr) comparisons with -1 or 0 (e.g. != -1, >= 0, > -1, == -1, < 0, <= -1) and their yoda-order variants that should use strings.Contains(s, substr) or !strings.Contains(s, substr)",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/stringsindexcontains",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "stringsindexcontains")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{(*ast.BinaryExpr)(nil)}
 
@@ -41,10 +48,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(expr.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "stringsindexcontains") {
 			return
 		}
 
diff --git a/pkg/linters/stringsindexcontains/testdata/src/stringsindexcontains/generated.go b/pkg/linters/stringsindexcontains/testdata/src/stringsindexcontains/generated.go
new file mode 100644
index 00000000000..5493ee3bc9a
--- /dev/null
+++ b/pkg/linters/stringsindexcontains/testdata/src/stringsindexcontains/generated.go
@@ -0,0 +1,9 @@
+// Code generated by tests. DO NOT EDIT.
+
+package stringsindexcontains
+
+import "strings"
+
+func generatedContainsCheck(s, sub string) bool {
+	return strings.Index(s, sub) != -1
+}
diff --git a/pkg/linters/timeafterleak/timeafterleak.go b/pkg/linters/timeafterleak/timeafterleak.go
index 38c69dbbaae..33bcf1bb0de 100644
--- a/pkg/linters/timeafterleak/timeafterleak.go
+++ b/pkg/linters/timeafterleak/timeafterleak.go
@@ -23,7 +23,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "timeafterleak",
 	Doc:      "reports time.After calls used as the channel-receive expression in a select CommClause that is enclosed by a for or range loop; does not flag receives inside case bodies, single-case selects without a default, or selects enclosed only by a function literal boundary",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/timeafterleak",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -32,7 +32,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "timeafterleak")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cur.Node().(*ast.CallExpr)
@@ -44,10 +51,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "timeafterleak") {
 			continue
 		}
 
diff --git a/pkg/linters/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/timesleepnocontext.go
index baf734a9f33..f7599756593 100644
--- a/pkg/linters/timesleepnocontext/timesleepnocontext.go
+++ b/pkg/linters/timesleepnocontext/timesleepnocontext.go
@@ -22,7 +22,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "timesleepnocontext",
 	Doc:      "reports time.Sleep calls inside context-receiving functions where a context-aware select should be used to allow cancellation",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/timesleepnocontext",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -31,7 +31,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "timesleepnocontext")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) {
 		call, ok := cur.Node().(*ast.CallExpr)
@@ -43,10 +50,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			continue
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "timesleepnocontext") {
 			continue
 		}
 
diff --git a/pkg/linters/tolowerequalfold/tolowerequalfold.go b/pkg/linters/tolowerequalfold/tolowerequalfold.go
index 5db3d03961c..81ba51567cc 100644
--- a/pkg/linters/tolowerequalfold/tolowerequalfold.go
+++ b/pkg/linters/tolowerequalfold/tolowerequalfold.go
@@ -24,7 +24,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "tolowerequalfold",
 	Doc:      "reports case-insensitive string comparisons using strings.ToLower/ToUpper that should use strings.EqualFold",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/tolowerequalfold",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -33,7 +33,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "tolowerequalfold")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 	caseConvAliases := collectCaseConvAliases(pass)
 
 	nodeFilter := []ast.Node{
@@ -49,7 +56,7 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 
-		if filecheck.IsTestFile(pass.Fset.Position(expr.Pos()).Filename) {
+		if filecheck.ShouldSkipFilename(pass.Fset.Position(expr.Pos()).Filename, generatedFiles) {
 			return
 		}
 
@@ -67,7 +74,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		if isEquivalentToEqualFold(pass, expr, caseConvAliases) {
-			if nolint.HasDirective(pass.Fset.PositionFor(expr.Pos(), false), noLintLinesByFile) {
+			if nolint.HasDirectiveForLinter(pass.Fset.PositionFor(expr.Pos(), false), noLintIndex, "tolowerequalfold") {
 				return
 			}
 			pass.Report(analysis.Diagnostic{
diff --git a/pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go b/pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go
index f7b4ec47677..63ef8ee1b94 100644
--- a/pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go
+++ b/pkg/linters/uncheckedtypeassertion/uncheckedtypeassertion.go
@@ -19,7 +19,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "uncheckedtypeassertion",
 	Doc:      "reports single-value type assertions that may panic if the dynamic type does not match",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/uncheckedtypeassertion",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -28,7 +28,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "uncheckedtypeassertion")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	// Build a parent map for each file so we can detect the two-value form.
 	fileParents := make(map[*ast.File]map[ast.Node]ast.Node)
@@ -41,13 +48,13 @@ func run(pass *analysis.Pass) (any, error) {
 	}
 
 	insp.Preorder(nodeFilter, func(n ast.Node) {
-		inspectTypeAssertExpr(pass, noLintLinesByFile, fileParents, n)
+		inspectTypeAssertExpr(pass, noLintIndex, generatedFiles, fileParents, n)
 	})
 
 	return nil, nil
 }
 
-func inspectTypeAssertExpr(pass *analysis.Pass, noLintLinesByFile map[string]map[int]struct{}, fileParents map[*ast.File]map[ast.Node]ast.Node, n ast.Node) {
+func inspectTypeAssertExpr(pass *analysis.Pass, noLintIndex nolint.DirectiveIndex, generatedFiles filecheck.GeneratedIndex, fileParents map[*ast.File]map[ast.Node]ast.Node, n ast.Node) {
 	typeAssert, ok := n.(*ast.TypeAssertExpr)
 	if !ok {
 		return
@@ -59,7 +66,7 @@ func inspectTypeAssertExpr(pass *analysis.Pass, noLintLinesByFile map[string]map
 	}
 
 	pos := pass.Fset.PositionFor(typeAssert.Pos(), false)
-	if filecheck.IsTestFile(pos.Filename) {
+	if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 		return
 	}
 
@@ -83,7 +90,7 @@ func inspectTypeAssertExpr(pass *analysis.Pass, noLintLinesByFile map[string]map
 	if t == nil {
 		return
 	}
-	if nolint.HasDirective(pos, noLintLinesByFile) {
+	if nolint.HasDirectiveForLinter(pos, noLintIndex, "uncheckedtypeassertion") {
 		return
 	}
 
diff --git a/pkg/linters/wgdonenotdeferred/wgdonenotdeferred.go b/pkg/linters/wgdonenotdeferred/wgdonenotdeferred.go
index 3addc8db761..91e18bc1984 100644
--- a/pkg/linters/wgdonenotdeferred/wgdonenotdeferred.go
+++ b/pkg/linters/wgdonenotdeferred/wgdonenotdeferred.go
@@ -21,7 +21,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "wgdonenotdeferred",
 	Doc:      "reports sync.WaitGroup Done() calls that are not deferred, which can cause deadlock if the function panics",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/wgdonenotdeferred",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -30,7 +30,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "wgdonenotdeferred")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.FuncDecl)(nil),
@@ -42,16 +49,16 @@ func run(pass *analysis.Pass) (any, error) {
 			return
 		}
 		pos := pass.Fset.PositionFor(fn.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		inspectBody(pass, noLintLinesByFile, fn.Body)
+		inspectBody(pass, noLintIndex, fn.Body)
 	})
 
 	return nil, nil
 }
 
-func inspectBody(pass *analysis.Pass, noLintLinesByFile map[string]map[int]struct{}, body *ast.BlockStmt) {
+func inspectBody(pass *analysis.Pass, noLintIndex nolint.DirectiveIndex, body *ast.BlockStmt) {
 	var stack []ast.Node
 
 	ast.Inspect(body, func(node ast.Node) bool {
@@ -91,7 +98,7 @@ func inspectBody(pass *analysis.Pass, noLintLinesByFile map[string]map[int]struc
 				if call, ok := exprStmt.X.(*ast.CallExpr); ok {
 					if isWaitGroupDone(pass, call) {
 						pos := pass.Fset.PositionFor(call.Pos(), false)
-						if !nolint.HasDirective(pos, noLintLinesByFile) {
+						if !nolint.HasDirectiveForLinter(pos, noLintIndex, "wgdonenotdeferred") {
 							pass.ReportRangef(call,
 								"sync.WaitGroup Done() should be deferred to prevent deadlock if the function panics")
 						}
diff --git a/pkg/linters/writebytestring/writebytestring.go b/pkg/linters/writebytestring/writebytestring.go
index 15ee19e96de..fcfcb9dc1a6 100644
--- a/pkg/linters/writebytestring/writebytestring.go
+++ b/pkg/linters/writebytestring/writebytestring.go
@@ -47,7 +47,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "writebytestring",
 	Doc:      "reports w.Write([]byte(s)) calls where s is a string that can be replaced with io.WriteString(w, s)",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/writebytestring",
-	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -56,7 +56,14 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintLinesByFile := nolint.BuildLineIndex(pass, "writebytestring")
+	noLintIndex, err := nolint.Index(pass)
+	if err != nil {
+		return nil, err
+	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 	filesWithImportEdit := make(map[token.Pos]bool)
 
 	nodeFilter := []ast.Node{
@@ -79,10 +86,10 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		pos := pass.Fset.PositionFor(call.Pos(), false)
-		if filecheck.IsTestFile(pos.Filename) {
+		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
-		if nolint.HasDirective(pos, noLintLinesByFile) {
+		if nolint.HasDirectiveForLinter(pos, noLintIndex, "writebytestring") {
 			return
 		}
 

From 24c637f15240fb9199ef6c931ddb1a8167b69be2 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 15 Jul 2026 10:27:46 +0000
Subject: [PATCH 03/10] docs(adr): add draft ADR-45700 for shared nolint and
 generated-file analyzer indexes

Records the architectural decision to extract per-analyzer comment scanning
into shared prerequisite analyzers (nolintindex, generatedfileindex) so that
each package's directive and generated-file indexes are built exactly once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 ...-nolint-generated-file-analyzer-indexes.md | 44 +++++++++++++++++++
 1 file changed, 44 insertions(+)
 create mode 100644 docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md

diff --git a/docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md b/docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md
new file mode 100644
index 00000000000..8d5fb8fcfcf
--- /dev/null
+++ b/docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md
@@ -0,0 +1,44 @@
+# ADR-45700: Share nolint and generated-file indexes across custom Go analyzers
+
+**Date**: 2026-07-15
+**Status**: Draft
+**Deciders**: Unknown (copilot-swe-agent, pelikhan)
+
+---
+
+### Context
+
+The custom Go linter suite in `pkg/linters/` contained duplicated logic in every analyzer: each independently scanned all AST comments to build a per-run nolint directive index, and each independently checked `IsTestFile` to skip test files. Generated Go source files were not skipped at all, making them eligible for lint diagnostics. With ~20+ analyzers each performing the same comment scan per package, the analysis framework was repeating this work N times per package.
+
+### Decision
+
+We will extract the nolint-directive scanning and generated-file detection into two dedicated prerequisite analyzers (`nolintindex` in `pkg/linters/internal/nolint` and `generatedfileindex` in `pkg/linters/internal/filecheck`). Every custom analyzer declares both as `Requires` dependencies and fetches their results via `pass.ResultOf`, so the index is built exactly once per package. We will replace per-analyzer `BuildLineIndex(pass, linterName)` calls with `nolint.Index(pass)` and replace `IsTestFile` guards with `ShouldSkipFilename(filename, generatedFiles)`, which skips both test files and files marked with the standard Go generated-file marker.
+
+### Alternatives Considered
+
+#### Alternative 1: Keep per-analyzer comment scanning, add generated-file detection to each
+
+Each analyzer continues to call `BuildLineIndex` locally but also calls a new per-analyzer `BuildGeneratedIndex`. This eliminates the generated-file gap without architectural change. Rejected because it multiplies the redundant comment and AST comment work proportionally to the number of analyzers and misses the opportunity to fix both problems with a single design change.
+
+#### Alternative 2: Package-level caching with `sync.Once` or a global map
+
+A shared utility function could cache results per `*analysis.Pass` using a global keyed map or `sync.Once`. This avoids changing the `Requires` dependency list. Rejected because it would be incompatible with how the Go analysis framework parallelises passes and could introduce data races; the `pass.ResultOf` prerequisite mechanism is the framework's canonical way to share computed data between analyzers in the same pass.
+
+### Consequences
+
+#### Positive
+- Eliminates N-fold redundant comment scanning: each package's nolint index and generated-file index is built exactly once regardless of how many analyzers run against it.
+- Makes file-skipping behavior consistent across all linters: generated files (detected via `ast.IsGenerated`) are now uniformly excluded in addition to test files.
+- Unit tests for the shared index helpers (`BuildDirectiveIndex`, `BuildGeneratedIndex`, `ShouldSkipFilename`) provide regression coverage that was previously absent.
+
+#### Negative
+- Each custom analyzer's `Requires` slice now includes two additional entries (`nolint.Analyzer`, `filecheck.Analyzer`), slightly increasing per-analyzer dependency declaration verbosity and making the dependency graph wider.
+- The `DirectiveIndex` now stores all linter names across all `nolint:` directives in a package rather than only those for the requesting linter; this trades a wider map at index-build time for elimination of repeated scans.
+
+#### Neutral
+- `HasDirective` (the old API) is preserved for backward compatibility but is now backed by `BuildDirectiveIndex`; callers on the old API continue to work without changes.
+- The `nolintindex` and `generatedfileindex` analyzers set `RunDespiteErrors: true`, so analysis proceeds even when the package has type errors.
+
+---
+
+*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*

From beca8596c1d24fb0d1d538c81258be2b6639ffd3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:05:58 +0000
Subject: [PATCH 04/10] fix linter position consistency for generated and
 nolint indexes

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
 pkg/linters/fprintlnsprintf/fprintlnsprintf.go   |  2 +-
 pkg/linters/internal/filecheck/filecheck.go      | 12 ++++++++----
 pkg/linters/internal/filecheck/filecheck_test.go |  5 +++++
 3 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/pkg/linters/fprintlnsprintf/fprintlnsprintf.go b/pkg/linters/fprintlnsprintf/fprintlnsprintf.go
index 519f8c82f97..54c7f6f19c4 100644
--- a/pkg/linters/fprintlnsprintf/fprintlnsprintf.go
+++ b/pkg/linters/fprintlnsprintf/fprintlnsprintf.go
@@ -56,7 +56,7 @@ func run(pass *analysis.Pass) (any, error) {
 		}
 
 		// Skip test files.
-		pos := pass.Fset.Position(call.Pos())
+		pos := pass.Fset.PositionFor(call.Pos(), false)
 		if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
 			return
 		}
diff --git a/pkg/linters/internal/filecheck/filecheck.go b/pkg/linters/internal/filecheck/filecheck.go
index 5dcbf566416..42a13647d26 100644
--- a/pkg/linters/internal/filecheck/filecheck.go
+++ b/pkg/linters/internal/filecheck/filecheck.go
@@ -41,11 +41,15 @@ func BuildGeneratedIndex(pass *analysis.Pass) GeneratedIndex {
 		if !ast.IsGenerated(file) {
 			continue
 		}
-		filename := pass.Fset.PositionFor(file.Pos(), false).Filename
-		if filename == "" {
-			continue
+		originalFilename := pass.Fset.PositionFor(file.Pos(), false).Filename
+		if originalFilename != "" {
+			generated[originalFilename] = struct{}{}
+		}
+
+		adjustedFilename := pass.Fset.Position(file.Pos()).Filename
+		if adjustedFilename != "" {
+			generated[adjustedFilename] = struct{}{}
 		}
-		generated[filename] = struct{}{}
 	}
 	return generated
 }
diff --git a/pkg/linters/internal/filecheck/filecheck_test.go b/pkg/linters/internal/filecheck/filecheck_test.go
index 83cae62f2cd..c8f8dd47d10 100644
--- a/pkg/linters/internal/filecheck/filecheck_test.go
+++ b/pkg/linters/internal/filecheck/filecheck_test.go
@@ -11,10 +11,12 @@ import (
 
 func TestBuildGeneratedIndex(t *testing.T) {
 	const generatedFilename = "generated.go"
+	const generatedAdjustedFilename = "/virtual/generated_alias.go"
 	const regularFilename = "regular.go"
 
 	fset := token.NewFileSet()
 	generatedFile, err := parser.ParseFile(fset, generatedFilename, `// Code generated by test. DO NOT EDIT.
+//line /virtual/generated_alias.go:1
 package p
 
 func generated() {}
@@ -34,6 +36,9 @@ func regular() {}
 	if !IsGeneratedFile(generatedFilename, idx) {
 		t.Fatalf("expected %q to be marked generated", generatedFilename)
 	}
+	if !IsGeneratedFile(generatedAdjustedFilename, idx) {
+		t.Fatalf("expected adjusted filename %q to be marked generated", generatedAdjustedFilename)
+	}
 	if IsGeneratedFile(regularFilename, idx) {
 		t.Fatalf("did not expect %q to be marked generated", regularFilename)
 	}

From b1d599f07c03bb46f2aaac81bc2af96b3a8f1924 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:11:32 +0000
Subject: [PATCH 05/10] avoid duplicate generated-file index inserts

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
 pkg/linters/internal/filecheck/filecheck.go | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/pkg/linters/internal/filecheck/filecheck.go b/pkg/linters/internal/filecheck/filecheck.go
index 42a13647d26..f99258f7b21 100644
--- a/pkg/linters/internal/filecheck/filecheck.go
+++ b/pkg/linters/internal/filecheck/filecheck.go
@@ -41,13 +41,14 @@ func BuildGeneratedIndex(pass *analysis.Pass) GeneratedIndex {
 		if !ast.IsGenerated(file) {
 			continue
 		}
-		originalFilename := pass.Fset.PositionFor(file.Pos(), false).Filename
+		pos := file.Pos()
+		originalFilename := pass.Fset.PositionFor(pos, false).Filename
 		if originalFilename != "" {
 			generated[originalFilename] = struct{}{}
 		}
 
-		adjustedFilename := pass.Fset.Position(file.Pos()).Filename
-		if adjustedFilename != "" {
+		adjustedFilename := pass.Fset.Position(pos).Filename
+		if adjustedFilename != "" && adjustedFilename != originalFilename {
 			generated[adjustedFilename] = struct{}{}
 		}
 	}

From 0c1978442c5be58a1dc750ececfb6d790eeaeadc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:17:08 +0000
Subject: [PATCH 06/10] document adjusted generated filename indexing

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
 pkg/linters/internal/filecheck/filecheck.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pkg/linters/internal/filecheck/filecheck.go b/pkg/linters/internal/filecheck/filecheck.go
index f99258f7b21..aa7ba7cba36 100644
--- a/pkg/linters/internal/filecheck/filecheck.go
+++ b/pkg/linters/internal/filecheck/filecheck.go
@@ -48,6 +48,8 @@ func BuildGeneratedIndex(pass *analysis.Pass) GeneratedIndex {
 		}
 
 		adjustedFilename := pass.Fset.Position(pos).Filename
+		// //line directives can remap positions to a different logical file path.
+		// Record both names so callers using adjusted or unadjusted positions can match.
 		if adjustedFilename != "" && adjustedFilename != originalFilename {
 			generated[adjustedFilename] = struct{}{}
 		}

From a6ca90f8cb28514cc0b9e56973301f2376253747 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:22:37 +0000
Subject: [PATCH 07/10] clarify file position handling and extend
 generated-file tests

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
 pkg/linters/internal/filecheck/filecheck.go   |  5 ++--
 .../internal/filecheck/filecheck_test.go      | 23 +++++++++++++++++++
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/pkg/linters/internal/filecheck/filecheck.go b/pkg/linters/internal/filecheck/filecheck.go
index aa7ba7cba36..df99c76ece9 100644
--- a/pkg/linters/internal/filecheck/filecheck.go
+++ b/pkg/linters/internal/filecheck/filecheck.go
@@ -42,14 +42,15 @@ func BuildGeneratedIndex(pass *analysis.Pass) GeneratedIndex {
 			continue
 		}
 		pos := file.Pos()
+		// PositionFor(..., false) preserves the original parsed filename, while
+		// Position(...) may return a //line-adjusted logical filename.
+		// Record both so callers using either position source can match.
 		originalFilename := pass.Fset.PositionFor(pos, false).Filename
 		if originalFilename != "" {
 			generated[originalFilename] = struct{}{}
 		}
 
 		adjustedFilename := pass.Fset.Position(pos).Filename
-		// //line directives can remap positions to a different logical file path.
-		// Record both names so callers using adjusted or unadjusted positions can match.
 		if adjustedFilename != "" && adjustedFilename != originalFilename {
 			generated[adjustedFilename] = struct{}{}
 		}
diff --git a/pkg/linters/internal/filecheck/filecheck_test.go b/pkg/linters/internal/filecheck/filecheck_test.go
index c8f8dd47d10..cc8c64e781e 100644
--- a/pkg/linters/internal/filecheck/filecheck_test.go
+++ b/pkg/linters/internal/filecheck/filecheck_test.go
@@ -59,3 +59,26 @@ func TestShouldSkipFilename(t *testing.T) {
 		t.Fatalf("did not expect regular file to be skipped")
 	}
 }
+
+func TestBuildGeneratedIndexDoesNotMarkNonGeneratedWithLineDirective(t *testing.T) {
+	const regularFilename = "regular.go"
+	const adjustedFilename = "/virtual/regular_alias.go"
+
+	fset := token.NewFileSet()
+	regularFile, err := parser.ParseFile(fset, regularFilename, `//line /virtual/regular_alias.go:1
+package p
+
+func regular() {}
+`, parser.ParseComments)
+	if err != nil {
+		t.Fatalf("ParseFile(regular) error = %v", err)
+	}
+
+	idx := BuildGeneratedIndex(&analysis.Pass{Fset: fset, Files: []*ast.File{regularFile}})
+	if IsGeneratedFile(regularFilename, idx) {
+		t.Fatalf("did not expect %q to be marked generated", regularFilename)
+	}
+	if IsGeneratedFile(adjustedFilename, idx) {
+		t.Fatalf("did not expect adjusted filename %q to be marked generated", adjustedFilename)
+	}
+}

From 312d9c22b4b9b76cf7cab99ea4e3de5ae082c55f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:45:24 +0000
Subject: [PATCH 08/10] apply generated-file skip and deprecate legacy nolint
 shims

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
 ...are-nolint-generated-file-analyzer-indexes.md |  6 +++---
 pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go | 10 +++++++++-
 .../testdata/src/fmterrorfnoverbs/generated.go   |  9 +++++++++
 pkg/linters/internal/nolint/nolint.go            | 16 +++++++++++++---
 .../strconvparseignorederror.go                  | 10 +++++++++-
 .../src/strconvparseignorederror/generated.go    | 10 ++++++++++
 6 files changed, 53 insertions(+), 8 deletions(-)
 create mode 100644 pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/generated.go
 create mode 100644 pkg/linters/strconvparseignorederror/testdata/src/strconvparseignorederror/generated.go

diff --git a/docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md b/docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md
index 8d5fb8fcfcf..00fcc0f21be 100644
--- a/docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md
+++ b/docs/adr/45700-share-nolint-generated-file-analyzer-indexes.md
@@ -1,7 +1,7 @@
 # ADR-45700: Share nolint and generated-file indexes across custom Go analyzers
 
 **Date**: 2026-07-15
-**Status**: Draft
+**Status**: Accepted
 **Deciders**: Unknown (copilot-swe-agent, pelikhan)
 
 ---
@@ -36,9 +36,9 @@ A shared utility function could cache results per `*analysis.Pass` using a globa
 - The `DirectiveIndex` now stores all linter names across all `nolint:` directives in a package rather than only those for the requesting linter; this trades a wider map at index-build time for elimination of repeated scans.
 
 #### Neutral
-- `HasDirective` (the old API) is preserved for backward compatibility but is now backed by `BuildDirectiveIndex`; callers on the old API continue to work without changes.
+- `HasDirective` and `BuildLineIndex` are preserved for backward compatibility but marked deprecated in favor of shared-index APIs (`HasDirectiveForLinter` and `Index`).
 - The `nolintindex` and `generatedfileindex` analyzers set `RunDespiteErrors: true`, so analysis proceeds even when the package has type errors.
 
 ---
 
-*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*
+*ADR created by [adr-writer agent] and finalized in this PR.*
diff --git a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
index dabce447d8b..a7900036fe4 100644
--- a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
+++ b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
@@ -11,6 +11,7 @@ import (
 	"golang.org/x/tools/go/analysis/passes/inspect"
 
 	"github.com/github/gh-aw/pkg/linters/internal/astutil"
+	"github.com/github/gh-aw/pkg/linters/internal/filecheck"
 	"github.com/github/gh-aw/pkg/linters/internal/nolint"
 )
 
@@ -19,7 +20,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "fmterrorfnoverbs",
 	Doc:      "reports fmt.Errorf calls whose format string contains no verbs, preferring errors.New",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/fmterrorfnoverbs",
-	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -32,6 +33,10 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.CallExpr)(nil),
@@ -64,6 +69,9 @@ func run(pass *analysis.Pass) (any, error) {
 
 		if !hasRealFormatVerb(val) {
 			position := pass.Fset.PositionFor(call.Pos(), false)
+			if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
+				return
+			}
 			if nolint.HasDirectiveForLinter(position, noLintIndex, "fmterrorfnoverbs") {
 				return
 			}
diff --git a/pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/generated.go b/pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/generated.go
new file mode 100644
index 00000000000..2ee70917ba6
--- /dev/null
+++ b/pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/generated.go
@@ -0,0 +1,9 @@
+// Code generated by tests. DO NOT EDIT.
+
+package fmterrorfnoverbs
+
+import fmtalias "fmt"
+
+func generatedBad() error {
+	return fmtalias.Errorf("generated static message")
+}
diff --git a/pkg/linters/internal/nolint/nolint.go b/pkg/linters/internal/nolint/nolint.go
index 286933d5836..5b373348bb8 100644
--- a/pkg/linters/internal/nolint/nolint.go
+++ b/pkg/linters/internal/nolint/nolint.go
@@ -78,9 +78,12 @@ func BuildDirectiveIndex(pass *analysis.Pass) DirectiveIndex {
 	return noLintLinesByFile
 }
 
-// HasDirective reports whether the given source position is covered by a
-// suppression directive for linterName (or "nolint:all").  Both same-line and
+// HasDirective reports whether the given source position is covered by any
+// suppression directive line in the legacy line-index shape. Both same-line and
 // previous-line directives are recognised, matching golangci-lint behaviour.
+//
+// Deprecated: use HasDirectiveForLinter instead, which operates on the shared
+// DirectiveIndex result and preserves per-linter filtering.
 func HasDirective(position token.Position, idx map[string]map[int]struct{}) bool {
 	if position.Filename == "" {
 		return false
@@ -110,9 +113,16 @@ func HasDirectiveForLinter(position token.Position, idx DirectiveIndex, linterNa
 // BuildLineIndex scans all comments in the analysis pass and returns a map
 // from filename → set of line numbers that carry a nolint directive for
 // linterName (e.g. "errstringmatch") or "all".
+//
+// Deprecated: use Index together with HasDirectiveForLinter so linters reuse
+// the shared analyzer result instead of rebuilding a per-linter map.
 func BuildLineIndex(pass *analysis.Pass, linterName string) map[string]map[int]struct{} {
 	noLintLinesByFile := make(map[string]map[int]struct{}, len(pass.Files))
-	for filename, lines := range BuildDirectiveIndex(pass) {
+	directiveIndex, err := Index(pass)
+	if err != nil {
+		directiveIndex = BuildDirectiveIndex(pass)
+	}
+	for filename, lines := range directiveIndex {
 		for line, names := range lines {
 			if !hasDirectiveName(names, linterName) {
 				continue
diff --git a/pkg/linters/strconvparseignorederror/strconvparseignorederror.go b/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
index ed83ff106b0..42500bfc5fc 100644
--- a/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
+++ b/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
@@ -11,6 +11,7 @@ import (
 	"golang.org/x/tools/go/analysis/passes/inspect"
 
 	"github.com/github/gh-aw/pkg/linters/internal/astutil"
+	"github.com/github/gh-aw/pkg/linters/internal/filecheck"
 	"github.com/github/gh-aw/pkg/linters/internal/nolint"
 )
 
@@ -19,7 +20,7 @@ var Analyzer = &analysis.Analyzer{
 	Name:     "strconvparseignorederror",
 	Doc:      "reports strconv parsing calls where the error return is discarded with _",
 	URL:      "https://github.com/github/gh-aw/tree/main/pkg/linters/strconvparseignorederror",
-	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer},
+	Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer},
 	Run:      run,
 }
 
@@ -41,6 +42,10 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
+	generatedFiles, err := filecheck.Index(pass)
+	if err != nil {
+		return nil, err
+	}
 
 	nodeFilter := []ast.Node{
 		(*ast.AssignStmt)(nil),
@@ -78,6 +83,9 @@ func run(pass *analysis.Pass) (any, error) {
 			if pkgName, ok := obj.(*types.PkgName); ok {
 				if pkgName.Imported().Path() == "strconv" {
 					position := pass.Fset.PositionFor(call.Pos(), false)
+					if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
+						return
+					}
 					if nolint.HasDirectiveForLinter(position, noLintIndex, "strconvparseignorederror") {
 						return
 					}
diff --git a/pkg/linters/strconvparseignorederror/testdata/src/strconvparseignorederror/generated.go b/pkg/linters/strconvparseignorederror/testdata/src/strconvparseignorederror/generated.go
new file mode 100644
index 00000000000..5f86144c988
--- /dev/null
+++ b/pkg/linters/strconvparseignorederror/testdata/src/strconvparseignorederror/generated.go
@@ -0,0 +1,10 @@
+// Code generated by tests. DO NOT EDIT.
+
+package strconvparseignorederror
+
+import "strconv"
+
+func generatedBad() {
+	n, _ := strconv.Atoi("42")
+	_ = n
+}

From 4f7c372c86e23bd0130e417322d9830cef90e817 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:50:18 +0000
Subject: [PATCH 09/10] rename nolint index locals for consistency

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
 pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go              | 4 ++--
 .../strconvparseignorederror/strconvparseignorederror.go      | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
index a7900036fe4..32b556a72f2 100644
--- a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
+++ b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
@@ -29,7 +29,7 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintIndex, err := nolint.Index(pass)
+	nolintIndex, err := nolint.Index(pass)
 	if err != nil {
 		return nil, err
 	}
@@ -72,7 +72,7 @@ func run(pass *analysis.Pass) (any, error) {
 			if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
 				return
 			}
-			if nolint.HasDirectiveForLinter(position, noLintIndex, "fmterrorfnoverbs") {
+			if nolint.HasDirectiveForLinter(position, nolintIndex, "fmterrorfnoverbs") {
 				return
 			}
 			pass.ReportRangef(call, "fmt.Errorf called with no format verbs; use errors.New(%s) instead", lit.Value)
diff --git a/pkg/linters/strconvparseignorederror/strconvparseignorederror.go b/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
index 42500bfc5fc..782091cb404 100644
--- a/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
+++ b/pkg/linters/strconvparseignorederror/strconvparseignorederror.go
@@ -38,7 +38,7 @@ func run(pass *analysis.Pass) (any, error) {
 	if err != nil {
 		return nil, err
 	}
-	noLintIndex, err := nolint.Index(pass)
+	nolintIndex, err := nolint.Index(pass)
 	if err != nil {
 		return nil, err
 	}
@@ -86,7 +86,7 @@ func run(pass *analysis.Pass) (any, error) {
 					if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
 						return
 					}
-					if nolint.HasDirectiveForLinter(position, noLintIndex, "strconvparseignorederror") {
+					if nolint.HasDirectiveForLinter(position, nolintIndex, "strconvparseignorederror") {
 						return
 					}
 					pass.ReportRangef(call, "error return from strconv.%s is discarded; parse failures produce zero values silently", sel.Sel.Name)

From a5b92baa747ebd8f1a01ba90fc8356949c6c52b9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Jul 2026 12:56:23 +0000
Subject: [PATCH 10/10] document legacy nolint fallback behavior

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
 pkg/linters/internal/nolint/nolint.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pkg/linters/internal/nolint/nolint.go b/pkg/linters/internal/nolint/nolint.go
index 5b373348bb8..f859070b91a 100644
--- a/pkg/linters/internal/nolint/nolint.go
+++ b/pkg/linters/internal/nolint/nolint.go
@@ -120,6 +120,8 @@ func BuildLineIndex(pass *analysis.Pass, linterName string) map[string]map[int]s
 	noLintLinesByFile := make(map[string]map[int]struct{}, len(pass.Files))
 	directiveIndex, err := Index(pass)
 	if err != nil {
+		// Keep legacy callers working even when nolint.Analyzer is not listed in
+		// Requires by rebuilding directly from comments.
 		directiveIndex = BuildDirectiveIndex(pass)
 	}
 	for filename, lines := range directiveIndex {
