diff --git a/ROADMAP.md b/ROADMAP.md index 656070d..845822e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -26,7 +26,7 @@ - [x] Python ecosystem support (`requirements.txt`, `pyproject.toml`) - [x] Severity filtering (`--min-severity`) -- [ ] File/path exclusion patterns +- [x] File/path exclusion patterns - [ ] Performance optimization for large repositories - [ ] More comprehensive test fixtures diff --git a/pr_body.md b/pr_body.md deleted file mode 100644 index 3834c21..0000000 --- a/pr_body.md +++ /dev/null @@ -1,23 +0,0 @@ -## What changed -Refactored the default text reporter (`src/reporters/text.ts`) to group findings by file and use ANSI colors for improved readability. - -## Why -The previous output format printed each finding on a new line with the file path at the end, which became difficult to read when there were multiple findings. By grouping findings under their respective file paths (similar to ESLint and modern CLI tools), the output is much cleaner and easier to parse visually. ANSI colors help quickly distinguish between errors, warnings, and informational messages. - -## Testing -``` -has-ai-comment.ts - 1 warning AI prompt artifact: "As an AI assistant" OPK-002 - -has-generated-note.py - 1 warning AI prompt artifact: "This code was generated" OPK-002 - -✖ 2 warnings in 13.0s -``` -(All 37 unit tests pass) - -## Risks -If a terminal does not support ANSI escape codes, the output will contain raw escape characters. This is mitigated by checking `process.env.NO_COLOR` and `process.env.TERM === 'dumb'`, which disables colored output in such environments. - -## Follow-up -Publish the `v0.2.0` release to npm. diff --git a/src/scanner/index.ts b/src/scanner/index.ts index 1be2228..6a1e19c 100644 --- a/src/scanner/index.ts +++ b/src/scanner/index.ts @@ -18,8 +18,24 @@ const SKIP_FILES = new Set([ 'Thumbs.db', ]); -async function discoverFiles(rootDir: string): Promise { +interface OpkConfig { + rules?: Record; + exclude?: string[]; +} + +function loadConfig(rootDir: string): OpkConfig | null { + const configPath = path.join(rootDir, 'opk.config.json'); + try { + const content = fs.readFileSync(configPath, 'utf8'); + return JSON.parse(content); + } catch { + return null; + } +} + +async function discoverFiles(rootDir: string, excludePatterns: string[] = []): Promise { const files: string[] = []; + const excludeRegexes = excludePatterns.map((pattern) => new RegExp(pattern)); async function walk(dir: string): Promise { let entries: fs.Dirent[]; @@ -35,6 +51,13 @@ async function discoverFiles(rootDir: string): Promise { } const fullPath = path.join(dir, entry.name); + let relativePath = path.relative(rootDir, fullPath); + // Normalize slashes for consistent regex matching across platforms + relativePath = relativePath.replace(/\\/g, '/'); + + if (excludeRegexes.some((r) => r.test(relativePath))) { + continue; + } if (entry.isDirectory()) { if (SKIP_DIRS.has(entry.name)) { @@ -45,7 +68,6 @@ async function discoverFiles(rootDir: string): Promise { if (SKIP_FILES.has(entry.name)) { continue; } - const relativePath = path.relative(rootDir, fullPath); files.push(relativePath); } } @@ -55,20 +77,6 @@ async function discoverFiles(rootDir: string): Promise { return files; } -interface OpkConfig { - rules?: Record; -} - -function loadConfig(rootDir: string): OpkConfig | null { - const configPath = path.join(rootDir, 'opk.config.json'); - try { - const content = fs.readFileSync(configPath, 'utf8'); - return JSON.parse(content); - } catch { - return null; - } -} - export async function scan( rootDir: string, minSeverity: 'info' | 'warning' | 'error' = 'info' @@ -76,14 +84,15 @@ export async function scan( const startTime = Date.now(); const absoluteRoot = path.resolve(rootDir); - const files = await discoverFiles(absoluteRoot); + const config = loadConfig(absoluteRoot); + const excludePatterns = config?.exclude || []; + + const files = await discoverFiles(absoluteRoot, excludePatterns); const context: ScanContext = { rootDir: absoluteRoot, files, }; - - const config = loadConfig(absoluteRoot); const disabledRules = new Set(); if (config?.rules) { diff --git a/tests/fixtures/scanner-config-exclude/has-finding.ts b/tests/fixtures/scanner-config-exclude/has-finding.ts new file mode 100644 index 0000000..c90c889 --- /dev/null +++ b/tests/fixtures/scanner-config-exclude/has-finding.ts @@ -0,0 +1,2 @@ +// TODO: implement +function c() {} diff --git a/tests/fixtures/scanner-config-exclude/has-ignored-file.ts b/tests/fixtures/scanner-config-exclude/has-ignored-file.ts new file mode 100644 index 0000000..fe8bf6c --- /dev/null +++ b/tests/fixtures/scanner-config-exclude/has-ignored-file.ts @@ -0,0 +1,2 @@ +// TODO implement +function a() {} diff --git a/tests/fixtures/scanner-config-exclude/opk.config.json b/tests/fixtures/scanner-config-exclude/opk.config.json new file mode 100644 index 0000000..5dc7cec --- /dev/null +++ b/tests/fixtures/scanner-config-exclude/opk.config.json @@ -0,0 +1,6 @@ +{ + "exclude": [ + ".*ignored.*", + "tests/.*" + ] +} diff --git a/tests/fixtures/scanner-config-exclude/tests/should-be-ignored.ts b/tests/fixtures/scanner-config-exclude/tests/should-be-ignored.ts new file mode 100644 index 0000000..7a0c861 --- /dev/null +++ b/tests/fixtures/scanner-config-exclude/tests/should-be-ignored.ts @@ -0,0 +1,2 @@ +// TODO implement +function b() {} diff --git a/tests/scanner/scanner.test.ts b/tests/scanner/scanner.test.ts index e0f895e..6de50be 100644 --- a/tests/scanner/scanner.test.ts +++ b/tests/scanner/scanner.test.ts @@ -28,4 +28,15 @@ test('Scanner Integration', async (t) => { const errorResult = await scan(rootDir, 'error'); assert.strictEqual(errorResult.findings.length, 0); }); + + await t.test('should skip excluded paths specified in opk.config.json', async () => { + const rootDir = path.join(FIXTURES_DIR, 'scanner-config-exclude'); + const result = await scan(rootDir); + + // opk.config.json excludes ".*ignored.*" and "tests/". + // has-ignored-file.ts and tests/should-be-ignored.ts should be skipped. + // has-finding.ts should be scanned and return a finding. + assert.strictEqual(result.findings.length, 1); + assert.ok(result.findings[0].filePath.includes('has-finding.ts')); + }); });