mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 00:01:39 +08:00
fix(affected): a --filter glob matches whole names, from any folder boundary (#3029)
`codegraph affected --filter` turned its glob into an unanchored regex by hand (`**` to `.+`, `*` to `[^/]*`, no `^` or `$`), so it matched anywhere inside a path: `tests/**/*.py` missed `tests/x.py` (`.+` needs a folder), a changed `tests/x.py.bak` counted as a test under `tests/*.py`, the changed test itself was missed under `tests/**/*.py`, `e2e/*.spec.ts` took an `xe2e/` folder, and `/tests/*.py` matched only `other/tests/...`. The filter is now a picomatch glob (an existing dependency, loaded only by this command) with `dot: true`, matched against the whole project-relative path. A pattern with no slash matches the file name at any depth. One with a slash matches from any folder boundary, an implicit leading `**/`, which keeps the help text's own `e2e/*.spec.ts` matching `apps/web/e2e/...` as the filter always did; a leading `/` or `./` anchors it at the project root, and a trailing `/` takes everything under that folder. A pattern picomatch rejects is reported as an invalid --filter instead of failing later. Validation: new CLI-level cli-affected-filter-globs tests (6): 4 fail on main's source and pass with the fix; the other 2 guard behavior kept on purpose (file-name patterns at any depth, folders outside the test conventions, dotfiles, a trailing-slash folder). The existing affected CLI suites pass. The triage fixture shows the same before and after. Reimplements PR #1273 by @Jseca666. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3d19da2f59
commit
60b0d7a92d
+45
-14
@@ -2617,6 +2617,37 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
// The matcher for `affected --filter <glob>`, tested against the
|
||||
// project-relative, forward-slash paths the index stores (#1273).
|
||||
//
|
||||
// - No slash (`*_test.go`, `test_*.py`): the file name, at any depth.
|
||||
// - A slash (`e2e/*.spec.ts`, `tests/**/*.py`): the path from any folder
|
||||
// boundary, so `e2e/*.spec.ts` also matches `apps/web/e2e/login.spec.ts`
|
||||
// (an implicit leading `**/`), as the filter always has.
|
||||
// - A leading `/` or `./`: from the project root only.
|
||||
// - A trailing `/` (`tests/`): everything under that folder.
|
||||
//
|
||||
// The whole name must match: `tests/*.py` no longer takes `tests/x.py.bak`,
|
||||
// nor `e2e/*.spec.ts` an `xe2e/` folder, and `tests/**/*.py` now takes
|
||||
// `tests/x.py` (`**` matches zero folders). Dotfiles count. picomatch is
|
||||
// loaded here, not at startup, since only this command needs it.
|
||||
async function affectedFilterMatcher(pattern: string): Promise<(filePath: string) => boolean> {
|
||||
const { default: picomatch } = await import('picomatch');
|
||||
// `windows: false`: the stored paths always use `/`, so the glob reads the
|
||||
// same on every platform (picomatch otherwise follows the host OS).
|
||||
const options = { dot: true, windows: false };
|
||||
let glob = pattern.trim();
|
||||
if (glob.endsWith('/')) glob += '**';
|
||||
if (!glob.includes('/')) {
|
||||
const matchName = picomatch(glob, options);
|
||||
return (filePath) => matchName(path.posix.basename(filePath));
|
||||
}
|
||||
const anchored = /^\.?\//.test(glob);
|
||||
glob = glob.replace(/^\.?\/+/, '');
|
||||
if (!anchored && !glob.startsWith('**/')) glob = `**/${glob}`;
|
||||
return picomatch(glob, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* codegraph affected [files...]
|
||||
*
|
||||
@@ -2633,7 +2664,7 @@ program
|
||||
.option('-p, --path <path>', 'Project path')
|
||||
.option('--stdin', 'Read file list from stdin (one per line)')
|
||||
.option('-d, --depth <number>', 'Max dependency traversal depth', '5')
|
||||
.option('-f, --filter <glob>', 'Custom glob filter for test files (e.g. "e2e/*.spec.ts")')
|
||||
.option('-f, --filter <glob>', 'Glob for the files that count as tests, matched against the whole name: "*_test.go" matches the file name at any depth, "e2e/*.spec.ts" that folder at any depth, "/tests/**/*.py" from the project root only')
|
||||
.option('-j, --json', 'Output as JSON')
|
||||
.option('-q, --quiet', 'Only output file paths, no decoration')
|
||||
.action(async (fileArgs: string[], options: { path?: string; stdin?: boolean; depth?: string; filter?: string; json?: boolean; quiet?: boolean }) => {
|
||||
@@ -2667,28 +2698,28 @@ program
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Custom filter pattern, compiled before the index is opened so a bad
|
||||
// pattern fails fast.
|
||||
let customFilter: ((filePath: string) => boolean) | null = null;
|
||||
if (options.filter) {
|
||||
try {
|
||||
customFilter = await affectedFilterMatcher(options.filter);
|
||||
} catch (err) {
|
||||
error(`Invalid --filter pattern "${options.filter}": ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const cg = await openIndexToRead(projectPath);
|
||||
const maxDepth = parseInt(options.depth || '5', 10);
|
||||
|
||||
// Custom filter pattern
|
||||
let customFilter: RegExp | null = null;
|
||||
if (options.filter) {
|
||||
// Convert glob to regex: ** → .+, * → [^/]*, . → \.
|
||||
const regex = options.filter
|
||||
.replace(/[+[\]{}()^$|\\]/g, '\\$&')
|
||||
.replace(/\./g, '\\.')
|
||||
.replace(/\*\*/g, '.+')
|
||||
.replace(/\*/g, '[^/]*');
|
||||
customFilter = new RegExp(regex);
|
||||
}
|
||||
|
||||
// One notion of "a test" for the whole tool (#1507): the CLI used to keep
|
||||
// its own six regexes here, which knew `.test.` and `/tests/` but not Go's
|
||||
// `_test.go`, Python's `test_x.py` or the JVM's `FooTest.kt` — so
|
||||
// `affected` reported "no tests" for whole ecosystems while `search` and
|
||||
// the MCP tools counted those very files as tests.
|
||||
function isTestFile(filePath: string): boolean {
|
||||
if (customFilter) return customFilter.test(filePath);
|
||||
if (customFilter) return customFilter(filePath);
|
||||
return isTestPath(filePath);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user