diff --git a/scanner/deps.go b/scanner/deps.go index 15e5956..a63a6e4 100644 --- a/scanner/deps.go +++ b/scanner/deps.go @@ -36,7 +36,9 @@ func ReadExternalDeps(ctx context.Context, root string, manifestByteBudget int64 return nil } if info.IsDir() { - if IgnoredDirs[info.Name()] { + // The walk root is what the user asked for, so it is never one of + // the hardcoded ignored directories. + if IgnoredDirs[info.Name()] && path != root { return filepath.SkipDir } return nil diff --git a/scanner/ignoretestdata_test.go b/scanner/ignoretestdata_test.go new file mode 100644 index 0000000..cdc475f --- /dev/null +++ b/scanner/ignoretestdata_test.go @@ -0,0 +1,113 @@ +package scanner + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +// Fixture repositories under testdata are not the project's source. Scanning +// them inflates file counts and lets a fixture's language change the project's +// reported coverage — a Swift fixture made the whole project report partial. +// Go's own toolchain ignores the directory for the same reason. +func TestScanSkipsTestdataDirectories(t *testing.T) { + root := t.TempDir() + write := func(rel, body string) { + t.Helper() + path := filepath.Join(root, filepath.FromSlash(rel)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + write("main.go", "package main\n\nfunc main() {}\n") + write("testdata/fixture/main.go", "package main\n\nfunc main() {}\n") + write("internal/testdata/nested/app.go", "package nested\n") + + files, err := ScanFiles(context.Background(), root, NewGitIgnoreCache(root), nil, nil) + if err != nil { + t.Fatalf("scan: %v", err) + } + for _, file := range files { + if file.Path != "main.go" { + t.Errorf("scan returned %q, want only main.go — testdata must be skipped at any depth", file.Path) + } + } + if len(files) != 1 { + t.Fatalf("scan returned %d files, want 1", len(files)) + } +} + +// The hardcoded ignore list must never apply to the directory the user asked +// for. `codemap testdata/`, or `cd vendor && codemap .`, previously matched the +// root's own base name and returned Files: 0 with no error and no explanation. +func TestIgnoredDirNameScansWhenItIsTheRoot(t *testing.T) { + for _, name := range []string{"testdata", "vendor", "node_modules"} { + t.Run(name, func(t *testing.T) { + root := filepath.Join(t.TempDir(), name) + if err := os.MkdirAll(root, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "app.go"), []byte("package app\n"), 0o644); err != nil { + t.Fatal(err) + } + + files, err := ScanFiles(context.Background(), root, NewGitIgnoreCache(root), nil, nil) + if err != nil { + t.Fatalf("scan: %v", err) + } + if len(files) != 1 || files[0].Path != "app.go" { + t.Fatalf("scan of %s/ as the root returned %v, want [app.go]", name, files) + } + }) + } +} + +// Nested ignored directories are still skipped when the root is itself named +// like one: the exemption covers the root only, not every directory sharing +// its name. +func TestIgnoredDirRootStillSkipsNestedIgnoredDirs(t *testing.T) { + root := filepath.Join(t.TempDir(), "testdata") + nested := filepath.Join(root, "fixture", "testdata") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "app.go"), []byte("package app\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(nested, "deep.go"), []byte("package deep\n"), 0o644); err != nil { + t.Fatal(err) + } + + files, err := ScanFiles(context.Background(), root, NewGitIgnoreCache(root), nil, nil) + if err != nil { + t.Fatalf("scan: %v", err) + } + if len(files) != 1 || files[0].Path != "app.go" { + t.Fatalf("scan returned %v, want only [app.go] — nested testdata must still be skipped", files) + } +} + +// A fixture is still scannable when it is itself the root, which is how the +// fixture tests in this package use them. +func TestTestdataFixtureScansWhenItIsTheRoot(t *testing.T) { + root := t.TempDir() + fixture := filepath.Join(root, "testdata", "fixture") + if err := os.MkdirAll(fixture, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fixture, "app.go"), []byte("package app\n"), 0o644); err != nil { + t.Fatal(err) + } + + files, err := ScanFiles(context.Background(), fixture, NewGitIgnoreCache(fixture), nil, nil) + if err != nil { + t.Fatalf("scan: %v", err) + } + if len(files) != 1 || files[0].Path != "app.go" { + t.Fatalf("scan of the fixture root returned %v, want [app.go]", files) + } +} diff --git a/scanner/walker.go b/scanner/walker.go index 5c4d7a8..0b17c0a 100644 --- a/scanner/walker.go +++ b/scanner/walker.go @@ -113,9 +113,13 @@ func (c *GitIgnoreCache) ShouldIgnore(absPath string) bool { // IgnoredDirs are directories to skip during scanning var IgnoredDirs = map[string]bool{ - ".git": true, - "node_modules": true, - "vendor": true, + ".git": true, + "node_modules": true, + "vendor": true, + // Go's own toolchain ignores testdata, and fixture repositories inside it + // are not the project's source: scanning them inflates file counts and + // lets a fixture's language change the project's reported coverage. + "testdata": true, "Pods": true, "build": true, "DerivedData": true, @@ -247,8 +251,11 @@ func ScanFiles(ctx context.Context, root string, cache *GitIgnoreCache, only []s name := info.Name() - // Fast path: skip hardcoded ignored dirs/files - if IgnoredDirs[name] { + // Fast path: skip hardcoded ignored dirs/files. Never applied to the + // walk root itself: a user who runs `codemap testdata/` or scans from + // inside vendor/ asked for exactly that tree, and skipping it here + // returns Files: 0 with no explanation. + if IgnoredDirs[name] && path != absRoot { if info.IsDir() { return filepath.SkipDir }