Skip to content

Commit c3eedf2

Browse files
committed
feat: honor ignore files and test exclusions
1 parent 0d17b82 commit c3eedf2

11 files changed

Lines changed: 503 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Parse `.gitignore`, `.ignore`, and `.git/info/exclude` during working-tree scans.
6+
- Add `--exclude-tests` to skip well-known test-case paths from aggregate scan analysis.
7+
58
## 0.0.1 - 2026-06-30
69

710
- Initial public code-signal scanner.

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ scanner scan .
5252
# Emit aggregate JSON
5353
scanner scan . --json
5454

55+
# Exclude well-known test files and test directories
56+
scanner scan . --exclude-tests
57+
5558
# Detect languages and module roots
5659
scanner detect .
5760

@@ -92,6 +95,14 @@ code-signal detects and analyzes these languages with built-in lexical/parser ch
9295

9396
Detection uses extensions, shebangs, and common manifests such as `go.mod`, `package.json`, `pyproject.toml`, `Cargo.toml`, `pom.xml`, Gradle files, `.csproj`, `.sln`, `composer.json`, `Gemfile`, `Dockerfile`, `Containerfile`, and `*.dockerfile`.
9497

98+
## Ignore files, test exclusion, and skip policy
99+
100+
Working-tree scans parse `.gitignore`, `.ignore`, and `.git/info/exclude` using gitignore-style rules before reading candidate files.
101+
102+
Pass `--exclude-tests` to `scan` or `diff` to skip well-known test-case paths from aggregate analysis. The built-in defaults cover common Go (`*_test.go`, `testdata`), Java (`src/test/**`, `*Test.java`, `*Tests.java`, `*IT.java`), Python (`tests/**`, `test_*.py`, `*_test.py`, `conftest.py`), TypeScript/JavaScript (`__tests__/**`, `*.test.*`, `*.spec.*`, `cypress/**`, `playwright/**`, `e2e/**`), and Rust (`tests/**`, `benches/**`, `*_test.rs`) conventions.
103+
104+
Built-in skips still apply for common dependency, generated, build, coverage, and minified paths such as `node_modules`, `vendor`, `target`, `dist`, `build`, `.next`, `coverage`, `third_party`, `generated`, `gen`, `*.min.js`, `*.min.css`, `*.pb.go`, and `*_generated.go`.
105+
95106
## Configuration
96107

