claude-crap 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +74 -7
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +1 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/dart-analyzer.d.ts +41 -0
- package/dist/adapters/dart-analyzer.d.ts.map +1 -0
- package/dist/adapters/dart-analyzer.js +120 -0
- package/dist/adapters/dart-analyzer.js.map +1 -0
- package/dist/adapters/dotnet-format.d.ts +35 -0
- package/dist/adapters/dotnet-format.d.ts.map +1 -0
- package/dist/adapters/dotnet-format.js +96 -0
- package/dist/adapters/dotnet-format.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/crap-config.d.ts +4 -0
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +51 -28
- package/dist/crap-config.js.map +1 -1
- package/dist/dashboard/file-detail.d.ts.map +1 -1
- package/dist/dashboard/file-detail.js.map +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +7 -12
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +89 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/workspace-walker.d.ts +4 -1
- package/dist/metrics/workspace-walker.d.ts.map +1 -1
- package/dist/metrics/workspace-walker.js +12 -28
- package/dist/metrics/workspace-walker.js.map +1 -1
- package/dist/monorepo/project-map.d.ts +112 -0
- package/dist/monorepo/project-map.d.ts.map +1 -0
- package/dist/monorepo/project-map.js +384 -0
- package/dist/monorepo/project-map.js.map +1 -0
- package/dist/scanner/auto-scan.d.ts +1 -0
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -5
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/bootstrap.d.ts +1 -1
- package/dist/scanner/bootstrap.d.ts.map +1 -1
- package/dist/scanner/bootstrap.js +15 -1
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +2 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -1
- package/dist/scanner/complexity-scanner.js +11 -26
- package/dist/scanner/complexity-scanner.js.map +1 -1
- package/dist/scanner/detector.d.ts +24 -4
- package/dist/scanner/detector.d.ts.map +1 -1
- package/dist/scanner/detector.js +110 -10
- package/dist/scanner/detector.js.map +1 -1
- package/dist/scanner/runner.d.ts +4 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +25 -3
- package/dist/scanner/runner.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +16 -1
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +16 -1
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/dist/shared/exclusions.d.ts +53 -0
- package/dist/shared/exclusions.d.ts.map +1 -0
- package/dist/shared/exclusions.js +126 -0
- package/dist/shared/exclusions.js.map +1 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +37 -0
- package/plugin/bundle/mcp-server.mjs +762 -144
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +15 -2
- package/plugin/package.json +2 -1
- package/scripts/bundle-plugin.mjs +2 -1
- package/src/adapters/common.ts +1 -1
- package/src/adapters/dart-analyzer.ts +161 -0
- package/src/adapters/dotnet-format.ts +125 -0
- package/src/adapters/index.ts +8 -0
- package/src/crap-config.ts +78 -18
- package/src/dashboard/file-detail.ts +0 -2
- package/src/dashboard/server.ts +9 -10
- package/src/index.ts +103 -5
- package/src/metrics/workspace-walker.ts +15 -27
- package/src/monorepo/project-map.ts +476 -0
- package/src/scanner/auto-scan.ts +17 -6
- package/src/scanner/bootstrap.ts +18 -1
- package/src/scanner/complexity-scanner.ts +15 -26
- package/src/scanner/detector.ts +119 -10
- package/src/scanner/runner.ts +25 -2
- package/src/schemas/tool-schemas.ts +17 -1
- package/src/shared/exclusions.ts +156 -0
- package/src/tests/adapters/dispatch.test.ts +2 -2
- package/src/tests/auto-scan.test.ts +2 -2
- package/src/tests/boot-monorepo.test.ts +804 -0
- package/src/tests/boot-scanner-detection.test.ts +692 -0
- package/src/tests/boot-single-project.test.ts +780 -0
- package/src/tests/exclusions.test.ts +117 -0
- package/src/tests/integration/mcp-server.integration.test.ts +2 -1
- package/src/tests/project-map.test.ts +302 -0
- package/src/tests/scanner-detector.test.ts +31 -11
package/src/scanner/detector.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* @module scanner/detector
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
20
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
21
|
+
import { join, resolve } from "node:path";
|
|
22
22
|
import { execFile } from "node:child_process";
|
|
23
23
|
import type { KnownScanner } from "../adapters/common.js";
|
|
24
24
|
|
|
@@ -36,6 +36,8 @@ export interface ScannerDetection {
|
|
|
36
36
|
reason: string;
|
|
37
37
|
/** Path to the config file that triggered detection, if any. */
|
|
38
38
|
configPath?: string;
|
|
39
|
+
/** Working directory to run the scanner from (defaults to workspace root). */
|
|
40
|
+
workingDir?: string;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
// ── Detection signals ──────────────────────────────────────────────
|
|
@@ -98,6 +100,19 @@ const SCANNER_SIGNALS: Record<KnownScanner, ScannerSignals> = {
|
|
|
98
100
|
packageJsonKeys: ["@stryker-mutator/core"],
|
|
99
101
|
binaryNames: ["stryker"],
|
|
100
102
|
},
|
|
103
|
+
dart_analyze: {
|
|
104
|
+
configFiles: [
|
|
105
|
+
"analysis_options.yaml",
|
|
106
|
+
"pubspec.yaml",
|
|
107
|
+
],
|
|
108
|
+
packageJsonKeys: [],
|
|
109
|
+
binaryNames: ["dart"],
|
|
110
|
+
},
|
|
111
|
+
dotnet_format: {
|
|
112
|
+
configFiles: [],
|
|
113
|
+
packageJsonKeys: [],
|
|
114
|
+
binaryNames: ["dotnet"],
|
|
115
|
+
},
|
|
101
116
|
};
|
|
102
117
|
|
|
103
118
|
// ── Probes ──────────────────────────────────────────────────────────
|
|
@@ -163,9 +178,9 @@ function probeBinary(binaryName: string): Promise<boolean> {
|
|
|
163
178
|
// ── Public API ──────────────────────────────────────────────────────
|
|
164
179
|
|
|
165
180
|
/**
|
|
166
|
-
* Detect which
|
|
167
|
-
*
|
|
168
|
-
*
|
|
181
|
+
* Detect which supported scanners are available in the given workspace.
|
|
182
|
+
* Probes config files, package.json, and binary availability in order,
|
|
183
|
+
* short-circuiting on first match.
|
|
169
184
|
*
|
|
170
185
|
* @param workspaceRoot Absolute path to the project root.
|
|
171
186
|
* @returns One {@link ScannerDetection} per known scanner.
|
|
@@ -173,7 +188,7 @@ function probeBinary(binaryName: string): Promise<boolean> {
|
|
|
173
188
|
export async function detectScanners(
|
|
174
189
|
workspaceRoot: string,
|
|
175
190
|
): Promise<ScannerDetection[]> {
|
|
176
|
-
const scanners: KnownScanner[] = ["eslint", "semgrep", "bandit", "stryker"];
|
|
191
|
+
const scanners: KnownScanner[] = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
|
|
177
192
|
|
|
178
193
|
const results = await Promise.all(
|
|
179
194
|
scanners.map(async (scanner): Promise<ScannerDetection> => {
|
|
@@ -188,12 +203,18 @@ export async function detectScanners(
|
|
|
188
203
|
};
|
|
189
204
|
}
|
|
190
205
|
|
|
191
|
-
// 2. Package.json probe
|
|
206
|
+
// 2. Package.json probe — declared in deps/devDeps, but is it
|
|
207
|
+
// actually installed? Check node_modules/.bin/ for the binary.
|
|
192
208
|
if (probePackageJson(workspaceRoot, scanner)) {
|
|
209
|
+
const binName = SCANNER_SIGNALS[scanner].binaryNames[0];
|
|
210
|
+
const binPath = binName ? join(workspaceRoot, "node_modules", ".bin", binName) : null;
|
|
211
|
+
const installed = binPath !== null && existsSync(binPath);
|
|
193
212
|
return {
|
|
194
213
|
scanner,
|
|
195
|
-
available:
|
|
196
|
-
reason:
|
|
214
|
+
available: installed,
|
|
215
|
+
reason: installed
|
|
216
|
+
? "found in package.json and installed"
|
|
217
|
+
: `found in package.json but not installed (run \`npm install\`)`,
|
|
197
218
|
};
|
|
198
219
|
}
|
|
199
220
|
|
|
@@ -220,5 +241,93 @@ export async function detectScanners(
|
|
|
220
241
|
return results;
|
|
221
242
|
}
|
|
222
243
|
|
|
244
|
+
// ── Monorepo subdirectory probing ────────────────────────────────
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Common monorepo directory names that may contain workspace
|
|
248
|
+
* subdirectories. Checked one level deep only.
|
|
249
|
+
*/
|
|
250
|
+
const MONOREPO_DIRS = ["apps", "packages", "libs", "modules", "services"];
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Detect scanners in monorepo subdirectories. Probes first-level
|
|
254
|
+
* children of common monorepo directories (apps/, packages/, etc.)
|
|
255
|
+
* and npm workspaces for scanner config files. Returns detections
|
|
256
|
+
* with a `workingDir` pointing to the subdirectory.
|
|
257
|
+
*
|
|
258
|
+
* This catches e.g. `apps/mobile/pubspec.yaml` in a polyglot monorepo
|
|
259
|
+
* where the root-level detector only finds ESLint.
|
|
260
|
+
*
|
|
261
|
+
* @param workspaceRoot Absolute path to the project root.
|
|
262
|
+
* @returns Additional detections from subdirectories (may be empty).
|
|
263
|
+
*/
|
|
264
|
+
export async function detectMonorepoScanners(
|
|
265
|
+
workspaceRoot: string,
|
|
266
|
+
): Promise<ScannerDetection[]> {
|
|
267
|
+
const subdirs = new Set<string>();
|
|
268
|
+
|
|
269
|
+
// 1. Read npm workspaces from package.json
|
|
270
|
+
try {
|
|
271
|
+
const pkgPath = join(workspaceRoot, "package.json");
|
|
272
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
273
|
+
const pkg = JSON.parse(raw) as Record<string, unknown>;
|
|
274
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
275
|
+
for (const ws of pkg.workspaces) {
|
|
276
|
+
if (typeof ws === "string" && !ws.includes("*")) {
|
|
277
|
+
const full = resolve(workspaceRoot, ws);
|
|
278
|
+
if (existsSync(full)) subdirs.add(full);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// No package.json or not parseable — continue
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 2. Scan common monorepo directories one level deep
|
|
287
|
+
for (const dir of MONOREPO_DIRS) {
|
|
288
|
+
const full = join(workspaceRoot, dir);
|
|
289
|
+
try {
|
|
290
|
+
const entries = readdirSync(full, { withFileTypes: true });
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
293
|
+
subdirs.add(join(full, entry.name));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
// Directory doesn't exist — skip
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (subdirs.size === 0) return [];
|
|
302
|
+
|
|
303
|
+
// 3. Probe each subdirectory for scanner config files
|
|
304
|
+
const detections: ScannerDetection[] = [];
|
|
305
|
+
const scanners: KnownScanner[] = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
|
|
306
|
+
|
|
307
|
+
for (const subdir of subdirs) {
|
|
308
|
+
for (const scanner of scanners) {
|
|
309
|
+
const configProbe = probeConfigFiles(subdir, scanner);
|
|
310
|
+
if (!configProbe.found) continue;
|
|
311
|
+
|
|
312
|
+
// For dart_analyze, also verify the binary is on PATH
|
|
313
|
+
if (scanner === "dart_analyze") {
|
|
314
|
+
const hasBinary = await probeBinary("dart");
|
|
315
|
+
if (!hasBinary) continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const relDir = subdir.replace(workspaceRoot + "/", "");
|
|
319
|
+
detections.push({
|
|
320
|
+
scanner,
|
|
321
|
+
available: true,
|
|
322
|
+
reason: `config file found in ${relDir}/`,
|
|
323
|
+
...(configProbe.path ? { configPath: configProbe.path } : {}),
|
|
324
|
+
workingDir: subdir,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return detections;
|
|
330
|
+
}
|
|
331
|
+
|
|
223
332
|
// Exported for testing
|
|
224
|
-
export { SCANNER_SIGNALS };
|
|
333
|
+
export { SCANNER_SIGNALS, MONOREPO_DIRS };
|
package/src/scanner/runner.ts
CHANGED
|
@@ -91,6 +91,26 @@ function getScannerCommand(
|
|
|
91
91
|
nonZeroIsNormal: false,
|
|
92
92
|
outputFile: join(workspaceRoot, "reports", "mutation", "mutation.json"),
|
|
93
93
|
};
|
|
94
|
+
case "dart_analyze":
|
|
95
|
+
return {
|
|
96
|
+
command: "dart",
|
|
97
|
+
args: ["analyze", "--format=json", "."],
|
|
98
|
+
timeoutMs: 120_000,
|
|
99
|
+
nonZeroIsNormal: true, // exits 3 when findings exist
|
|
100
|
+
};
|
|
101
|
+
case "dotnet_format":
|
|
102
|
+
return {
|
|
103
|
+
command: "dotnet",
|
|
104
|
+
args: [
|
|
105
|
+
"format",
|
|
106
|
+
"--verify-no-changes",
|
|
107
|
+
"--report",
|
|
108
|
+
join(workspaceRoot, ".claude-crap", "dotnet-report.json"),
|
|
109
|
+
],
|
|
110
|
+
timeoutMs: 120_000,
|
|
111
|
+
nonZeroIsNormal: true,
|
|
112
|
+
outputFile: join(workspaceRoot, ".claude-crap", "dotnet-report.json"),
|
|
113
|
+
};
|
|
94
114
|
}
|
|
95
115
|
}
|
|
96
116
|
|
|
@@ -101,21 +121,24 @@ function getScannerCommand(
|
|
|
101
121
|
*
|
|
102
122
|
* @param scanner Which scanner to run.
|
|
103
123
|
* @param workspaceRoot Absolute path to the project root (used as cwd).
|
|
124
|
+
* @param options Optional overrides.
|
|
104
125
|
* @returns A {@link ScannerRunResult} with stdout or file output.
|
|
105
126
|
*/
|
|
106
127
|
export function runScanner(
|
|
107
128
|
scanner: KnownScanner,
|
|
108
129
|
workspaceRoot: string,
|
|
130
|
+
options?: { workingDir?: string },
|
|
109
131
|
): Promise<ScannerRunResult> {
|
|
110
132
|
const start = Date.now();
|
|
111
|
-
const
|
|
133
|
+
const cwd = options?.workingDir ?? workspaceRoot;
|
|
134
|
+
const cmd = getScannerCommand(scanner, cwd);
|
|
112
135
|
|
|
113
136
|
return new Promise((resolve) => {
|
|
114
137
|
execFile(
|
|
115
138
|
cmd.command,
|
|
116
139
|
cmd.args,
|
|
117
140
|
{
|
|
118
|
-
cwd
|
|
141
|
+
cwd,
|
|
119
142
|
timeout: cmd.timeoutMs,
|
|
120
143
|
maxBuffer: 50 * 1024 * 1024, // 50 MB — large codebases produce verbose output
|
|
121
144
|
env: { ...process.env, FORCE_COLOR: "0" }, // suppress ANSI in output
|
|
@@ -134,11 +134,27 @@ export const scoreProjectSchema = {
|
|
|
134
134
|
description:
|
|
135
135
|
"Output format. `markdown` returns only the chat summary, `json` returns only the structured snapshot, `both` (default) returns both as separate content blocks.",
|
|
136
136
|
},
|
|
137
|
+
scope: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Optional project name from the project map. When provided, the score is computed only for files within that project's subtree. Omit to score the entire workspace.",
|
|
140
|
+
},
|
|
137
141
|
},
|
|
138
142
|
required: [],
|
|
139
143
|
additionalProperties: false,
|
|
140
144
|
} as const;
|
|
141
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Schema for the `list_projects` tool. Returns the discovered sub-projects
|
|
148
|
+
* in the workspace, or an empty list for single-project workspaces.
|
|
149
|
+
*/
|
|
150
|
+
export const listProjectsSchema = {
|
|
151
|
+
type: "object",
|
|
152
|
+
description: "List all discovered sub-projects in the workspace. In a monorepo, returns each sub-project with its type, path, and recommended scanner. In a single-project workspace, returns an empty list.",
|
|
153
|
+
properties: {},
|
|
154
|
+
required: [],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
} as const;
|
|
157
|
+
|
|
142
158
|
/**
|
|
143
159
|
* Schema for the `require_test_harness` tool. Checks whether a production
|
|
144
160
|
* source file has an accompanying test file in any of the conventional
|
|
@@ -181,7 +197,7 @@ export const ingestScannerOutputSchema = {
|
|
|
181
197
|
properties: {
|
|
182
198
|
scanner: {
|
|
183
199
|
type: "string",
|
|
184
|
-
enum: ["semgrep", "eslint", "bandit", "stryker"],
|
|
200
|
+
enum: ["semgrep", "eslint", "bandit", "stryker", "dart_analyze", "dotnet_format"],
|
|
185
201
|
description: "Identifier of the producing scanner.",
|
|
186
202
|
},
|
|
187
203
|
rawOutput: {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized file and directory exclusion system.
|
|
3
|
+
*
|
|
4
|
+
* Every filesystem walker in the codebase (workspace-walker,
|
|
5
|
+
* complexity-scanner, dashboard file-detail) imports from this module
|
|
6
|
+
* instead of maintaining its own `SKIP_DIRS` constant. This
|
|
7
|
+
* guarantees all subsystems agree on what to exclude.
|
|
8
|
+
*
|
|
9
|
+
* User-configurable exclusions from `.claude-crap.json` are layered
|
|
10
|
+
* on top of the defaults via {@link createExclusionFilter}.
|
|
11
|
+
*
|
|
12
|
+
* @module shared/exclusions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import picomatch from "picomatch";
|
|
16
|
+
|
|
17
|
+
// ── Default exclusions ──────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Directories excluded by name at any depth. A walker that encounters
|
|
21
|
+
* a directory entry whose name is in this set should skip the entire
|
|
22
|
+
* subtree. The set covers package managers, VCS, build outputs for
|
|
23
|
+
* all major frameworks, language-specific caches, and plugin state.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
26
|
+
// Package managers / vendored deps
|
|
27
|
+
"node_modules",
|
|
28
|
+
"vendor",
|
|
29
|
+
|
|
30
|
+
// Version control
|
|
31
|
+
".git",
|
|
32
|
+
|
|
33
|
+
// Build outputs (general)
|
|
34
|
+
"dist",
|
|
35
|
+
"build",
|
|
36
|
+
"bundle",
|
|
37
|
+
"out",
|
|
38
|
+
"target",
|
|
39
|
+
"coverage",
|
|
40
|
+
|
|
41
|
+
// Framework build outputs
|
|
42
|
+
".next", // Next.js
|
|
43
|
+
".nuxt", // Nuxt 2
|
|
44
|
+
".output", // Nuxt 3
|
|
45
|
+
".vercel", // Vercel
|
|
46
|
+
".svelte-kit", // SvelteKit
|
|
47
|
+
".astro", // Astro
|
|
48
|
+
".angular", // Angular
|
|
49
|
+
".turbo", // Turborepo
|
|
50
|
+
".parcel-cache",// Parcel
|
|
51
|
+
".expo", // Expo / React Native
|
|
52
|
+
|
|
53
|
+
// Language-specific caches
|
|
54
|
+
".venv",
|
|
55
|
+
"venv",
|
|
56
|
+
"__pycache__",
|
|
57
|
+
".cache",
|
|
58
|
+
".dart_tool", // Dart / Flutter
|
|
59
|
+
".gradle", // Gradle
|
|
60
|
+
|
|
61
|
+
// IDE state
|
|
62
|
+
".idea",
|
|
63
|
+
|
|
64
|
+
// Plugin state
|
|
65
|
+
".claude-crap",
|
|
66
|
+
".codesight",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Filename-level glob patterns that match generated or minified files
|
|
71
|
+
* regardless of which directory they live in. Matched against the
|
|
72
|
+
* bare filename (not the full path).
|
|
73
|
+
*/
|
|
74
|
+
export const DEFAULT_SKIP_PATTERNS: ReadonlyArray<string> = [
|
|
75
|
+
"*.min.js",
|
|
76
|
+
"*.min.css",
|
|
77
|
+
"*.min.mjs",
|
|
78
|
+
"*.min.cjs",
|
|
79
|
+
"*.bundle.js",
|
|
80
|
+
"*.chunk.js",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// ── Exclusion filter ────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stateless, pre-compiled filter that every filesystem walker uses
|
|
87
|
+
* to decide whether to skip a directory or file.
|
|
88
|
+
*/
|
|
89
|
+
export interface ExclusionFilter {
|
|
90
|
+
/** Returns `true` when the directory should be skipped entirely. */
|
|
91
|
+
shouldSkipDir(dirName: string): boolean;
|
|
92
|
+
/** Returns `true` when the file should be excluded from analysis. */
|
|
93
|
+
shouldSkipFile(relativePath: string, fileName: string): boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create an {@link ExclusionFilter} that combines the built-in
|
|
98
|
+
* defaults with optional user-defined patterns from `.claude-crap.json`.
|
|
99
|
+
*
|
|
100
|
+
* User patterns follow `.gitignore`-style conventions:
|
|
101
|
+
* - `apps/legacy/` → trailing `/` means directory exclusion
|
|
102
|
+
* - `*.proto.ts` → glob matched against workspace-relative path
|
|
103
|
+
* - `src/generated/**` → path-prefix glob
|
|
104
|
+
*
|
|
105
|
+
* Picomatch matchers are compiled once at construction, so per-file
|
|
106
|
+
* checks are O(1) set lookups plus O(n) matcher calls where n is
|
|
107
|
+
* the small number of user patterns (typically < 20).
|
|
108
|
+
*
|
|
109
|
+
* @param userExclusions Optional patterns from `.claude-crap.json`.
|
|
110
|
+
*/
|
|
111
|
+
export function createExclusionFilter(
|
|
112
|
+
userExclusions?: ReadonlyArray<string>,
|
|
113
|
+
): ExclusionFilter {
|
|
114
|
+
// Split user patterns into directory exclusions and file globs
|
|
115
|
+
const extraDirs = new Set<string>();
|
|
116
|
+
const fileGlobs: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const pattern of userExclusions ?? []) {
|
|
119
|
+
if (pattern.endsWith("/")) {
|
|
120
|
+
// Directory exclusion — strip trailing slash
|
|
121
|
+
extraDirs.add(pattern.slice(0, -1));
|
|
122
|
+
} else {
|
|
123
|
+
fileGlobs.push(pattern);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Compile filename-level matchers once
|
|
128
|
+
const defaultFileMatchers = DEFAULT_SKIP_PATTERNS.map((p) =>
|
|
129
|
+
picomatch(p, { dot: true }),
|
|
130
|
+
);
|
|
131
|
+
const userFileMatchers = fileGlobs.map((p) =>
|
|
132
|
+
picomatch(p, { dot: true }),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
shouldSkipDir(dirName: string): boolean {
|
|
137
|
+
// Hidden directories are always skipped except .claude-plugin
|
|
138
|
+
if (dirName.startsWith(".") && dirName !== ".claude-plugin") {
|
|
139
|
+
return DEFAULT_SKIP_DIRS.has(dirName) || true;
|
|
140
|
+
}
|
|
141
|
+
return DEFAULT_SKIP_DIRS.has(dirName) || extraDirs.has(dirName);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
shouldSkipFile(relativePath: string, fileName: string): boolean {
|
|
145
|
+
// Check filename against default minified/bundled patterns
|
|
146
|
+
for (const matcher of defaultFileMatchers) {
|
|
147
|
+
if (matcher(fileName)) return true;
|
|
148
|
+
}
|
|
149
|
+
// Check against user-defined globs (matched on relative path)
|
|
150
|
+
for (const matcher of userFileMatchers) {
|
|
151
|
+
if (matcher(relativePath) || matcher(fileName)) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -94,7 +94,7 @@ describe("adaptScannerOutput", () => {
|
|
|
94
94
|
assert.throws(() => adaptScannerOutput(scanner, {}));
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it("KNOWN_SCANNERS is frozen and contains
|
|
98
|
-
assert.deepEqual([...KNOWN_SCANNERS].sort(), ["bandit", "eslint", "semgrep", "stryker"]);
|
|
97
|
+
it("KNOWN_SCANNERS is frozen and contains all supported names", () => {
|
|
98
|
+
assert.deepEqual([...KNOWN_SCANNERS].sort(), ["bandit", "dart_analyze", "dotnet_format", "eslint", "semgrep", "stryker"]);
|
|
99
99
|
});
|
|
100
100
|
});
|
|
@@ -34,7 +34,7 @@ describe("autoScan", () => {
|
|
|
34
34
|
outputDir: join(dir, ".claude-crap/reports"),
|
|
35
35
|
});
|
|
36
36
|
const result = await autoScan(dir, store, logger);
|
|
37
|
-
assert.equal(result.detected.length,
|
|
37
|
+
assert.equal(result.detected.length, 6);
|
|
38
38
|
assert.ok(result.totalDurationMs >= 0);
|
|
39
39
|
// No scanners available means no results
|
|
40
40
|
// (unless the host has scanner binaries installed)
|
|
@@ -129,7 +129,7 @@ describe("autoScan", () => {
|
|
|
129
129
|
const result = await autoScan(dir, store, logger);
|
|
130
130
|
|
|
131
131
|
const scannerNames = result.detected.map((d) => d.scanner).sort();
|
|
132
|
-
assert.deepEqual(scannerNames, ["bandit", "eslint", "semgrep", "stryker"]);
|
|
132
|
+
assert.deepEqual(scannerNames, ["bandit", "dart_analyze", "dotnet_format", "eslint", "semgrep", "stryker"]);
|
|
133
133
|
} finally {
|
|
134
134
|
rmSync(dir, { recursive: true, force: true });
|
|
135
135
|
}
|