Skip to content

Commit 2fcb428

Browse files
jedudenclaude
andcommitted
Fix cachedGitignore cache-key correctness
Address PR #215 review (thread on internal/fix/fix.go:65): the previous "drop the Abs error fallback" simplification was wrong — filepath.Abs returns "" on failure (not the input string), so on the rare error path every relative dir would collide on the empty cache key and share one matcher across unrelated directories. My preceding comment also misdescribed Abs's error semantics. Fix the right way: don't normalize the cache key at all. lint.NewGitignoreMatcher does its own filepath.Abs internally to root the matcher, so the cache key only needs to be deterministic across calls within a Fix run, which prepareFile already guarantees by passing the same form (filepath.Dir(path) or f.RootDir). Use the dir string verbatim and update the comment to match what the code actually does. Add TestFixer_CachedGitignore_DistinctKeys documenting the cache contract: distinct inputs yield distinct matchers, repeated input hits the cache, empty-string input is its own entry rather than aliasing with everything else. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d6a4890 commit 2fcb428

2 files changed

Lines changed: 45 additions & 14 deletions

File tree

internal/fix/fix.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,26 @@ type Fixer struct {
4242
}
4343

4444
// cachedGitignore returns a GitignoreMatcher for the given directory,
45-
// creating and caching it on first use. Mirrors engine.Runner so the
46-
// fix path's lint.File values give catalog (and any other rule that
47-
// calls f.GetGitignore()) the same matcher the check path would.
45+
// creating and caching it on first use so the matcher tree is walked
46+
// once per (Fixer, dir). Mirrors engine.Runner so the fix path's
47+
// lint.File values give catalog (and any other rule that calls
48+
// f.GetGitignore()) the same matcher the check path would.
4849
//
49-
// filepath.Abs is allowed to error (it can fail when the process
50-
// can't read its current directory); on that path it returns the
51-
// input string unchanged, which is still a usable cache key — the
52-
// only consequence is that two callers passing the same relative
53-
// path from different working directories would share a cache entry,
54-
// which is acceptable for the fix pipeline that always passes either
55-
// filepath.Dir(path) or f.RootDir.
50+
// The cache is keyed on the raw dir string. lint.NewGitignoreMatcher
51+
// canonicalizes the path internally before walking, so the matcher
52+
// itself is correctly rooted regardless of whether dir was passed
53+
// absolute or relative; the cache key just has to be deterministic
54+
// across calls within a Fix run, which prepareFile guarantees by
55+
// always passing the same form (filepath.Dir(path) or f.RootDir).
5656
func (f *Fixer) cachedGitignore(dir string) *lint.GitignoreMatcher {
5757
if f.gitignoreCache == nil {
5858
f.gitignoreCache = make(map[string]*lint.GitignoreMatcher)
5959
}
60-
absDir, _ := filepath.Abs(dir)
61-
if m, ok := f.gitignoreCache[absDir]; ok {
60+
if m, ok := f.gitignoreCache[dir]; ok {
6261
return m
6362
}
64-
m := lint.NewGitignoreMatcher(absDir)
65-
f.gitignoreCache[absDir] = m
63+
m := lint.NewGitignoreMatcher(dir)
64+
f.gitignoreCache[dir] = m
6665
return m
6766
}
6867

internal/fix/fix_generated_ranges_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,38 @@ func TestFix_FilesHaveGitignoreFunc(t *testing.T) {
161161
}
162162
}
163163

164+
// TestFixer_CachedGitignore_DistinctKeys directly exercises the
165+
// cache contract: distinct inputs must yield distinct matchers, and
166+
// a repeat input must hit the cache. The previous implementation
167+
// canonicalized via filepath.Abs and silently swallowed Abs's error
168+
// return, which would collapse every relative path to the empty
169+
// string ("") cache key on Abs failure (e.g., unreadable cwd) and
170+
// share one matcher across unrelated directories.
171+
func TestFixer_CachedGitignore_DistinctKeys(t *testing.T) {
172+
fixer := &Fixer{}
173+
174+
a1 := fixer.cachedGitignore("/tmp/aaa")
175+
b := fixer.cachedGitignore("/tmp/bbb")
176+
a2 := fixer.cachedGitignore("/tmp/aaa")
177+
empty1 := fixer.cachedGitignore("")
178+
empty2 := fixer.cachedGitignore("")
179+
180+
require.NotNil(t, a1)
181+
require.NotNil(t, b)
182+
require.NotNil(t, empty1)
183+
184+
assert.NotSame(t, a1, b,
185+
"different directories must produce different matchers; the previous "+
186+
"Abs-no-fallback impl shared one cache entry across unrelated dirs "+
187+
"on the Abs-error path")
188+
assert.Same(t, a1, a2,
189+
"repeated input must hit the cache and return the same matcher pointer")
190+
assert.Same(t, empty1, empty2,
191+
"empty-string input is its own cache entry, not aliased with /tmp/aaa")
192+
assert.NotSame(t, a1, empty1,
193+
"empty-string input must not collide with /tmp/aaa")
194+
}
195+
164196
// TestFix_FilesHaveGitignoreFuncWithRootDir covers the prepareFile
165197
// branch where Fixer.RootDir is set (gitignore matcher is anchored at
166198
// the root rather than the file's directory). Without a test that

0 commit comments

Comments
 (0)