97108
Configuration is optional. If present, `scanner.json` is loaded from the scanned repository root or from `--config`.
@@ -102,7 +113,8 @@ Configuration is optional. If present, `scanner.json` is loaded from the scanned
102113
"default_timeout_seconds": 30,
103114
"max_file_bytes": 1048576,
104115
"follow_symlinks": false,
105-
"workers": 0
116+
"workers": 0,
117+
"exclude_tests": false
106118
},
107119
"score": {
108120
"fail_under": 75,

internal/cli/cli.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323

2424
flagBase = "--base"
2525
flagConfig = "--config"
26+
flagExcludeTests = "--exclude-tests"
2627
flagFailOnRegression = "--fail-on-regression"
2728
flagFailUnder = "--fail-under"
2829
flagHead = "--head"
@@ -89,17 +90,19 @@ type diffArgs struct {
8990
configPath string
9091
json bool
9192
failOnRegression bool
93+
excludeTests bool
9294
timeout time.Duration
9395
timeoutSet bool
9496
}
9597

9698
type scanArgs struct {
97-
path string
98-
configPath string
99-
json bool
100-
failUnder *int
101-
timeout time.Duration
102-
timeoutSet bool
99+
path string
100+
configPath string
101+
json bool
102+
failUnder *int
103+
excludeTests bool
104+
timeout time.Duration
105+
timeoutSet bool
103106
}
104107

105108
type detectArgs struct {
@@ -129,6 +132,9 @@ func runDiff(ctx context.Context, args []string, stdout io.Writer) (int, error)
129132
if err != nil {
130133
return 2, err
131134
}
135+
if parsed.excludeTests {
136+
cfg.Scan.ExcludeTests = true
137+
}
132138
diff, err := scan.Diff(ctx, parsed.repo, parsed.base, parsed.head, parsed.scopePath, scan.DiffOptions{Config: cfg})
133139
if err != nil {
134140
return 1, err
@@ -160,6 +166,9 @@ func runScan(ctx context.Context, args []string, stdout io.Writer) (int, error)
160166
if err != nil {
161167
return 2, err
162168
}
169+
if parsed.excludeTests {
170+
cfg.Scan.ExcludeTests = true
171+
}
163172
scanReport, err := scan.ScanPath(ctx, parsed.path, scan.ScanOptions{Config: cfg})
164173
if err != nil {
165174
return 1, err
@@ -260,6 +269,9 @@ func parseDiffFlag(args []string, index int, parsed *diffArgs) (int, bool, error
260269
case flagConfig:
261270
next, err := assignFlagValue(args, index, flagConfig, &parsed.configPath)
262271
return next, true, err
272+
case flagExcludeTests:
273+
parsed.excludeTests = true
274+
return index, true, nil
263275
case flagTimeout:
264276
next, err := assignTimeoutValue(args, index, &parsed.timeout, &parsed.timeoutSet)
265277
return next, true, err
@@ -303,6 +315,9 @@ func parseScanFlag(args []string, index int, parsed *scanArgs) (int, bool, error
303315
case flagConfig:
304316
next, err := assignFlagValue(args, index, flagConfig, &parsed.configPath)
305317
return next, true, err
318+
case flagExcludeTests:
319+
parsed.excludeTests = true
320+
return index, true, nil
306321
case flagTimeout:
307322
next, err := assignTimeoutValue(args, index, &parsed.timeout, &parsed.timeoutSet)
308323
return next, true, err

internal/cli/cli_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cli
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"errors"
78
"os"
89
"path/filepath"
@@ -72,6 +73,44 @@ func TestScanCommandKeepsZeroExitForLowScore(t *testing.T) {
7273
t.Fatalf("stdout = %q, want score output", stdout.String())
7374
}
7475
}
76+
func TestScanCommandExcludeTestsUsesDefaultPatterns(t *testing.T) {
77+
root := t.TempDir()
78+
writeCLITestFile(t, root, "cmd/main.go", "package main\n")
79+
longLine := "// " + strings.Repeat("x", 140) + "\n"
80+
for _, name := range []string{
81+
"cmd/main_test.go",
82+
"testdata/golden.go",
83+
"src/test/java/AppTest.java",
84+
"tests/test_app.py",
85+
"web/src/app.spec.ts",
86+
"web/src/__tests__/app.test.js",
87+
"crates/core/tests/integration.rs",
88+
} {
89+
writeCLITestFile(t, root, name, longLine)
90+
}
91+
92+
var stdout, stderr bytes.Buffer
93+
code := Run(context.Background(), []string{"scan", "--exclude-tests", "--json", root}, &stdout, &stderr)
94+
if code != 0 {
95+
t.Fatalf("code = %d stdout = %q stderr = %q, want 0", code, stdout.String(), stderr.String())
96+
}
97+
var decoded struct {
98+
Totals struct {
99+
FilesScanned int `json:"files_scanned"`
100+
FilesSkipped int `json:"files_skipped"`
101+
Warnings int `json:"warnings"`
102+
} `json:"totals"`
103+
}
104+
if err := json.Unmarshal(stdout.Bytes(), &decoded); err != nil {
105+
t.Fatalf("unmarshal scan JSON: %v\n%s", err, stdout.String())
106+
}
107+
if decoded.Totals.FilesScanned != 1 || decoded.Totals.FilesSkipped != 7 {
108+
t.Fatalf("totals = %#v, want one source file scanned and seven test files skipped", decoded.Totals)
109+
}
110+
if decoded.Totals.Warnings != 0 {
111+
t.Fatalf("warnings = %d, want ignored test long lines excluded", decoded.Totals.Warnings)
112+
}
113+
}
75114

76115
func TestUnknownCommandReturnsCLIError(t *testing.T) {
77116
var stdout, stderr bytes.Buffer
@@ -134,6 +173,17 @@ func TestRunReturnsOneForRuntimeInputErrors(t *testing.T) {
134173
}
135174
}
136175

176+
func writeCLITestFile(t *testing.T, root, name, content string) {
177+
t.Helper()
178+
path := filepath.Join(root, filepath.FromSlash(name))
179+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
180+
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
181+
}
182+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
183+
t.Fatalf("write %s: %v", name, err)
184+
}
185+
}
186+
137187
var errFailingWrite = errors.New("write failed")
138188

139189
type failingWriter struct{}

internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type ScanConfig struct {
2222
MaxFileBytes int64 `json:"max_file_bytes"`
2323
FollowSymlinks bool `json:"follow_symlinks"`
2424
Workers int `json:"workers"`
25+
ExcludeTests bool `json:"exclude_tests"`
2526
}
2627

2728
// ScoreConfig controls aggregate score deductions.
@@ -50,6 +51,7 @@ type scanFile struct {
5051
MaxFileBytes *int64 `json:"max_file_bytes"`
5152
FollowSymlinks *bool `json:"follow_symlinks"`
5253
Workers *int `json:"workers"`
54+
ExcludeTests *bool `json:"exclude_tests"`
5355
}
5456

5557
type scoreFile struct {
@@ -151,6 +153,9 @@ func mergeScanConfig(cfg *ScanConfig, file *scanFile) {
151153
if file.Workers != nil {
152154
cfg.Workers = *file.Workers
153155
}
156+
if file.ExcludeTests != nil {
157+
cfg.ExcludeTests = *file.ExcludeTests
158+
}
154159
}
155160

156161
func mergeScoreConfig(cfg *ScoreConfig, file *scoreFile) {

internal/config/config_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ func TestLoadMergesScannerJSONScanAndScoreOverrides(t *testing.T) {
4141
"default_timeout_seconds": 7,
4242
"max_file_bytes": 2048,
4343
"follow_symlinks": true,
44-
"workers": 3
44+
"workers": 3,
45+
"exclude_tests": true
4546
},
4647
"score": {
4748
"fail_under": 91,
@@ -70,6 +71,9 @@ func TestLoadMergesScannerJSONScanAndScoreOverrides(t *testing.T) {
7071
if cfg.Scan.Workers != 3 {
7172
t.Fatalf("Workers = %d, want 3", cfg.Scan.Workers)
7273
}
74+
if !cfg.Scan.ExcludeTests {
75+
t.Fatalf("ExcludeTests = false, want true")
76+
}
7377
if cfg.Score.FailUnder != 91 || cfg.Score.Error != 15 || cfg.Score.Warning != 5 || cfg.Score.Info != 2 {
7478
t.Fatalf("Score = %#v, want overridden values", cfg.Score)
7579
}

0 commit comments

Comments
 (0)