From ab03d07ecf40970db70ca679d3bad7cd6a923280 Mon Sep 17 00:00:00 2001 From: Shahnoor Chowdhury Eshan <146880991+eshanclio@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:06:34 -0600 Subject: [PATCH] Fix gosec and nilaway findings, enforce both in CI Triages all 61 gosec findings and all 3 nilaway findings, then removes continue-on-error from the existing gosec/nilaway CI jobs so new findings block merges going forward. Also fixes two other pre-existing CI failures (govulncheck, lint) uncovered while getting this branch fully green end to end, and the lint job's own full pre-existing findings backlog once fixing its version-pin bug let it actually run. gosec: - G104 (unhandled Close errors): replaced with plain `_ =` assignments matching the codebase's existing convention, needing no suppression at all. - G301/G306/G302 (permissions): tightened directories to 0o750 and app-owned files to 0o600; .gitignore intentionally kept at the conventional 0o644 (world-readable, shared in the repo). - G304 (dynamic path), G202 (SQL concat), G115 (int narrowing), G401/G505 (SHA-1), G103 (unsafe.Pointer): verified genuine false positives by inspection and suppressed with an inline `// #nosec Gxxx -- reason` comment (gosec's own directive, since golangci-lint's `//nolint` is not understood when gosec runs standalone as it does in CI). This includes six findings in the Linux-only inotify backend that a local macOS run can't see, since it's gated by a linux build tag. - watcher/backend_inotify.go also hoists an int->uint32 conversion out of a for-loop header: gosec's own nosec-comment scanner fails to suppress a finding on the very next line when the loop header carries its own nosec comment, so the conversion moves to its own statement before the loop. nilaway: restructured indexer's batch-embedding path so the `origins` accumulator is a non-nil `make([]T, 0, n)` instead of a nil `var`, and merged two loops into one straight-line block, giving nilaway's flow analysis a provably non-nil path. Behavior-preserving; covered by existing indexer_test.go fakes. govulncheck: bumps golang.org/x/text to v0.39.0, fixing GO-2026-5970 (infinite loop on invalid input), reached transitively through the tokenizer's Unicode normalization. lint: golangci-lint-action@v6's `version: latest` resolves within the v1.x line, which refuses to run against this module's go 1.26.5 directive. Moves to golangci-lint-action@v9 pinned to golangci-lint v2.12.2, which supports it. That version-pin fix let the job actually lint for the first time (it was crashing before reaching any file), surfacing a 223-issue backlog (218 errcheck + 5 staticcheck) across 21 files hidden behind both the crash and golangci-lint's own default max-issues-per-linter/max-same-issues caps. Fixed all of them: unchecked errors become `_ =` (or the package's existing closeQuietly helper for io.Closer values in store/sqlite.go and store/graph.go), and the 5 staticcheck simplifications (3x WriteString(Sprintf(...)) -> Fprintf, one De Morgan's law rewrite, one redundant embedded-field selector) are applied as suggested. --- .github/workflows/ci.yml | 35 +++++--- cmd/codamigo/doctor_cmd.go | 4 +- cmd/codamigo/graph_cmd.go | 2 +- cmd/codamigo/init_cmd.go | 18 ++-- cmd/codamigo/main.go | 12 +-- cmd/codamigo/map_cmd.go | 2 +- cmd/codamigo/reset_cmd.go | 8 +- cmd/codamigo/search_cmd.go | 2 +- config/config.go | 8 +- config/config_test.go | 16 ++-- go.mod | 2 +- go.sum | 4 +- indexer/graph_integration_test.go | 4 +- indexer/indexer.go | 41 ++++----- indexer/indexer_test.go | 44 ++++----- localembed/cache_test.go | 2 +- localembed/download.go | 5 +- localembed/embedder.go | 4 +- localembed/inference_test.go | 2 +- mcp/graph_test.go | 6 +- mcp/server_test.go | 20 ++--- query/graph_test.go | 2 +- query/query.go | 8 +- query/query_test.go | 48 +++++----- store/graph.go | 4 +- store/graph_test.go | 2 +- store/sqlite.go | 119 ++++++++++++++----------- store/store_test.go | 86 +++++++++--------- walker/walker.go | 2 +- walker/walker_test.go | 142 +++++++++++++++--------------- watcher/backend_inotify.go | 16 ++-- watcher/backend_kqueue.go | 28 +++--- watcher/fsnotify.go | 6 +- watcher/watcher.go | 4 +- watcher/watcher_test.go | 24 ++--- 35 files changed, 384 insertions(+), 348 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06a1af3..abe2504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,17 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends libsqlite3-dev + # golangci-lint-action@v6's "latest" resolves within the v1.x line + # (v1.64.8 at time of writing), which refuses to run against this + # module's go 1.26.5 directive ("the Go language version used to + # build golangci-lint is lower than the targeted Go version"). v9 + # supports golangci-lint v2, and pinning a v2 release keeps the + # Go-version check satisfied without depending on how "latest" + # happens to resolve. - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: - version: latest + version: v2.12.2 args: --build-tags=sqlite_fts5 test: @@ -225,14 +232,15 @@ jobs: # set up — gives gosec's type-checking source loader what it needs to # actually analyze cgo-dependent packages instead of skipping them. # - # continue-on-error: a first run against this codebase turned up 61 - # pre-existing findings (mostly G104 unhandled-error-on-Close, plus a - # handful of G301/G304/G306/G115/G202/G401/G505). None of that is this - # job's fault to gate on — report-only until it's triaged, then remove - # continue-on-error so new findings start blocking merges. + # The 61 pre-existing findings from the first run against this codebase + # (mostly G104 unhandled-error-on-Close, plus a handful of + # G301/G304/G306/G115/G202/G401/G505) have all been triaged: fixed where + # real, suppressed with an inline `// #nosec Gxxx -- reason` comment + # (gosec's own directive — it does not understand golangci-lint's + # `//nolint` comments when run standalone like this) where the flagged + # pattern is safe by inspection. New findings now block merges. gosec: runs-on: ubuntu-latest - continue-on-error: true steps: - name: Checkout uses: actions/checkout@v4 @@ -260,12 +268,15 @@ jobs: # see, which is the flag NilAway's own docs call out as the standard way to # keep a first run's signal-to-noise ratio sane on an existing codebase. # - # continue-on-error: a first run against this codebase found 3 potential - # nil panics in indexer/indexer.go, pre-existing and unrelated to this - # workflow. Report-only until triaged, then remove continue-on-error. + # The 3 potential nil panics found by the first run against this codebase + # (all in indexer/indexer.go, one structural root cause) have been fixed: + # a nilable `var x []T` accumulator became a non-nil `make([]T, 0, n)`, + # and the two consuming loops were merged into the same block that + # assigns and validates the embedder's result slices, giving NilAway's + # flow analysis a straight-line path to see them as non-nil. New findings + # now block merges. nilaway: runs-on: ubuntu-latest - continue-on-error: true steps: - name: Checkout uses: actions/checkout@v4 diff --git a/cmd/codamigo/doctor_cmd.go b/cmd/codamigo/doctor_cmd.go index 8c3cb6e..c893917 100644 --- a/cmd/codamigo/doctor_cmd.go +++ b/cmd/codamigo/doctor_cmd.go @@ -119,7 +119,7 @@ func doctorCmd() *cli.Command { if err != nil { fmt.Printf("[FAIL] Store open error: %v\n", err) } else { - defer s.Close() + defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way stats, err := s.Stats(ctx) if err != nil { fmt.Printf("[FAIL] Stats error: %v\n", err) @@ -157,7 +157,7 @@ func doctorCmd() *cli.Command { if err != nil { fmt.Printf("[FAIL] Walker error: %v\n", err) } else { - defer w.Close() + defer func() { _ = w.Close() }() // best-effort cleanup; the process is exiting either way count := 0 errCount := 0 for _, err := range w.Walk(ctx) { diff --git a/cmd/codamigo/graph_cmd.go b/cmd/codamigo/graph_cmd.go index 3dda050..2aaf88f 100644 --- a/cmd/codamigo/graph_cmd.go +++ b/cmd/codamigo/graph_cmd.go @@ -106,7 +106,7 @@ func runGraphQuery( if err != nil { return err } - defer s.Close() + defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way refs, err := run(query.New(emb, s), symbol) if err != nil { diff --git a/cmd/codamigo/init_cmd.go b/cmd/codamigo/init_cmd.go index 088aadd..9fd3b23 100644 --- a/cmd/codamigo/init_cmd.go +++ b/cmd/codamigo/init_cmd.go @@ -77,7 +77,7 @@ func initCmd() *cli.Command { } // Write global config. - if err := os.MkdirAll(filepath.Dir(globalPath), 0o755); err != nil { + if err := os.MkdirAll(filepath.Dir(globalPath), 0o750); err != nil { return fmt.Errorf("creating config directory: %w", err) } // 0o600 below matters: this file may hold an API key or a @@ -111,11 +111,11 @@ func initCmd() *cli.Command { // Always create project config if missing — runs regardless of global config state. projectPath := config.ProjectConfigPath() if _, err := os.Stat(projectPath); errors.Is(err, fs.ErrNotExist) { - if err := os.MkdirAll(filepath.Dir(projectPath), 0o755); err != nil { + if err := os.MkdirAll(filepath.Dir(projectPath), 0o750); err != nil { return fmt.Errorf("creating project config directory: %w", err) } projectContent := "# codamigo project settings\n# include_patterns: []\n# exclude_patterns: []\n" - if err := os.WriteFile(projectPath, []byte(projectContent), 0o644); err != nil { + if err := os.WriteFile(projectPath, []byte(projectContent), 0o600); err != nil { return fmt.Errorf("writing project config: %w", err) } fmt.Printf("Created project config: %s\n", projectPath) @@ -134,11 +134,11 @@ func initCmd() *cli.Command { dataDir, err := config.ProjectDataDir(wd) if err != nil { fmt.Fprintf(os.Stderr, "Warning: could not resolve data directory: %v\n", err) - } else if err := os.MkdirAll(dataDir, 0o755); err != nil { + } else if err := os.MkdirAll(dataDir, 0o750); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not create data directory: %v\n", err) } else { pathFile := filepath.Join(dataDir, "project_path") - if err := os.WriteFile(pathFile, []byte(wd), 0o644); err != nil { + if err := os.WriteFile(pathFile, []byte(wd), 0o600); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not write project_path: %v\n", err) } } @@ -201,9 +201,9 @@ func initCmd() *cli.Command { // buffered input from piped stdin is not lost between calls. func readPrompt(scanner *bufio.Scanner, w io.Writer, prompt, defaultVal string) string { if defaultVal != "" { - fmt.Fprintf(w, "%s [%s]: ", prompt, defaultVal) + _, _ = fmt.Fprintf(w, "%s [%s]: ", prompt, defaultVal) } else { - fmt.Fprintf(w, "%s: ", prompt) + _, _ = fmt.Fprintf(w, "%s: ", prompt) } if scanner.Scan() { line := strings.TrimSpace(scanner.Text()) @@ -227,6 +227,7 @@ func appendToGitignore(projectRoot string) error { const entry = ".codamigo/" gitignorePath := filepath.Join(projectRoot, ".gitignore") + // #nosec G304 -- gitignorePath is derived from the CLI's own project root, not external input content, err := os.ReadFile(gitignorePath) if err != nil && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("reading .gitignore: %w", err) @@ -238,6 +239,9 @@ func appendToGitignore(projectRoot string) error { } } + // #nosec G304,G302 -- 0o644 matches .gitignore's conventional world-readable + // permissions; gitignorePath is derived from the CLI's own project root, + // not external input. f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("opening .gitignore: %w", err) diff --git a/cmd/codamigo/main.go b/cmd/codamigo/main.go index c2b450d..74d7b93 100644 --- a/cmd/codamigo/main.go +++ b/cmd/codamigo/main.go @@ -130,8 +130,8 @@ func indexCmd() *cli.Command { if err != nil { return err } - defer s.Close() - defer w.Close() //nolint:errcheck + defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way + defer func() { _ = w.Close() }() // best-effort cleanup; the process is exiting either way // Wrap ctx so the TUI can cancel the indexer directly via ctrl+c. // In TTY mode the terminal is in raw mode (ISIG cleared), so ctrl+c @@ -233,8 +233,8 @@ func serveCmd() *cli.Command { if err != nil { return err } - defer s.Close() - defer w.Close() + defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way + defer func() { _ = w.Close() }() // best-effort cleanup; the process is exiting either way queryEmb, err := queryEmbedderFor(cfg, indexEmb) if err != nil { return fmt.Errorf("creating embedder: %w", err) @@ -251,7 +251,7 @@ func serveCmd() *cli.Command { if err != nil { return fmt.Errorf("creating watcher: %w", err) } - defer wch.Close() + defer func() { _ = wch.Close() }() // best-effort cleanup; the process is exiting either way srv := mcp.NewServer(q, idx, wch, mcp.WithNonCodeLanguages(cfg.NonCodeLanguages), mcp.WithGraph(cfg.GraphEnabled()), @@ -422,7 +422,7 @@ func buildComponents(cfg *config.Config, storePath string, dim int) (*chunker.Ch filter := buildExtensionFilter(allLangs) w, err := walker.New(cfg.ProjectRoot, cfg, walker.WithFileFilter(filter)) if err != nil { - s.Close() + _ = s.Close() // best-effort cleanup; the walker error below is the one worth reporting return nil, nil, nil, fmt.Errorf("creating walker: %w", err) } return c, s, w, nil diff --git a/cmd/codamigo/map_cmd.go b/cmd/codamigo/map_cmd.go index d2ad844..d1a47ef 100644 --- a/cmd/codamigo/map_cmd.go +++ b/cmd/codamigo/map_cmd.go @@ -52,7 +52,7 @@ func mapCmd() *cli.Command { if err != nil { return err } - defer s.Close() + defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way q := query.New(emb, s) diff --git a/cmd/codamigo/reset_cmd.go b/cmd/codamigo/reset_cmd.go index fe3e003..6e8dd31 100644 --- a/cmd/codamigo/reset_cmd.go +++ b/cmd/codamigo/reset_cmd.go @@ -44,19 +44,19 @@ func resetCmd() *cli.Command { // is testable without touching os.Stdin/os.Stdout. func runReset(storePath string, force bool, in io.Reader, out io.Writer) error { if _, err := os.Stat(storePath); errors.Is(err, fs.ErrNotExist) { - fmt.Fprintln(out, "Nothing to reset.") + _, _ = fmt.Fprintln(out, "Nothing to reset.") return nil } if !force { - fmt.Fprintf(out, "The following file will be deleted:\n %s\nDelete store file? [y/N]: ", storePath) + _, _ = fmt.Fprintf(out, "The following file will be deleted:\n %s\nDelete store file? [y/N]: ", storePath) scanner := bufio.NewScanner(in) answer := "" if scanner.Scan() { answer = strings.TrimSpace(scanner.Text()) } if !strings.EqualFold(answer, "y") { - fmt.Fprintln(out, "Aborted.") + _, _ = fmt.Fprintln(out, "Aborted.") return nil } } @@ -66,6 +66,6 @@ func runReset(storePath string, force bool, in io.Reader, out io.Writer) error { return fmt.Errorf("deleting store%s: %w", suffix, err) } } - fmt.Fprintf(out, "Store deleted: %s\n", storePath) + _, _ = fmt.Fprintf(out, "Store deleted: %s\n", storePath) return nil } diff --git a/cmd/codamigo/search_cmd.go b/cmd/codamigo/search_cmd.go index 2774870..9e0c71b 100644 --- a/cmd/codamigo/search_cmd.go +++ b/cmd/codamigo/search_cmd.go @@ -109,7 +109,7 @@ func searchCmd() *cli.Command { if err != nil { return err } - defer s.Close() + defer func() { _ = s.Close() }() // best-effort cleanup; the process is exiting either way maxTokens := cmd.Int("max-tokens") if maxTokens < 0 { diff --git a/config/config.go b/config/config.go index f2e842d..47d0f59 100644 --- a/config/config.go +++ b/config/config.go @@ -11,7 +11,7 @@ package config import ( - "crypto/sha1" + "crypto/sha1" // #nosec G505 -- non-cryptographic use, see ProjectHash "encoding/hex" "errors" "fmt" @@ -307,6 +307,9 @@ func ProjectConfigPath() string { // that "/home/user/project/" and "/home/user/project" produce the same hash. func ProjectHash(projectRoot string) string { trimmed := strings.Trim(projectRoot, "/") + // #nosec G401 -- non-cryptographic use: stable directory-name hash, not a + // security boundary; switching algorithms would relocate existing users' + // data dirs. sum := sha1.Sum([]byte(trimmed)) return hex.EncodeToString(sum[:]) } @@ -365,11 +368,12 @@ func HomeProjectConfigPath(projectRoot string) (string, error) { // Returns an error if the file does not exist, contains unknown keys, or has // malformed duration strings. func Load(path string) (*Config, error) { + // #nosec G304 -- path is the CLI's own config path (flag/default), not external input f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("opening config %q: %w", path, err) } - defer f.Close() + defer func() { _ = f.Close() }() // best-effort cleanup; the file is only being read dec := yaml.NewDecoder(f) dec.KnownFields(true) diff --git a/config/config_test.go b/config/config_test.go index fc81346..e1d66cb 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -288,7 +288,9 @@ func writeTempConfig(t *testing.T, content string) string { if _, err := f.WriteString(content); err != nil { t.Fatalf("writing temp config: %v", err) } - f.Close() + if err := f.Close(); err != nil { + t.Fatalf("closing temp config: %v", err) + } return f.Name() } @@ -326,9 +328,13 @@ func ExampleLoad() { if err != nil { panic(err) } - defer os.Remove(f.Name()) - f.WriteString("embedding_model: my-model\npoll_interval: 10s\n") - f.Close() + defer func() { _ = os.Remove(f.Name()) }() + if _, err := f.WriteString("embedding_model: my-model\npoll_interval: 10s\n"); err != nil { + panic(err) + } + if err := f.Close(); err != nil { + panic(err) + } cfg, err := config.Load(f.Name()) if err != nil { @@ -591,7 +597,7 @@ func TestProjectHash(t *testing.T) { t.Errorf("hash length = %d, want 40", len(h)) } for _, c := range h { - if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') { + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { t.Errorf("hash contains non-hex char %q in %q", c, h) } } diff --git a/go.mod b/go.mod index 6fd7bd6..713e87c 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.48.0 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go.sum b/go.sum index fb20be7..c25f359 100644 --- a/go.sum +++ b/go.sum @@ -168,8 +168,8 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= diff --git a/indexer/graph_integration_test.go b/indexer/graph_integration_test.go index c9244d7..1fce4b5 100644 --- a/indexer/graph_integration_test.go +++ b/indexer/graph_integration_test.go @@ -24,7 +24,7 @@ func graphFixture(t *testing.T, src string, opts ...indexer.Option) store.Store if err != nil { t.Fatal(err) } - t.Cleanup(func() { s.Close() }) + t.Cleanup(func() { _ = s.Close() }) root := t.TempDir() if err = os.WriteFile(filepath.Join(root, "main.go"), []byte(src), 0o644); err != nil { @@ -159,7 +159,7 @@ func TestIndex_ReindexReplacesGraph(t *testing.T) { if err != nil { t.Fatal(err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() path := filepath.Join(root, "main.go") diff --git a/indexer/indexer.go b/indexer/indexer.go index 26d361d..2178e83 100644 --- a/indexer/indexer.go +++ b/indexer/indexer.go @@ -476,7 +476,7 @@ func (idx *Indexer) processStageBatch(ctx context.Context, paths []string) error // Stage 3: Embed // Collect all content hashes across all files. var allContentHashes []string - var origins []embedOrigin + origins := make([]embedOrigin, 0, len(chunked)) contentHashMap := make([][]string, len(chunked)) for i, cf := range chunked { @@ -512,12 +512,11 @@ func (idx *Indexer) processStageBatch(ctx context.Context, paths []string) error // owning at least one failed chunk is marked failed and skipped from // writing; its existing store record (if any) is left untouched so the // next indexing run will retry the file. - var newEmbeddings [][]float32 - var embedErrs []error failedFileIdx := make(map[int]struct{}) failedFileErrs := make(map[int][]error) + embeddingsByFile := make(map[int]map[int][]float32) // fileIdx -> chunkIdx -> embedding if len(uncachedTexts) > 0 { - newEmbeddings, embedErrs = idx.embedder.EmbedBatchPartial(ctx, uncachedTexts) + newEmbeddings, embedErrs := idx.embedder.EmbedBatchPartial(ctx, uncachedTexts) if ctxErr := ctx.Err(); ctxErr != nil { return ctxErr } @@ -525,30 +524,24 @@ func (idx *Indexer) processStageBatch(ctx context.Context, paths []string) error return fmt.Errorf("embedder returned %d vectors / %d errs for %d texts", len(newEmbeddings), len(embedErrs), len(uncachedTexts)) } + // Distribute results (failures and SUCCESSFUL embeddings) back to + // per-file records in a single pass. for k, e := range embedErrs { - if e == nil { + origin := origins[k] + if e != nil { + failedFileIdx[origin.fileIdx] = struct{}{} + failedFileErrs[origin.fileIdx] = append(failedFileErrs[origin.fileIdx], e) + slog.WarnContext(ctx, "embedding chunk failed", + slog.String("path", chunked[origin.fileIdx].info.path), + slog.Int("chunk", origin.chunkIdx), + slog.Any("error", e)) continue } - fi := origins[k].fileIdx - failedFileIdx[fi] = struct{}{} - failedFileErrs[fi] = append(failedFileErrs[fi], e) - slog.WarnContext(ctx, "embedding chunk failed", - slog.String("path", chunked[fi].info.path), - slog.Int("chunk", origins[k].chunkIdx), - slog.Any("error", e)) - } - } - - // Distribute SUCCESSFUL embeddings back to per-file records. - embeddingsByFile := make(map[int]map[int][]float32) // fileIdx -> chunkIdx -> embedding - for k, origin := range origins { - if embedErrs[k] != nil { - continue - } - if embeddingsByFile[origin.fileIdx] == nil { - embeddingsByFile[origin.fileIdx] = make(map[int][]float32) + if embeddingsByFile[origin.fileIdx] == nil { + embeddingsByFile[origin.fileIdx] = make(map[int][]float32) + } + embeddingsByFile[origin.fileIdx][origin.chunkIdx] = newEmbeddings[k] } - embeddingsByFile[origin.fileIdx][origin.chunkIdx] = newEmbeddings[k] } // Stage 4: Write diff --git a/indexer/indexer_test.go b/indexer/indexer_test.go index 7fb2201..417b4b9 100644 --- a/indexer/indexer_test.go +++ b/indexer/indexer_test.go @@ -63,7 +63,7 @@ func TestNew(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() cfg := &config.Config{ProjectRoot: root} @@ -88,7 +88,7 @@ func TestNew_NilDependencies(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() w, err := walker.New(root, &config.Config{ProjectRoot: root}) @@ -127,7 +127,7 @@ func TestWithOnIndexed_FiresAfterWrite(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() if err = os.WriteFile(filepath.Join(root, "a.txt"), []byte("hello"), 0o644); err != nil { @@ -157,7 +157,7 @@ func TestIndexFiles_DeletesMissing(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() // Pre-insert a record for a file that doesn't exist on disk. @@ -370,7 +370,7 @@ func TestIndexFiles_EmbeddingFailurePreservesOldChunks(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -446,7 +446,7 @@ func TestIndexFiles_SkipsExcluded(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -480,7 +480,7 @@ func TestIndexFile_SkipsOutsideRoot(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -522,7 +522,7 @@ func TestIndexFile_DotDotHiddenNotRejected(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -562,7 +562,7 @@ func TestIndex_ConcurrentProcessing(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -605,7 +605,7 @@ func TestIndexer_Progress(t *testing.T) { if err != nil { t.Fatal(err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() goFile := filepath.Join(root, "main.go") if err = os.WriteFile(goFile, []byte("package main"), 0o644); err != nil { @@ -641,7 +641,7 @@ func TestIndexer_Progress(t *testing.T) { if err != nil { t.Fatal(err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() content := []byte("package main") goFile := filepath.Join(root, "main.go") @@ -677,7 +677,7 @@ func TestIndexer_Progress(t *testing.T) { if err != nil { t.Fatal(err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() txtFile := filepath.Join(root, "doc.txt") if err = os.WriteFile(txtFile, []byte("hello world"), 0o644); err != nil { @@ -713,7 +713,7 @@ func TestIndexer_Progress(t *testing.T) { if err != nil { t.Fatal(err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() bigFile := filepath.Join(root, "big.go") if err = os.WriteFile(bigFile, []byte(strings.Repeat("x", 100)), 0o644); err != nil { @@ -744,7 +744,7 @@ func TestIndexer_Progress(t *testing.T) { if err != nil { t.Fatal(err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() if err = os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644); err != nil { t.Fatal(err) @@ -775,7 +775,7 @@ func TestIndexer_Progress_Concurrent(t *testing.T) { if err != nil { t.Fatal(err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() for i := range fileCount { content := fmt.Sprintf("package main\n\nfunc f%d() {}\n", i) @@ -813,7 +813,7 @@ func TestIndexBatch_CrossFileEmbeddingReuse(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -870,7 +870,7 @@ func TestIndexBatch_StageBatchBoundary(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -928,7 +928,7 @@ func TestIndexBatch_AllChunksFail_SkipsEntireBatch(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() @@ -990,7 +990,7 @@ func TestIndex_PartialEmbedFailure_SkipsAffectedFilesOnly(t *testing.T) { if err != nil { t.Fatalf("store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() goodPath := filepath.Join(root, "good.go") @@ -1058,7 +1058,7 @@ func TestIndex_NilProgress_LogsButDoesNotPanic(t *testing.T) { if err != nil { t.Fatalf("store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() okPath := filepath.Join(root, "ok.go") @@ -1110,7 +1110,7 @@ func TestIndex_ContinuesAcrossStageBatches_AfterPartialFailure(t *testing.T) { if err != nil { t.Fatalf("store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() root := t.TempDir() const concurrency = 1 @@ -1178,7 +1178,7 @@ func TestStaleFiles(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() root := t.TempDir() diff --git a/localembed/cache_test.go b/localembed/cache_test.go index 7e9de71..bb721ca 100644 --- a/localembed/cache_test.go +++ b/localembed/cache_test.go @@ -273,7 +273,7 @@ func writeSized(t *testing.T, path string, size int64) { if err != nil { t.Fatalf("Create %s: %v", path, err) } - defer f.Close() + defer func() { _ = f.Close() }() if size > 0 { if err := f.Truncate(size); err != nil { t.Fatalf("Truncate %s: %v", path, err) diff --git a/localembed/download.go b/localembed/download.go index 51da499..ff6fcf4 100644 --- a/localembed/download.go +++ b/localembed/download.go @@ -67,7 +67,7 @@ func Download(ctx context.Context, opts DownloadOptions) (*DownloadResult, error if len(m.Files) == 0 { return nil, fmt.Errorf("%w: %s has an empty manifest", ErrUnknownModel, m.DisplayName()) } - if err := os.MkdirAll(opts.ModelDir, 0o755); err != nil { + if err := os.MkdirAll(opts.ModelDir, 0o750); err != nil { return nil, fmt.Errorf("creating model directory: %w", err) } @@ -215,11 +215,12 @@ func removeDownloaded(path string) { } func sha256File(path string) (string, error) { + // #nosec G304 -- path is a file this process just downloaded into its own model cache dir, not external input f, err := os.Open(path) if err != nil { return "", err } - defer f.Close() + defer func() { _ = f.Close() }() // the file is only being read h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err diff --git a/localembed/embedder.go b/localembed/embedder.go index 387dae2..5c8e153 100644 --- a/localembed/embedder.go +++ b/localembed/embedder.go @@ -275,7 +275,7 @@ func New(opts Options) (*Embedder, error) { descriptor: descriptor, queryPrefix: descriptor.QueryPrefix, backendName: backendName, - padID: int32(padID), + padID: int32(padID), // #nosec G115 -- padID is a tokenizer vocab id, always well under int32 range dim: dim, maxSeqLen: maxSeqLen, seqB: seqB, @@ -471,7 +471,7 @@ func (s *shared) encode(text string) []int32 { } row := make([]int32, len(ids)) for i, id := range ids { - row[i] = int32(id) + row[i] = int32(id) // #nosec G115 -- id is a tokenizer vocab id, always well under int32 range } return row } diff --git a/localembed/inference_test.go b/localembed/inference_test.go index b4a750e..aa611d4 100644 --- a/localembed/inference_test.go +++ b/localembed/inference_test.go @@ -469,7 +469,7 @@ func loadGolden(t *testing.T, dim int) [][]float32 { if err != nil { t.Fatalf("opening golden fixture: %v", err) } - defer f.Close() + defer func() { _ = f.Close() }() var out [][]float32 var current []float32 diff --git a/mcp/graph_test.go b/mcp/graph_test.go index 97af8f7..b4021e8 100644 --- a/mcp/graph_test.go +++ b/mcp/graph_test.go @@ -19,7 +19,7 @@ func setupGraphServer(t *testing.T, opts ...mcp.Option) *mcp.Server { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { s.Close() }) + t.Cleanup(func() { _ = s.Close() }) rec := func(id, path, name string, start, end int) store.Record { return store.Record{ @@ -73,7 +73,7 @@ func listToolNames(t *testing.T, srv *mcp.Server) map[string]*mcpsdk.Tool { if err != nil { t.Fatalf("connect: %v", err) } - t.Cleanup(func() { session.Close() }) + t.Cleanup(func() { _ = session.Close() }) res, err := session.ListTools(ctx, nil) if err != nil { @@ -221,7 +221,7 @@ func TestHandleGetCallers_GraphNotBuilt(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { s.Close() }) + t.Cleanup(func() { _ = s.Close() }) if err = s.Upsert(t.Context(), []store.Record{{ ID: "r1", FilePath: "a.go", Language: "go", diff --git a/mcp/server_test.go b/mcp/server_test.go index 1fdb6c6..6c33ad0 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -62,7 +62,7 @@ func setupTestServer(t *testing.T) *mcp.Server { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { s.Close() }) + t.Cleanup(func() { _ = s.Close() }) ctx := t.Context() records := []store.Record{ @@ -237,7 +237,7 @@ func setupTestServerWithRecords(t *testing.T, n int) *mcp.Server { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { st.Close() }) + t.Cleanup(func() { _ = st.Close() }) records := make([]store.Record, n) for i := range n { @@ -336,7 +336,7 @@ func TestHandleSearch_RefreshCooldown(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { st.Close() }) + t.Cleanup(func() { _ = st.Close() }) emb := &fakeEmbedder{vec: []float32{1, 0, 0}} q := query.New(emb, st) @@ -450,7 +450,7 @@ func TestHandleSearch_StaleRefreshInPlace(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { st.Close() }) + t.Cleanup(func() { _ = st.Close() }) ctx := t.Context() seedFile(t, st, "a.go", "oldhash", "OLD content") @@ -499,7 +499,7 @@ func TestHandleSearch_ManyStaleFlaggedNotRefreshed(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { st.Close() }) + t.Cleanup(func() { _ = st.Close() }) ctx := t.Context() const n = 15 // exceeds staleRefreshThreshold (10) @@ -544,7 +544,7 @@ func TestHandleSearch_FreshResultsNotFlagged(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { st.Close() }) + t.Cleanup(func() { _ = st.Close() }) ctx := t.Context() seedFile(t, st, "a.go", "samehash", "content") @@ -761,7 +761,7 @@ func setupTestServerWithMarkdown(t *testing.T) *mcp.Server { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { s.Close() }) + t.Cleanup(func() { _ = s.Close() }) ctx := t.Context() records := []store.Record{ @@ -832,7 +832,7 @@ func TestWatchLoop_ReindexTriggersFullIndex(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { st.Close() }) + t.Cleanup(func() { _ = st.Close() }) emb := &fakeEmbedder{vec: []float32{1, 0, 0}} q := query.New(emb, st) @@ -856,8 +856,8 @@ func TestWatchLoop_ReindexTriggersFullIndex(t *testing.T) { // Use a pipe so ServeIO blocks on reading (MCP loop waits for input). r, w := io.Pipe() - defer w.Close() - defer r.Close() + defer func() { _ = w.Close() }() + defer func() { _ = r.Close() }() var wg sync.WaitGroup wg.Add(1) diff --git a/query/graph_test.go b/query/graph_test.go index 2ebf75e..c734636 100644 --- a/query/graph_test.go +++ b/query/graph_test.go @@ -16,7 +16,7 @@ func graphSetup(t *testing.T, entries []store.FileRecords) (*query.Querier, stor if err != nil { t.Fatalf("open store: %v", err) } - t.Cleanup(func() { s.Close() }) + t.Cleanup(func() { _ = s.Close() }) if len(entries) > 0 { if err = s.ReplaceByFiles(t.Context(), entries); err != nil { diff --git a/query/query.go b/query/query.go index 227c2d4..2be945f 100644 --- a/query/query.go +++ b/query/query.go @@ -548,7 +548,7 @@ func renderMap(packages []packageGroup, opts MapOptions) string { vis = isExported(sym.Name, fileLang, sym.NodeKind) } lineRange := formatLineRange(sym.StartLine, sym.EndLine) - fb.WriteString(fmt.Sprintf(" %s%s %s%s\n", vis, formatNodeKind(sym.NodeKind), sym.Name, lineRange)) + fmt.Fprintf(&fb, " %s%s %s%s\n", vis, formatNodeKind(sym.NodeKind), sym.Name, lineRange) // Render children of this top-level symbol. for _, child := range children[sym.Name] { @@ -557,7 +557,7 @@ func renderMap(packages []packageGroup, opts MapOptions) string { childVis = isExported(child.Name, fileLang, child.NodeKind) } childLineRange := formatLineRange(child.StartLine, child.EndLine) - fb.WriteString(fmt.Sprintf(" %s%s %s%s\n", childVis, formatNodeKind(child.NodeKind), child.Name, childLineRange)) + fmt.Fprintf(&fb, " %s%s %s%s\n", childVis, formatNodeKind(child.NodeKind), child.Name, childLineRange) rendered[child.Parent+"\x00"+child.Name+"\x00"+strconv.Itoa(child.StartLine)] = struct{}{} } } @@ -576,7 +576,7 @@ func renderMap(packages []packageGroup, opts MapOptions) string { vis = isExported(sym.Name, fileLang, sym.NodeKind) } lineRange := formatLineRange(sym.StartLine, sym.EndLine) - fb.WriteString(fmt.Sprintf(" %s%s %s%s\n", vis, formatNodeKind(sym.NodeKind), sym.Name, lineRange)) + fmt.Fprintf(&fb, " %s%s %s%s\n", vis, formatNodeKind(sym.NodeKind), sym.Name, lineRange) } fb.WriteString("\n") @@ -601,7 +601,7 @@ func renderMap(packages []packageGroup, opts MapOptions) string { if truncated { remainingFiles := totalFiles - filesWritten remainingPkgs := len(packages) - pkgsWritten - b.WriteString(fmt.Sprintf("# ... %d more files in %d packages (truncated to ~%d tokens)\n", remainingFiles, remainingPkgs, opts.MaxTokens)) + fmt.Fprintf(&b, "# ... %d more files in %d packages (truncated to ~%d tokens)\n", remainingFiles, remainingPkgs, opts.MaxTokens) } return b.String() diff --git a/query/query_test.go b/query/query_test.go index 72eed7c..a51df28 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -47,7 +47,7 @@ func TestNew_NilDependencies(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() for _, tc := range []struct { name string @@ -74,7 +74,7 @@ func TestQuerier_Search(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -117,7 +117,7 @@ func TestQuerier_SearchWithOptions_PathFilter(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -165,7 +165,7 @@ func TestQuerier_SearchWithOptions_Offset(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -229,7 +229,7 @@ func TestQuerier_Map_BasicOutput(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -297,7 +297,7 @@ func TestQuerier_Map_Truncation(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() var records []store.Record @@ -334,7 +334,7 @@ func TestQuerier_Map_EmptyStore(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() emb := &fakeEmbedder{vec: []float32{1, 0, 0}} q := query.New(emb, s) @@ -355,7 +355,7 @@ func TestQuerier_Map_NestedSymbolsIndented(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -399,7 +399,7 @@ func TestQuerier_SearchWithOptions_MaxTokens(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() content := strings.Repeat("x", 100) @@ -437,7 +437,7 @@ func TestQuerier_SearchWithOptions_MaxTokens_Zero(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -470,7 +470,7 @@ func TestQuerier_SearchWithOptions_MaxTokens_AlwaysOneResult(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -537,7 +537,7 @@ func TestQuerier_CacheHit(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -584,7 +584,7 @@ func TestQuerier_SearchWithOptions_PackageFilter(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -632,7 +632,7 @@ func TestQuerier_Map_AllOptionsZeroValue(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -673,7 +673,7 @@ func TestQuerier_Map_CodeOnlyFilter(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -739,7 +739,7 @@ func TestQuerier_Map_LineRanges(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -786,7 +786,7 @@ func TestQuerier_Map_FileSummary(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() // 5 symbols in one file to trigger summary (threshold is 5). @@ -832,7 +832,7 @@ func TestQuerier_Map_Visibility(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -875,7 +875,7 @@ func TestQuerier_Map_VisibilityPython(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -918,7 +918,7 @@ func TestQuerier_Map_VisibilityJSExport(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -961,7 +961,7 @@ func TestQuerier_Map_NestingHierarchy(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1037,7 +1037,7 @@ func TestQuerier_Map_OrphanChildren(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1072,7 +1072,7 @@ func TestQuerier_Map_CodeOnlyCustomLanguages(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1129,7 +1129,7 @@ func TestQuerier_Map_CodeOnlyEmptyList(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ diff --git a/store/graph.go b/store/graph.go index 0469b1c..74705af 100644 --- a/store/graph.go +++ b/store/graph.go @@ -55,7 +55,7 @@ func (s *sqliteStore) scanEdges(ctx context.Context, query string, args []any) ( if err != nil { return nil, err } - defer rows.Close() + defer closeQuietly(rows) var edges []Edge for rows.Next() { @@ -105,7 +105,7 @@ func (s *sqliteStore) scanImports(ctx context.Context, query string, args []any) if err != nil { return nil, err } - defer rows.Close() + defer closeQuietly(rows) var imports []Import for rows.Next() { diff --git a/store/graph_test.go b/store/graph_test.go index 375780f..60506ee 100644 --- a/store/graph_test.go +++ b/store/graph_test.go @@ -13,7 +13,7 @@ func graphStore(t *testing.T) store.Store { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - t.Cleanup(func() { s.Close() }) + t.Cleanup(func() { _ = s.Close() }) return s } diff --git a/store/sqlite.go b/store/sqlite.go index 110a72f..852fa9e 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -6,6 +6,7 @@ import ( "database/sql" "errors" "fmt" + "io" "math" "os" "path/filepath" @@ -46,7 +47,7 @@ func NewSQLiteStore(dbPath string, embeddingModel string, embeddingDim int) (Sto } if dir := filepath.Dir(dbPath); dir != "." { - if err := os.MkdirAll(dir, 0o755); err != nil { + if err := os.MkdirAll(dir, 0o750); err != nil { return nil, fmt.Errorf("creating store directory: %w", err) } } @@ -63,13 +64,13 @@ func NewSQLiteStore(dbPath string, embeddingModel string, embeddingDim int) (Sto writer.SetMaxOpenConns(1) if err = writer.Ping(); err != nil { - writer.Close() + closeQuietly(writer) return nil, fmt.Errorf("connecting to writer database: %w", err) } reader, err := sql.Open("sqlite3", dbPath+"?"+pragmas+"&mode=ro") if err != nil { - writer.Close() + closeQuietly(writer) return nil, fmt.Errorf("opening reader database: %w", err) } maxReaders := max(min(runtime.NumCPU(), 4), 2) @@ -77,8 +78,8 @@ func NewSQLiteStore(dbPath string, embeddingModel string, embeddingDim int) (Sto s := &sqliteStore{reader: reader, writer: writer, embeddingDim: embeddingDim} if err = s.initSchema(context.Background(), embeddingModel, embeddingDim); err != nil { - writer.Close() - reader.Close() + closeQuietly(writer) + closeQuietly(reader) return nil, fmt.Errorf("initializing schema: %w", err) } @@ -95,13 +96,13 @@ func newMemoryStore(embeddingModel string, embeddingDim int) (Store, error) { db.SetMaxIdleConns(1) if err = db.Ping(); err != nil { - db.Close() + closeQuietly(db) return nil, fmt.Errorf("connecting to in-memory database: %w", err) } s := &sqliteStore{reader: db, writer: db, embeddingDim: embeddingDim} if err = s.initSchema(context.Background(), embeddingModel, embeddingDim); err != nil { - db.Close() + closeQuietly(db) return nil, fmt.Errorf("initializing schema: %w", err) } @@ -192,7 +193,7 @@ func (s *sqliteStore) createSchema(ctx context.Context, embeddingModel string, e if err != nil { return fmt.Errorf("beginning transaction: %w", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() for _, stmt := range ddl { if _, err := tx.ExecContext(ctx, stmt); err != nil { @@ -277,9 +278,11 @@ func (s *sqliteStore) SetMeta(ctx context.Context, key, value string) error { } func (s *sqliteStore) Close() error { - s.writer.ExecContext(context.Background(), "PRAGMA optimize") //nolint:errcheck + // PRAGMA optimize is best-effort maintenance: a failure here is harmless + // and would only mask the Close errors collected below. + _, _ = s.writer.ExecContext(context.Background(), "PRAGMA optimize") if s.reader != s.writer { - s.reader.ExecContext(context.Background(), "PRAGMA optimize") //nolint:errcheck + _, _ = s.reader.ExecContext(context.Background(), "PRAGMA optimize") } var errs []error if err := s.writer.Close(); err != nil { @@ -343,7 +346,7 @@ func (s *sqliteStore) Upsert(ctx context.Context, records []Record) error { if err != nil { return fmt.Errorf("beginning transaction: %w", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() chunkStmt, err := tx.PrepareContext(ctx, `INSERT OR REPLACE INTO chunks (id, file_path, language, content, content_hash, node_kind, name, parent, start_line, end_line) @@ -351,35 +354,35 @@ func (s *sqliteStore) Upsert(ctx context.Context, records []Record) error { if err != nil { return fmt.Errorf("preparing chunks insert: %w", err) } - defer chunkStmt.Close() + defer closeQuietly(chunkStmt) vecDeleteStmt, err := tx.PrepareContext(ctx, `DELETE FROM vec_chunks WHERE id = ?`) if err != nil { return fmt.Errorf("preparing vec delete: %w", err) } - defer vecDeleteStmt.Close() + defer closeQuietly(vecDeleteStmt) vecStmt, err := tx.PrepareContext(ctx, `INSERT INTO vec_chunks (id, language, embedding) VALUES (?, ?, ?)`) if err != nil { return fmt.Errorf("preparing vec insert: %w", err) } - defer vecStmt.Close() + defer closeQuietly(vecStmt) ftsDeleteStmt, err := tx.PrepareContext(ctx, `DELETE FROM chunks_fts WHERE id = ?`) if err != nil { return fmt.Errorf("preparing fts delete: %w", err) } - defer ftsDeleteStmt.Close() + defer closeQuietly(ftsDeleteStmt) ftsInsertStmt, err := tx.PrepareContext(ctx, `INSERT INTO chunks_fts (id, content, name, parent) VALUES (?, ?, ?, ?)`) if err != nil { return fmt.Errorf("preparing fts insert: %w", err) } - defer ftsInsertStmt.Close() + defer closeQuietly(ftsInsertStmt) for _, r := range records { if _, err = chunkStmt.ExecContext(ctx, r.ID, r.FilePath, r.Language, r.Content, r.ContentHash, r.NodeKind, r.Name, r.Parent, r.StartLine, r.EndLine); err != nil { @@ -422,25 +425,25 @@ func (s *sqliteStore) Delete(ctx context.Context, ids []string) error { if err != nil { return fmt.Errorf("beginning transaction: %w", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() delChunk, err := tx.PrepareContext(ctx, "DELETE FROM chunks WHERE id = ?") if err != nil { return fmt.Errorf("preparing chunk delete: %w", err) } - defer delChunk.Close() + defer closeQuietly(delChunk) delVec, err := tx.PrepareContext(ctx, "DELETE FROM vec_chunks WHERE id = ?") if err != nil { return fmt.Errorf("preparing vec delete: %w", err) } - defer delVec.Close() + defer closeQuietly(delVec) delFts, err := tx.PrepareContext(ctx, "DELETE FROM chunks_fts WHERE id = ?") if err != nil { return fmt.Errorf("preparing fts delete: %w", err) } - defer delFts.Close() + defer closeQuietly(delFts) for _, id := range ids { if _, err = delChunk.ExecContext(ctx, id); err != nil { @@ -462,13 +465,13 @@ func (s *sqliteStore) DeleteByFile(ctx context.Context, filePath string) error { if err != nil { return fmt.Errorf("beginning transaction: %w", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() rows, err := tx.QueryContext(ctx, "SELECT id FROM chunks WHERE file_path = ?", filePath) if err != nil { return fmt.Errorf("querying chunks for file %q: %w", filePath, err) } - defer rows.Close() + defer closeQuietly(rows) var ids []string for rows.Next() { @@ -481,7 +484,7 @@ func (s *sqliteStore) DeleteByFile(ctx context.Context, filePath string) error { if err = rows.Err(); err != nil { return fmt.Errorf("iterating chunk ids: %w", err) } - rows.Close() + closeQuietly(rows) for _, id := range ids { if _, err = tx.ExecContext(ctx, "DELETE FROM vec_chunks WHERE id = ?", id); err != nil { @@ -537,7 +540,7 @@ func (s *sqliteStore) ReplaceByFiles(ctx context.Context, entries []FileRecords) if err != nil { return fmt.Errorf("beginning transaction: %w", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() chunkStmt, err := tx.PrepareContext(ctx, `INSERT OR REPLACE INTO chunks (id, file_path, language, content, content_hash, node_kind, name, parent, start_line, end_line) @@ -545,45 +548,45 @@ func (s *sqliteStore) ReplaceByFiles(ctx context.Context, entries []FileRecords) if err != nil { return fmt.Errorf("preparing chunks insert: %w", err) } - defer chunkStmt.Close() + defer closeQuietly(chunkStmt) vecDeleteStmt, err := tx.PrepareContext(ctx, `DELETE FROM vec_chunks WHERE id = ?`) if err != nil { return fmt.Errorf("preparing vec delete: %w", err) } - defer vecDeleteStmt.Close() + defer closeQuietly(vecDeleteStmt) vecStmt, err := tx.PrepareContext(ctx, `INSERT INTO vec_chunks (id, language, embedding) VALUES (?, ?, ?)`) if err != nil { return fmt.Errorf("preparing vec insert: %w", err) } - defer vecStmt.Close() + defer closeQuietly(vecStmt) ftsDeleteStmt, err := tx.PrepareContext(ctx, `DELETE FROM chunks_fts WHERE id = ?`) if err != nil { return fmt.Errorf("preparing fts delete: %w", err) } - defer ftsDeleteStmt.Close() + defer closeQuietly(ftsDeleteStmt) ftsStmt, err := tx.PrepareContext(ctx, `INSERT INTO chunks_fts (id, content, name, parent) VALUES (?, ?, ?, ?)`) if err != nil { return fmt.Errorf("preparing fts insert: %w", err) } - defer ftsStmt.Close() + defer closeQuietly(ftsStmt) edgeStmt, err := tx.PrepareContext(ctx, `INSERT INTO edges (src_id, file_path, src_name, kind, dst_name, dst_qualifier, line) VALUES (?, ?, ?, ?, ?, ?, ?)`) if err != nil { return fmt.Errorf("preparing edges insert: %w", err) } - defer edgeStmt.Close() + defer closeQuietly(edgeStmt) importStmt, err := tx.PrepareContext(ctx, `INSERT INTO file_imports (file_path, module, alias, line) VALUES (?, ?, ?, ?)`) if err != nil { return fmt.Errorf("preparing imports insert: %w", err) } - defer importStmt.Close() + defer closeQuietly(importStmt) for _, entry := range entries { rows, err := tx.QueryContext(ctx, "SELECT id FROM chunks WHERE file_path = ?", entry.FilePath) @@ -594,16 +597,16 @@ func (s *sqliteStore) ReplaceByFiles(ctx context.Context, entries []FileRecords) for rows.Next() { var id string if err = rows.Scan(&id); err != nil { - rows.Close() + closeQuietly(rows) return fmt.Errorf("scanning chunk id: %w", err) } oldIDs = append(oldIDs, id) } if err = rows.Err(); err != nil { - rows.Close() + closeQuietly(rows) return fmt.Errorf("iterating chunk ids: %w", err) } - rows.Close() + closeQuietly(rows) for _, id := range oldIDs { if _, err = vecDeleteStmt.ExecContext(ctx, id); err != nil { @@ -728,17 +731,17 @@ func (s *sqliteStore) searchOnce(ctx context.Context, q SearchQuery, fetchLimit var id string var dist float64 if err = vecRows.Scan(&id, &dist); err != nil { - vecRows.Close() + closeQuietly(vecRows) return nil, fmt.Errorf("scanning vector result: %w", err) } idScores[id] = &scored{id: id, vecRank: rank} rank++ } if err = vecRows.Err(); err != nil { - vecRows.Close() + closeQuietly(vecRows) return nil, fmt.Errorf("iterating vector results: %w", err) } - vecRows.Close() + closeQuietly(vecRows) } // Step 2: BM25 full-text search with optional filter JOIN. @@ -750,7 +753,7 @@ func (s *sqliteStore) searchOnce(ctx context.Context, q SearchQuery, fetchLimit if err != nil { return nil, fmt.Errorf("bm25 search: %w", err) } - defer ftsRows.Close() + defer closeQuietly(ftsRows) rank := 1 for ftsRows.Next() { @@ -890,6 +893,14 @@ func placeholders(n int) string { return strings.Repeat("?,", n)[:n*2-1] } +// closeQuietly closes c, discarding any error. Used for best-effort cleanup +// during error unwinding (a more specific error is already being returned) +// or for a query's own rows.Close() after the query has been fully +// consumed, where a close failure would not be actionable either way. +func closeQuietly(c io.Closer) { + _ = c.Close() +} + func (s *sqliteStore) fetchRecordsFiltered(ctx context.Context, ids []string, q SearchQuery) (map[string]*Record, error) { if len(ids) == 0 { return map[string]*Record{}, nil @@ -950,23 +961,23 @@ func (s *sqliteStore) fetchRecordsFiltered(ctx context.Context, ids []string, q var r Record if q.MetadataOnly { if err = rows.Scan(&r.ID, &r.FilePath, &r.Language, &r.NodeKind, &r.Name, &r.Parent, &r.StartLine, &r.EndLine); err != nil { - rows.Close() + closeQuietly(rows) return nil, fmt.Errorf("scanning record: %w", err) } } else { if err = rows.Scan(&r.ID, &r.FilePath, &r.Language, &r.Content, &r.ContentHash, &r.NodeKind, &r.Name, &r.Parent, &r.StartLine, &r.EndLine); err != nil { - rows.Close() + closeQuietly(rows) return nil, fmt.Errorf("scanning record: %w", err) } } result[r.ID] = &r } if err = rows.Err(); err != nil { - rows.Close() + closeQuietly(rows) return nil, fmt.Errorf("iterating records: %w", err) } - rows.Close() + closeQuietly(rows) } return result, nil @@ -991,6 +1002,7 @@ func (s *sqliteStore) FileHashes(ctx context.Context, filePaths []string) (map[s } rows, err := s.reader.QueryContext(ctx, + // #nosec G202 -- ph is a fixed "?,?,..." placeholder string from placeholders(), not user input; values are passed via args "SELECT path, content_hash FROM files WHERE path IN ("+ph+")", args...) if err != nil { return nil, fmt.Errorf("querying file hashes: %w", err) @@ -999,16 +1011,16 @@ func (s *sqliteStore) FileHashes(ctx context.Context, filePaths []string) (map[s for rows.Next() { var path, hash string if err = rows.Scan(&path, &hash); err != nil { - rows.Close() + closeQuietly(rows) return nil, fmt.Errorf("scanning file hash: %w", err) } result[path] = hash } if err = rows.Err(); err != nil { - rows.Close() + closeQuietly(rows) return nil, fmt.Errorf("iterating file hashes: %w", err) } - rows.Close() + closeQuietly(rows) } return result, nil @@ -1033,6 +1045,7 @@ func (s *sqliteStore) FileStates(ctx context.Context, filePaths []string) (map[s } rows, err := s.reader.QueryContext(ctx, + // #nosec G202 -- ph is a fixed "?,?,..." placeholder string from placeholders(), not user input; values are passed via args "SELECT path, content_hash, mtime, size FROM files WHERE path IN ("+ph+")", args...) if err != nil { return nil, fmt.Errorf("querying file states: %w", err) @@ -1042,16 +1055,16 @@ func (s *sqliteStore) FileStates(ctx context.Context, filePaths []string) (map[s var path string var st FileState if err = rows.Scan(&path, &st.ContentHash, &st.Mtime, &st.Size); err != nil { - rows.Close() + closeQuietly(rows) return nil, fmt.Errorf("scanning file state: %w", err) } result[path] = st } if err = rows.Err(); err != nil { - rows.Close() + closeQuietly(rows) return nil, fmt.Errorf("iterating file states: %w", err) } - rows.Close() + closeQuietly(rows) } return result, nil @@ -1062,7 +1075,7 @@ func (s *sqliteStore) ChunkHashesByFile(ctx context.Context, filePath string) (m if err != nil { return nil, fmt.Errorf("querying chunks for file %q: %w", filePath, err) } - defer rows.Close() + defer closeQuietly(rows) result := make(map[string]string) for rows.Next() { @@ -1112,7 +1125,7 @@ func (s *sqliteStore) EmbeddingsByContentHash(ctx context.Context, contentHashes if err != nil { return fmt.Errorf("querying embeddings by content hash: %w", err) } - defer rows.Close() + defer closeQuietly(rows) for rows.Next() { var ch string @@ -1176,7 +1189,7 @@ func (s *sqliteStore) ListFiles(ctx context.Context) ([]string, error) { if err != nil { return nil, fmt.Errorf("listing files: %w", err) } - defer rows.Close() + defer closeQuietly(rows) paths := make([]string, 0) for rows.Next() { @@ -1206,7 +1219,7 @@ func (s *sqliteStore) Stats(ctx context.Context) (IndexStats, error) { if err != nil { return IndexStats{}, fmt.Errorf("querying language stats: %w", err) } - defer rows.Close() + defer closeQuietly(rows) stats.Languages = make(map[string]int) for rows.Next() { @@ -1239,7 +1252,7 @@ func (s *sqliteStore) ListSymbols(ctx context.Context) ([]Symbol, error) { if err != nil { return nil, fmt.Errorf("listing symbols: %w", err) } - defer rows.Close() + defer closeQuietly(rows) symbols := make([]Symbol, 0, count) for rows.Next() { diff --git a/store/store_test.go b/store/store_test.go index c23f9fb..1b929f4 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -62,7 +62,7 @@ func TestNewSQLiteStore(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() } func TestNewSQLiteStore_MetadataPersisted(t *testing.T) { @@ -81,13 +81,13 @@ func TestNewSQLiteStore_MetadataPersisted(t *testing.T) { if v != "test-model" { t.Errorf("embedding_model = %q, want %q", v, "test-model") } - s.Close() + _ = s.Close() s2, err := store.NewSQLiteStore(dbPath, "test-model", 3) if err != nil { t.Fatalf("second open: %v", err) } - defer s2.Close() + defer func() { _ = s2.Close() }() } func TestNewSQLiteStore_ModelMismatch(t *testing.T) { @@ -97,7 +97,7 @@ func TestNewSQLiteStore_ModelMismatch(t *testing.T) { if err != nil { t.Fatalf("first open: %v", err) } - s.Close() + _ = s.Close() _, err = store.NewSQLiteStore(dbPath, "model-b", 3) if err == nil { @@ -112,7 +112,7 @@ func TestNewSQLiteStore_DimMismatch(t *testing.T) { if err != nil { t.Fatalf("first open: %v", err) } - s.Close() + _ = s.Close() _, err = store.NewSQLiteStore(dbPath, "model-a", 768) if err == nil { @@ -135,7 +135,7 @@ func TestNewSQLiteStore_SchemaVersionMismatch(t *testing.T) { if err = s.SetMeta(t.Context(), "schema_version", "2"); err != nil { t.Fatalf("SetMeta: %v", err) } - s.Close() + _ = s.Close() _, err = store.NewSQLiteStore(dbPath, "model-a", 3) if err == nil { @@ -176,7 +176,7 @@ func TestCheckpoint(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() r := makeRecord("main.go", "main", "func main() {}", []float32{0.1, 0.2, 0.3}) @@ -210,7 +210,7 @@ func TestUpsert_and_Delete(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() r := makeRecord("main.go", "main", "func main() {}", []float32{0.1, 0.2, 0.3}) @@ -234,7 +234,7 @@ func TestUpsert_BatchMultipleRecords(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -253,7 +253,7 @@ func TestDeleteByFile(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -294,7 +294,7 @@ func TestUpsert_WrongEmbeddingDimension(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() r := makeRecord("main.go", "main", "func main() {}", []float32{0.1, 0.2, 0.3, 0.4}) // 4 floats, store expects 3 @@ -308,7 +308,7 @@ func TestUpsert_EmptySlice(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() if err = s.Upsert(ctx, nil); err != nil { @@ -324,7 +324,7 @@ func TestFileHashes(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() // No files yet — query should return empty map. @@ -367,7 +367,7 @@ func TestListFiles(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() @@ -400,7 +400,7 @@ func TestSearch_VectorOnly(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -436,7 +436,7 @@ func TestSearch_BM25Only(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -472,7 +472,7 @@ func TestSearch_HybridMerge(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -510,7 +510,7 @@ func TestSearch_LanguageFilter(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() goRecord := makeRecord("main.go", "main", "func main() {}", []float32{0.9, 0.1, 0.0}) @@ -545,7 +545,7 @@ func TestSearch_EmptyStore(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() results, err := s.Search(ctx, store.SearchQuery{ @@ -566,7 +566,7 @@ func TestEmbeddingsByContentHash(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() r1 := makeRecord("a.go", "funcA", "func A() {}", []float32{0.1, 0.2, 0.3}) @@ -602,7 +602,7 @@ func TestSearch_PathFilter(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -666,7 +666,7 @@ func TestSearch_PathFilter_Empty(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -703,7 +703,7 @@ func TestNewSQLiteStore_CreatesDirectory(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() if _, err = os.Stat(filepath.Join(dir, "nested", "subdir")); errors.Is(err, fs.ErrNotExist) { t.Error("NewSQLiteStore did not create nested directory") @@ -716,7 +716,7 @@ func TestSQLiteStore_SearchOffset(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -767,7 +767,7 @@ func TestSQLiteStore_Stats(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() // Empty store. @@ -814,7 +814,7 @@ func TestIntegration_FullWorkflow(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() @@ -924,7 +924,7 @@ func TestListSymbols(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1004,7 +1004,7 @@ func TestReplaceByFiles(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() oldRecords := []store.Record{ @@ -1077,7 +1077,7 @@ func TestReplaceByFiles_RollbackOnBadEmbedding(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() oldRecord := store.Record{ @@ -1133,7 +1133,7 @@ func TestReplaceByFiles_MultiFile(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() entries := []store.FileRecords{ @@ -1221,7 +1221,7 @@ func TestFileHashes_Batch(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() entries := []store.FileRecords{ @@ -1274,7 +1274,7 @@ func TestSearch_CombinedFilters(t *testing.T) { if err != nil { t.Fatalf("open: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1313,7 +1313,7 @@ func TestSQLiteStore_CurrentSchemaVersion(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() v, err := s.Meta(ctx, "schema_version") @@ -1330,7 +1330,7 @@ func TestListSymbols_EmptyStore(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() symbols, err := s.ListSymbols(t.Context()) if err != nil { @@ -1347,7 +1347,7 @@ func TestSearch_NameFilter(t *testing.T) { if err != nil { t.Fatalf("open: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1381,7 +1381,7 @@ func TestSearch_NodeKindFilter(t *testing.T) { if err != nil { t.Fatalf("open: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1415,7 +1415,7 @@ func TestSearch_MetadataOnly(t *testing.T) { if err != nil { t.Fatalf("open: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1454,7 +1454,7 @@ func TestSearch_SingleLanguageKNNPreFilter(t *testing.T) { if err != nil { t.Fatalf("open: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := []store.Record{ @@ -1488,7 +1488,7 @@ func TestSearch_BatchFetch(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() records := make([]store.Record, 20) @@ -1538,7 +1538,7 @@ func TestSearch_IterativeDeepening(t *testing.T) { if err != nil { t.Fatalf("open: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() @@ -1592,7 +1592,7 @@ func TestEmbeddingsByContentHash_Deterministic(t *testing.T) { if err != nil { t.Fatalf("NewSQLiteStore: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() @@ -1648,7 +1648,7 @@ func TestConcurrentReadWrite(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() @@ -1728,7 +1728,7 @@ func TestFileStates(t *testing.T) { if err != nil { t.Fatalf("open store: %v", err) } - defer s.Close() + defer func() { _ = s.Close() }() ctx := t.Context() // Absent path: omitted from the result. diff --git a/walker/walker.go b/walker/walker.go index 5ac6d0b..1a85f63 100644 --- a/walker/walker.go +++ b/walker/walker.go @@ -387,7 +387,7 @@ func readIgnoreFile(ctx context.Context, fsys fs.FS, filePath string) []string { if err != nil { return nil } - defer f.Close() + defer func() { _ = f.Close() }() // best-effort cleanup; the file is only being read var lines []string scanner := bufio.NewScanner(f) diff --git a/walker/walker_test.go b/walker/walker_test.go index 1304209..1113906 100644 --- a/walker/walker_test.go +++ b/walker/walker_test.go @@ -21,7 +21,7 @@ func setupTree(t *testing.T) string { dirs := []string{"src", "src/utils", "vendor", ".git"} for _, d := range dirs { - os.MkdirAll(filepath.Join(root, d), 0o755) + _ = os.MkdirAll(filepath.Join(root, d), 0o755) } files := map[string]string{ @@ -33,7 +33,7 @@ func setupTree(t *testing.T) string { ".gitignore": "vendor/\n", } for name, content := range files { - os.WriteFile(filepath.Join(root, name), []byte(content), 0o644) + _ = os.WriteFile(filepath.Join(root, name), []byte(content), 0o644) } return root @@ -46,7 +46,7 @@ func TestWalk_BasicTraversal(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var paths []string for path, err := range w.Walk(t.Context()) { @@ -89,7 +89,7 @@ func TestWalk_IncludePatterns(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() for path, err := range w.Walk(t.Context()) { if err != nil { @@ -112,7 +112,7 @@ func TestWalk_ExcludePatterns(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() for path, err := range w.Walk(t.Context()) { if err != nil { @@ -132,7 +132,7 @@ func TestWalk_ContextCancellation(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithCancel(t.Context()) cancel() // cancel immediately @@ -160,7 +160,7 @@ func TestMatch(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() if !w.Match(filepath.Join(root, "src/main.go")) { t.Error("Match should return true for src/main.go") @@ -200,7 +200,7 @@ func TestWalk_SkipsCodamigoDir(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() for path, err := range w.Walk(t.Context()) { if err != nil { @@ -219,7 +219,7 @@ func TestMatch_SkipsCodamigoDir(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() codamigoPath := filepath.Join(root, ".codamigo", "store.db") if w.Match(codamigoPath) { @@ -233,14 +233,14 @@ func TestWalk_NestedGitignore(t *testing.T) { // Create directory structure. dirs := []string{"src", "src/vendor", "src/lib", "build"} for _, d := range dirs { - os.MkdirAll(filepath.Join(root, d), 0o755) + _ = os.MkdirAll(filepath.Join(root, d), 0o755) } // Root .gitignore: ignore build/ - os.WriteFile(filepath.Join(root, ".gitignore"), []byte("build/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".gitignore"), []byte("build/\n"), 0o644) // Nested .gitignore in src/: ignore vendor/ - os.WriteFile(filepath.Join(root, "src", ".gitignore"), []byte("vendor/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, "src", ".gitignore"), []byte("vendor/\n"), 0o644) // Create files. files := []string{ @@ -251,7 +251,7 @@ func TestWalk_NestedGitignore(t *testing.T) { "build/output.go", } for _, f := range files { - os.WriteFile(filepath.Join(root, f), []byte("package x"), 0o644) + _ = os.WriteFile(filepath.Join(root, f), []byte("package x"), 0o644) } cfg := &config.Config{ProjectRoot: root} @@ -259,7 +259,7 @@ func TestWalk_NestedGitignore(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var walked []string for path, err := range w.Walk(t.Context()) { @@ -282,16 +282,16 @@ func TestWalk_NestedGitignore(t *testing.T) { func TestWalker_IsIgnored(t *testing.T) { root := t.TempDir() - os.MkdirAll(filepath.Join(root, "src", "vendor"), 0o755) - os.WriteFile(filepath.Join(root, ".gitignore"), []byte("*.log\n"), 0o644) - os.WriteFile(filepath.Join(root, "src", ".gitignore"), []byte("vendor/\n"), 0o644) + _ = os.MkdirAll(filepath.Join(root, "src", "vendor"), 0o755) + _ = os.WriteFile(filepath.Join(root, ".gitignore"), []byte("*.log\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, "src", ".gitignore"), []byte("vendor/\n"), 0o644) cfg := &config.Config{ProjectRoot: root} w, err := walker.New(root, cfg) if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() tests := []struct { path string @@ -316,14 +316,14 @@ func TestWalk_NestedGitignoreNegation(t *testing.T) { dirs := []string{"src", "src/generated", "src/generated/keep"} for _, d := range dirs { - os.MkdirAll(filepath.Join(root, d), 0o755) + _ = os.MkdirAll(filepath.Join(root, d), 0o755) } // Root .gitignore: ignore all generated/ dirs. - os.WriteFile(filepath.Join(root, ".gitignore"), []byte("generated/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".gitignore"), []byte("generated/\n"), 0o644) // Nested .gitignore in src/: re-include generated/ via negation. - os.WriteFile(filepath.Join(root, "src", ".gitignore"), []byte("!generated/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, "src", ".gitignore"), []byte("!generated/\n"), 0o644) files := []string{ "main.go", @@ -331,7 +331,7 @@ func TestWalk_NestedGitignoreNegation(t *testing.T) { "src/generated/models.go", } for _, f := range files { - os.WriteFile(filepath.Join(root, f), []byte("package x"), 0o644) + _ = os.WriteFile(filepath.Join(root, f), []byte("package x"), 0o644) } cfg := &config.Config{ProjectRoot: root} @@ -339,7 +339,7 @@ func TestWalk_NestedGitignoreNegation(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var walked []string for path, err := range w.Walk(t.Context()) { @@ -362,18 +362,18 @@ func TestWalk_NestedGitignoreNegation(t *testing.T) { func TestWalk_CaignoreOnly(t *testing.T) { root := t.TempDir() - os.MkdirAll(filepath.Join(root, "vendor"), 0o755) - os.WriteFile(filepath.Join(root, "vendor", "dep.go"), []byte("package dep"), 0o644) - os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) + _ = os.MkdirAll(filepath.Join(root, "vendor"), 0o755) + _ = os.WriteFile(filepath.Join(root, "vendor", "dep.go"), []byte("package dep"), 0o644) + _ = os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) // No .gitignore — only .caignore. - os.WriteFile(filepath.Join(root, ".caignore"), []byte("vendor/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".caignore"), []byte("vendor/\n"), 0o644) cfg := &config.Config{ProjectRoot: root} w, err := walker.New(root, cfg) if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var walked []string for path, err := range w.Walk(t.Context()) { @@ -395,22 +395,22 @@ func TestWalk_CaignoreOnly(t *testing.T) { func TestWalk_CaignoreExtends(t *testing.T) { root := t.TempDir() - os.MkdirAll(filepath.Join(root, "logs"), 0o755) - os.MkdirAll(filepath.Join(root, "tmp"), 0o755) - os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) - os.WriteFile(filepath.Join(root, "logs", "app.log"), []byte("log data"), 0o644) - os.WriteFile(filepath.Join(root, "tmp", "scratch.txt"), []byte("temp"), 0o644) + _ = os.MkdirAll(filepath.Join(root, "logs"), 0o755) + _ = os.MkdirAll(filepath.Join(root, "tmp"), 0o755) + _ = os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) + _ = os.WriteFile(filepath.Join(root, "logs", "app.log"), []byte("log data"), 0o644) + _ = os.WriteFile(filepath.Join(root, "tmp", "scratch.txt"), []byte("temp"), 0o644) // .gitignore ignores logs/ - os.WriteFile(filepath.Join(root, ".gitignore"), []byte("logs/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".gitignore"), []byte("logs/\n"), 0o644) // .caignore additionally ignores tmp/ - os.WriteFile(filepath.Join(root, ".caignore"), []byte("tmp/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".caignore"), []byte("tmp/\n"), 0o644) cfg := &config.Config{ProjectRoot: root} w, err := walker.New(root, cfg) if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var walked []string for path, err := range w.Walk(t.Context()) { @@ -433,20 +433,20 @@ func TestWalk_CaignoreExtends(t *testing.T) { func TestWalk_CaignoreNegationOverride(t *testing.T) { root := t.TempDir() - os.MkdirAll(filepath.Join(root, "generated"), 0o755) - os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) - os.WriteFile(filepath.Join(root, "generated", "models.go"), []byte("package gen"), 0o644) + _ = os.MkdirAll(filepath.Join(root, "generated"), 0o755) + _ = os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) + _ = os.WriteFile(filepath.Join(root, "generated", "models.go"), []byte("package gen"), 0o644) // .gitignore ignores generated/ - os.WriteFile(filepath.Join(root, ".gitignore"), []byte("generated/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".gitignore"), []byte("generated/\n"), 0o644) // .caignore re-includes generated/ via negation. - os.WriteFile(filepath.Join(root, ".caignore"), []byte("!generated/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".caignore"), []byte("!generated/\n"), 0o644) cfg := &config.Config{ProjectRoot: root} w, err := walker.New(root, cfg) if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var walked []string for path, err := range w.Walk(t.Context()) { @@ -469,15 +469,15 @@ func TestWalk_CaignoreNegationOverride(t *testing.T) { func TestWalk_CaignoreFileNotYielded(t *testing.T) { root := t.TempDir() - os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) - os.WriteFile(filepath.Join(root, ".caignore"), []byte("*.tmp\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".caignore"), []byte("*.tmp\n"), 0o644) cfg := &config.Config{ProjectRoot: root} w, err := walker.New(root, cfg) if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() for path, err := range w.Walk(t.Context()) { if err != nil { @@ -493,18 +493,18 @@ func TestWalk_CaignoreFileNotYielded(t *testing.T) { func TestWalker_CaignoreMatchAndIsIgnored(t *testing.T) { root := t.TempDir() - os.MkdirAll(filepath.Join(root, "cache"), 0o755) - os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) - os.WriteFile(filepath.Join(root, "cache", "data.bin"), []byte("binary"), 0o644) + _ = os.MkdirAll(filepath.Join(root, "cache"), 0o755) + _ = os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) + _ = os.WriteFile(filepath.Join(root, "cache", "data.bin"), []byte("binary"), 0o644) // No .gitignore. .caignore ignores cache/. - os.WriteFile(filepath.Join(root, ".caignore"), []byte("cache/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, ".caignore"), []byte("cache/\n"), 0o644) cfg := &config.Config{ProjectRoot: root} w, err := walker.New(root, cfg) if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() if !w.Match(filepath.Join(root, "main.go")) { t.Error("Match should return true for main.go") @@ -525,22 +525,22 @@ func TestWalk_NestedCaignore(t *testing.T) { dirs := []string{"src", "src/fixtures", "lib"} for _, d := range dirs { - os.MkdirAll(filepath.Join(root, d), 0o755) + _ = os.MkdirAll(filepath.Join(root, d), 0o755) } - os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) - os.WriteFile(filepath.Join(root, "src", "app.go"), []byte("package src"), 0o644) - os.WriteFile(filepath.Join(root, "src", "fixtures", "data.json"), []byte("{}"), 0o644) - os.WriteFile(filepath.Join(root, "lib", "util.go"), []byte("package lib"), 0o644) + _ = os.WriteFile(filepath.Join(root, "main.go"), []byte("package main"), 0o644) + _ = os.WriteFile(filepath.Join(root, "src", "app.go"), []byte("package src"), 0o644) + _ = os.WriteFile(filepath.Join(root, "src", "fixtures", "data.json"), []byte("{}"), 0o644) + _ = os.WriteFile(filepath.Join(root, "lib", "util.go"), []byte("package lib"), 0o644) // Nested .caignore in src/ ignores fixtures/ (only applies under src/). - os.WriteFile(filepath.Join(root, "src", ".caignore"), []byte("fixtures/\n"), 0o644) + _ = os.WriteFile(filepath.Join(root, "src", ".caignore"), []byte("fixtures/\n"), 0o644) cfg := &config.Config{ProjectRoot: root} w, err := walker.New(root, cfg) if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var walked []string for path, err := range w.Walk(t.Context()) { @@ -585,7 +585,7 @@ func TestWalker_GitignorePartialReadOnScannerError(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var walked []string for path, err := range w.Walk(t.Context()) { @@ -677,7 +677,7 @@ func TestWalk_Symlinks(t *testing.T) { if err != nil { t.Fatalf("walker.New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() var yielded []string for path, err := range w.Walk(t.Context()) { @@ -712,16 +712,16 @@ func TestWalk_Symlinks(t *testing.T) { func TestConcurrentWalkAndMatch(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "a.go"), []byte("package a"), 0o644) - os.MkdirAll(filepath.Join(dir, "sub"), 0o755) - os.WriteFile(filepath.Join(dir, "sub", "b.go"), []byte("package b"), 0o644) + _ = os.WriteFile(filepath.Join(dir, "a.go"), []byte("package a"), 0o644) + _ = os.MkdirAll(filepath.Join(dir, "sub"), 0o755) + _ = os.WriteFile(filepath.Join(dir, "sub", "b.go"), []byte("package b"), 0o644) cfg := config.Defaults() w, err := walker.New(dir, cfg) if err != nil { t.Fatalf("New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() @@ -772,7 +772,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() var yielded []string for path, err := range w.Walk(t.Context()) { @@ -796,7 +796,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() for path, err := range w.Walk(t.Context()) { if err != nil { @@ -816,7 +816,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() for path, err := range w.Walk(t.Context()) { if err != nil { @@ -837,7 +837,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() var yielded []string for path, err := range w.Walk(t.Context()) { @@ -858,7 +858,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() if w.Match(filepath.Join(root, "font.ttf")) { t.Error("Match should return false for .ttf with extension filter") @@ -876,7 +876,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() if w.Match(filepath.Join(root, "data.csv")) { t.Error("Match should return false for .csv (unsupported extension via IndexFiles path)") @@ -895,7 +895,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() var yielded []string for path, err := range w.Walk(t.Context()) { @@ -926,7 +926,7 @@ func TestWithFileFilter(t *testing.T) { if err != nil { t.Fatal(err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() diff --git a/watcher/backend_inotify.go b/watcher/backend_inotify.go index c677992..0046eb5 100644 --- a/watcher/backend_inotify.go +++ b/watcher/backend_inotify.go @@ -67,7 +67,7 @@ func newBackend(ev chan fsEvent, errs chan error) (backend, error) { } func (w *inotifyWatcher) Close() error { - if w.shared.close() { + if w.close() { return nil } @@ -106,12 +106,12 @@ func (w *inotifyWatcher) register(path string, flags uint32) error { return err } - if _, ok := w.watches.wd[uint32(wd)]; ok { + if _, ok := w.watches.wd[uint32(wd)]; ok { // #nosec G115 -- wd is an inotify watch descriptor, checked != -1 above; always small and non-negative return nil } w.watches.add(&iwatch{ - wd: uint32(wd), + wd: uint32(wd), // #nosec G115 -- wd is an inotify watch descriptor, checked != -1 above; always small and non-negative path: path, }) return nil @@ -178,9 +178,12 @@ func (w *inotifyWatcher) readEvents() { continue } + // n is a byte count from Read() into a fixed 4096-event buffer, + // checked >= SizeofInotifyEvent above; never near uint32 range. + limit := uint32(n - unix.SizeofInotifyEvent) // #nosec G115 var offset uint32 - for offset <= uint32(n-unix.SizeofInotifyEvent) { - inEvent := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) + for offset <= limit { + inEvent := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) // #nosec G103 -- standard way to decode inotify's binary event stream; offset is bounds-checked by the loop condition above if inEvent.Mask&unix.IN_Q_OVERFLOW != 0 { if !w.sendError(ErrEventOverflow) { @@ -209,7 +212,7 @@ func (w *inotifyWatcher) readEvents() { func (w *inotifyWatcher) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offset uint32) (fsEvent, bool, error) { w.mu.Lock() - watch := w.watches.byWd(uint32(inEvent.Wd)) + watch := w.watches.byWd(uint32(inEvent.Wd)) // #nosec G115 -- inEvent.Wd mirrors a watch descriptor this process itself registered; always small and non-negative if watch == nil { w.mu.Unlock() return fsEvent{}, true, nil @@ -271,6 +274,7 @@ func (w *inotifyWatcher) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byt func inotifyEventName(buf *[65536]byte, offset, nameLen uint32) string { start := int(offset + unix.SizeofInotifyEvent) + // #nosec G103 -- casting the raw read buffer to a fixed-size byte array is the standard way to read the variable-length name inotify appends after each event; start/nameLen come from the kernel-reported event length, bounds-checked by readEvents' loop bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[start]))[:nameLen:nameLen] for nameLen > 0 && bytes[nameLen-1] == 0 { nameLen-- diff --git a/watcher/backend_kqueue.go b/watcher/backend_kqueue.go index 9ad1750..f27cded 100644 --- a/watcher/backend_kqueue.go +++ b/watcher/backend_kqueue.go @@ -200,7 +200,7 @@ func newKqueue() (kq int, closepipe [2]int, err error) { err = unix.Pipe(closepipe[:]) if err != nil { - unix.Close(kq) + _ = unix.Close(kq) return kq, closepipe, err } unix.CloseOnExec(closepipe[0]) @@ -212,16 +212,16 @@ func newKqueue() (kq int, closepipe [2]int, err error) { ok, err := unix.Kevent(kq, changes, nil, nil) if ok == -1 { - unix.Close(kq) - unix.Close(closepipe[0]) - unix.Close(closepipe[1]) + _ = unix.Close(kq) + _ = unix.Close(closepipe[0]) + _ = unix.Close(closepipe[1]) return kq, closepipe, err } return kq, closepipe, nil } func (w *kqueueWatcher) Close() error { - if w.shared.close() { + if w.close() { return nil } @@ -239,11 +239,11 @@ func (w *kqueueWatcher) Close() error { continue } _ = w.register([]int{info.wd}, unix.EV_DELETE, 0) - unix.Close(info.wd) + _ = unix.Close(info.wd) w.watches.remove(info.wd, name) } - unix.Close(w.closepipe[1]) // Send "quit" message to readEvents + _ = unix.Close(w.closepipe[1]) // Send "quit" message to readEvents return nil } @@ -276,14 +276,14 @@ func (w *kqueueWatcher) remove(name string, unwatchFiles bool) error { return err } - unix.Close(info.wd) + _ = unix.Close(info.wd) isDir := w.watches.remove(info.wd, name) if unwatchFiles && isDir { pathsToRemove := w.watches.watchesInDir(name) for _, p := range pathsToRemove { - w.Remove(p) + _ = w.Remove(p) } } return nil @@ -351,7 +351,7 @@ func (w *kqueueWatcher) addWatch(name string, flags uint32, listDir bool) (strin err := w.register([]int{info.wd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags) if err != nil { - unix.Close(info.wd) + _ = unix.Close(info.wd) return "", err } @@ -386,7 +386,7 @@ func (w *kqueueWatcher) readEvents() { close(w.events) close(w.errors) _ = unix.Close(w.kq) - unix.Close(w.closepipe[0]) + _ = unix.Close(w.closepipe[0]) }() eventBuffer := make([]unix.Kevent_t, 10) @@ -402,7 +402,7 @@ func (w *kqueueWatcher) readEvents() { for _, kevent := range kevents { var ( - wd = int(kevent.Ident) + wd = int(kevent.Ident) // #nosec G115 -- kqueue idents mirror small fds/watch descriptors, never near int overflow mask = uint32(kevent.Fflags) ) @@ -424,12 +424,12 @@ func (w *kqueueWatcher) readEvents() { event := w.newEvent(path.name, path.linkName, mask) if event.Has(fsRename) || event.Has(fsRemove) { - w.remove(event.Name, false) + _ = w.remove(event.Name, false) w.watches.markSeen(event.Name, false) } if path.isDir && event.Has(fsWrite) && !event.Has(fsRemove) { - w.dirChange(event.Name) + _ = w.dirChange(event.Name) } else if !w.sendEvent(event) { return } diff --git a/watcher/fsnotify.go b/watcher/fsnotify.go index 60d0377..ad59a90 100644 --- a/watcher/fsnotify.go +++ b/watcher/fsnotify.go @@ -57,7 +57,7 @@ func newFSNotifyWatcher(cfg *config.Config, matchFn func(string) bool, fsys fs.F watchLimitHit, err := w.addDirs() if err != nil { - bw.Close() + _ = bw.Close() // best-effort cleanup; the addDirs error below is the one worth reporting return nil, false, err } @@ -257,9 +257,9 @@ func (w *fsnotifyWatcher) probe(timeout time.Duration) bool { return w.probeFn(timeout) } probePath := filepath.Join(w.root, ".watchprobe") - defer os.Remove(probePath) + defer func() { _ = os.Remove(probePath) }() // best-effort cleanup of the throwaway probe file - if err := os.WriteFile(probePath, []byte("probe"), 0o644); err != nil { + if err := os.WriteFile(probePath, []byte("probe"), 0o600); err != nil { slog.Warn("fsnotify probe: cannot create probe file", slog.Any("error", err)) return true } diff --git a/watcher/watcher.go b/watcher/watcher.go index 2678e81..83c00d7 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -93,13 +93,13 @@ func New(cfg *config.Config, matchFn func(string) bool, fsys fs.FS) (Watcher, er return newPollWatcher(cfg, matchFn, fsys), nil } if watchLimitHit { - fw.Close() + _ = fw.Close() // best-effort cleanup; we fall back to the poll watcher either way slog.Warn("watch limit reached, falling back to polling", slog.String("hint", "on Linux: increase fs.inotify.max_user_watches; on macOS: increase ulimit -n")) return newPollWatcher(cfg, matchFn, fsys), nil } if !fw.probe(2 * time.Second) { - fw.Close() + _ = fw.Close() // best-effort cleanup; we fall back to the poll watcher either way slog.Warn("fsnotify probe failed — events not delivered; falling back to polling", slog.String("hint", "this may indicate a Docker bind mount or network filesystem; set watch_mode: \"poll\" to skip this probe")) return newPollWatcher(cfg, matchFn, fsys), nil diff --git a/watcher/watcher_test.go b/watcher/watcher_test.go index d2b9b70..d3da856 100644 --- a/watcher/watcher_test.go +++ b/watcher/watcher_test.go @@ -34,7 +34,7 @@ func TestPollWatcher_DetectsNewFile(t *testing.T) { if err != nil { t.Fatalf("new watcher: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second) defer cancel() @@ -84,7 +84,7 @@ func TestPollWatcher_DetectsModification(t *testing.T) { if err != nil { t.Fatalf("new watcher: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second) defer cancel() @@ -133,7 +133,7 @@ func TestPollWatcher_DetectsRemoval(t *testing.T) { if err != nil { t.Fatalf("new watcher: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second) defer cancel() @@ -175,7 +175,7 @@ func TestPollWatcher_ContextCancellation(t *testing.T) { if err != nil { t.Fatalf("new watcher: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithCancel(t.Context()) ch := w.Watch(ctx) @@ -215,7 +215,7 @@ func TestFSNotifyWatcher_DetectsNewFile(t *testing.T) { if err != nil { t.Skipf("fsnotify not available: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() @@ -262,7 +262,7 @@ func TestFSNotifyWatcher_DetectsRemoval(t *testing.T) { if err != nil { t.Skipf("fsnotify not available: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() @@ -305,7 +305,7 @@ func TestPollWatcher_AdaptiveInterval(t *testing.T) { if err != nil { t.Fatalf("New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() @@ -356,7 +356,7 @@ func TestFSNotifyWatcher_DetectsModification(t *testing.T) { if err != nil { t.Skipf("fsnotify not available: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() @@ -403,7 +403,7 @@ func TestPollWatcher_MatchFnFilters(t *testing.T) { if err != nil { t.Fatalf("new watcher: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() @@ -502,7 +502,7 @@ func TestFSNotifyWatcher_CleansUpDeletedDir(t *testing.T) { if err != nil { t.Skipf("fsnotify not available: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() @@ -584,7 +584,7 @@ func TestFSNotifyWatcher_DetectsNewSubdir(t *testing.T) { if err != nil { t.Skipf("fsnotify not available: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() @@ -666,7 +666,7 @@ func TestNew_AutoModeWithProbe(t *testing.T) { if err != nil { t.Fatalf("New: %v", err) } - defer w.Close() + defer func() { _ = w.Close() }() // In "auto" mode on a real filesystem, the probe should succeed and // return an fsnotify watcher. Verify the watcher delivers events.