claude-crap 0.3.5 → 0.3.7
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 +13 -0
- package/dist/dashboard/file-detail.d.ts +77 -0
- package/dist/dashboard/file-detail.d.ts.map +1 -0
- package/dist/dashboard/file-detail.js +120 -0
- package/dist/dashboard/file-detail.js.map +1 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +108 -1
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/scanner/auto-scan.d.ts +8 -1
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -1
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +54 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -0
- package/dist/scanner/complexity-scanner.js +176 -0
- package/dist/scanner/complexity-scanner.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/dashboard/public/index.html +432 -12
- package/plugin/bundle/mcp-server.mjs +429 -71
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +2 -2
- package/plugin/package.json +1 -1
- package/scripts/bundle-plugin.mjs +53 -2
- package/src/dashboard/file-detail.ts +197 -0
- package/src/dashboard/public/index.html +432 -12
- package/src/dashboard/server.ts +141 -1
- package/src/index.ts +20 -2
- package/src/scanner/auto-scan.ts +26 -0
- package/src/scanner/complexity-scanner.ts +233 -0
- package/src/tests/complexity-scanner.test.ts +263 -0
- package/src/tests/file-detail-api.test.ts +258 -0
package/src/index.ts
CHANGED
|
@@ -114,6 +114,7 @@ async function main(): Promise<void> {
|
|
|
114
114
|
sarifStore,
|
|
115
115
|
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot),
|
|
116
116
|
logger,
|
|
117
|
+
astEngine,
|
|
117
118
|
});
|
|
118
119
|
} catch (err) {
|
|
119
120
|
logger.warn(
|
|
@@ -581,7 +582,10 @@ async function main(): Promise<void> {
|
|
|
581
582
|
case "auto_scan": {
|
|
582
583
|
logger.info({ tool: "auto_scan" }, "Tool call received");
|
|
583
584
|
try {
|
|
584
|
-
const result = await autoScan(config.pluginRoot, sarifStore, logger
|
|
585
|
+
const result = await autoScan(config.pluginRoot, sarifStore, logger, {
|
|
586
|
+
engine: astEngine,
|
|
587
|
+
cyclomaticMax: config.cyclomaticMax,
|
|
588
|
+
});
|
|
585
589
|
const markdown = renderAutoScanMarkdown(result);
|
|
586
590
|
return {
|
|
587
591
|
content: [
|
|
@@ -669,7 +673,10 @@ async function main(): Promise<void> {
|
|
|
669
673
|
// If the agent calls score_project before scanning finishes, it gets
|
|
670
674
|
// whatever is in the SARIF store so far. The next call after completion
|
|
671
675
|
// reflects all findings.
|
|
672
|
-
autoScan(config.pluginRoot, sarifStore, logger
|
|
676
|
+
autoScan(config.pluginRoot, sarifStore, logger, {
|
|
677
|
+
engine: astEngine,
|
|
678
|
+
cyclomaticMax: config.cyclomaticMax,
|
|
679
|
+
})
|
|
673
680
|
.then((result) => {
|
|
674
681
|
const scanners = result.results
|
|
675
682
|
.filter((r) => r.success)
|
|
@@ -759,6 +766,17 @@ function renderAutoScanMarkdown(result: import("./scanner/auto-scan.js").AutoSca
|
|
|
759
766
|
lines.push("");
|
|
760
767
|
}
|
|
761
768
|
|
|
769
|
+
// Complexity scan
|
|
770
|
+
if (result.complexityScan) {
|
|
771
|
+
const cs = result.complexityScan;
|
|
772
|
+
lines.push("### Cyclomatic complexity scan\n");
|
|
773
|
+
lines.push(`- Files scanned: **${cs.filesScanned}**`);
|
|
774
|
+
lines.push(`- Functions analyzed: **${cs.functionsAnalyzed}**`);
|
|
775
|
+
lines.push(`- Violations: **${cs.violations}**`);
|
|
776
|
+
lines.push(`- Duration: ${(cs.durationMs / 1000).toFixed(1)}s`);
|
|
777
|
+
lines.push("");
|
|
778
|
+
}
|
|
779
|
+
|
|
762
780
|
// Summary
|
|
763
781
|
lines.push(
|
|
764
782
|
`**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1000).toFixed(1)}s`,
|
package/src/scanner/auto-scan.ts
CHANGED
|
@@ -25,7 +25,9 @@ import type { Logger } from "pino";
|
|
|
25
25
|
import { detectScanners, type ScannerDetection } from "./detector.js";
|
|
26
26
|
import { runScanner, type ScannerRunResult } from "./runner.js";
|
|
27
27
|
import { bootstrapScanner } from "./bootstrap.js";
|
|
28
|
+
import { scanComplexity, type ComplexityScanResult } from "./complexity-scanner.js";
|
|
28
29
|
import { adaptScannerOutput, type KnownScanner } from "../adapters/index.js";
|
|
30
|
+
import type { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
|
|
29
31
|
import type { SarifStore } from "../sarif/sarif-store.js";
|
|
30
32
|
|
|
31
33
|
// ── Types ──────────────────────────────────────────────────────────
|
|
@@ -53,6 +55,8 @@ export interface AutoScanResult {
|
|
|
53
55
|
totalFindings: number;
|
|
54
56
|
/** Wall-clock time for the entire auto-scan. */
|
|
55
57
|
totalDurationMs: number;
|
|
58
|
+
/** Result of the built-in cyclomatic complexity scan, when enabled. */
|
|
59
|
+
complexityScan?: ComplexityScanResult;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
// ── Orchestrator ───────────────────────────────────────────────────
|
|
@@ -93,6 +97,7 @@ export async function autoScan(
|
|
|
93
97
|
workspaceRoot: string,
|
|
94
98
|
sarifStore: SarifStore,
|
|
95
99
|
logger: Logger,
|
|
100
|
+
options?: { engine?: TreeSitterEngine; cyclomaticMax?: number },
|
|
96
101
|
): Promise<AutoScanResult> {
|
|
97
102
|
const start = Date.now();
|
|
98
103
|
|
|
@@ -246,10 +251,31 @@ export async function autoScan(
|
|
|
246
251
|
await sarifStore.persist();
|
|
247
252
|
}
|
|
248
253
|
|
|
254
|
+
// 5. Run built-in cyclomatic complexity scanner
|
|
255
|
+
let complexityScan: ComplexityScanResult | undefined;
|
|
256
|
+
if (options?.engine) {
|
|
257
|
+
try {
|
|
258
|
+
complexityScan = await scanComplexity(
|
|
259
|
+
workspaceRoot,
|
|
260
|
+
options.engine,
|
|
261
|
+
sarifStore,
|
|
262
|
+
{ cyclomaticMax: options.cyclomaticMax ?? 15 },
|
|
263
|
+
logger,
|
|
264
|
+
);
|
|
265
|
+
totalFindings += complexityScan.violations;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
logger.warn(
|
|
268
|
+
{ err: (err as Error).message },
|
|
269
|
+
"auto-scan: complexity scanner failed — continuing without it",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
249
274
|
return {
|
|
250
275
|
detected,
|
|
251
276
|
results,
|
|
252
277
|
totalFindings,
|
|
253
278
|
totalDurationMs: Date.now() - start,
|
|
279
|
+
...(complexityScan ? { complexityScan } : {}),
|
|
254
280
|
};
|
|
255
281
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cyclomatic complexity scanner.
|
|
3
|
+
*
|
|
4
|
+
* Walks the workspace, analyzes each supported source file with
|
|
5
|
+
* tree-sitter, and emits SARIF findings for functions whose cyclomatic
|
|
6
|
+
* complexity exceeds the configured threshold (`cyclomaticMax`).
|
|
7
|
+
*
|
|
8
|
+
* This scanner is an internal analyzer — it is NOT a "known scanner"
|
|
9
|
+
* in the `KnownScanner` union (eslint/semgrep/bandit/stryker). It
|
|
10
|
+
* bypasses the adapter pipeline and writes SARIF directly via
|
|
11
|
+
* `wrapResultsInSarif()` from the common adapter helpers.
|
|
12
|
+
*
|
|
13
|
+
* Severity mapping:
|
|
14
|
+
* - `warning` — CC > threshold but < 2× threshold
|
|
15
|
+
* - `error` — CC >= 2× threshold (aligns with CRAP index hard block at CC >= 30)
|
|
16
|
+
*
|
|
17
|
+
* @module scanner/complexity-scanner
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { promises as fs } from "node:fs";
|
|
21
|
+
import { join, relative } from "node:path";
|
|
22
|
+
import type { Logger } from "pino";
|
|
23
|
+
|
|
24
|
+
import { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
|
|
25
|
+
import { detectLanguageFromPath } from "../ast/language-config.js";
|
|
26
|
+
import { wrapResultsInSarif, estimateEffortMinutes } from "../adapters/common.js";
|
|
27
|
+
import type { SarifStore } from "../sarif/sarif-store.js";
|
|
28
|
+
import type { SarifLevel } from "../sarif/sarif-builder.js";
|
|
29
|
+
|
|
30
|
+
// ── Constants ─────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** Directories that should never be scanned. Mirrors `workspace-walker.ts`. */
|
|
33
|
+
const SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
34
|
+
"node_modules",
|
|
35
|
+
".git",
|
|
36
|
+
"dist",
|
|
37
|
+
"build",
|
|
38
|
+
"out",
|
|
39
|
+
"target",
|
|
40
|
+
".venv",
|
|
41
|
+
"venv",
|
|
42
|
+
"__pycache__",
|
|
43
|
+
".cache",
|
|
44
|
+
".next",
|
|
45
|
+
".nuxt",
|
|
46
|
+
".claude-crap",
|
|
47
|
+
".codesight",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/** Hard cap on files to prevent unbounded analysis. */
|
|
51
|
+
const MAX_FILES = 20_000;
|
|
52
|
+
|
|
53
|
+
/** SARIF rule ID for cyclomatic complexity violations. */
|
|
54
|
+
const RULE_ID = "complexity/cyclomatic-max";
|
|
55
|
+
|
|
56
|
+
/** Source tool identifier used in SARIF properties. */
|
|
57
|
+
const SOURCE_TOOL = "complexity";
|
|
58
|
+
|
|
59
|
+
// ── Types ─────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** Result of a complexity scan run. */
|
|
62
|
+
export interface ComplexityScanResult {
|
|
63
|
+
/** Number of source files successfully analyzed. */
|
|
64
|
+
readonly filesScanned: number;
|
|
65
|
+
/** Total number of functions found across all files. */
|
|
66
|
+
readonly functionsAnalyzed: number;
|
|
67
|
+
/** Number of functions that exceeded the threshold. */
|
|
68
|
+
readonly violations: number;
|
|
69
|
+
/** Wall-clock time for the entire scan. */
|
|
70
|
+
readonly durationMs: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Configuration accepted by the scanner. */
|
|
74
|
+
export interface ComplexityScanConfig {
|
|
75
|
+
/** Maximum cyclomatic complexity allowed per function. */
|
|
76
|
+
readonly cyclomaticMax: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Scanner ───────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Scan a workspace for cyclomatic complexity violations.
|
|
83
|
+
*
|
|
84
|
+
* Walks the directory tree, analyzes each source file with the
|
|
85
|
+
* tree-sitter engine, and emits SARIF findings for functions above
|
|
86
|
+
* the configured threshold. Findings are ingested into the provided
|
|
87
|
+
* `SarifStore` and persisted to disk.
|
|
88
|
+
*
|
|
89
|
+
* @param workspaceRoot Absolute path to the workspace root.
|
|
90
|
+
* @param engine Initialized tree-sitter engine instance.
|
|
91
|
+
* @param sarifStore Live SARIF store to ingest findings into.
|
|
92
|
+
* @param config Scanner configuration (threshold).
|
|
93
|
+
* @param logger Pino logger for progress and error reporting.
|
|
94
|
+
* @returns Summary of what was scanned and found.
|
|
95
|
+
*/
|
|
96
|
+
export async function scanComplexity(
|
|
97
|
+
workspaceRoot: string,
|
|
98
|
+
engine: TreeSitterEngine,
|
|
99
|
+
sarifStore: SarifStore,
|
|
100
|
+
config: ComplexityScanConfig,
|
|
101
|
+
logger: Logger,
|
|
102
|
+
): Promise<ComplexityScanResult> {
|
|
103
|
+
const start = Date.now();
|
|
104
|
+
const threshold = config.cyclomaticMax;
|
|
105
|
+
const errorThreshold = threshold * 2;
|
|
106
|
+
|
|
107
|
+
// 1. Collect supported source files
|
|
108
|
+
const files = await collectSourceFiles(workspaceRoot);
|
|
109
|
+
logger.info(
|
|
110
|
+
{ fileCount: files.length, threshold },
|
|
111
|
+
"complexity-scanner: starting analysis",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// 2. Analyze each file and collect violations
|
|
115
|
+
const sarifResults: object[] = [];
|
|
116
|
+
let filesScanned = 0;
|
|
117
|
+
let functionsAnalyzed = 0;
|
|
118
|
+
let violations = 0;
|
|
119
|
+
|
|
120
|
+
for (const filePath of files) {
|
|
121
|
+
const language = detectLanguageFromPath(filePath);
|
|
122
|
+
if (!language) continue;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const metrics = await engine.analyzeFile({ filePath, language });
|
|
126
|
+
filesScanned += 1;
|
|
127
|
+
functionsAnalyzed += metrics.functions.length;
|
|
128
|
+
|
|
129
|
+
for (const fn of metrics.functions) {
|
|
130
|
+
if (fn.cyclomaticComplexity <= threshold) continue;
|
|
131
|
+
|
|
132
|
+
const level: SarifLevel =
|
|
133
|
+
fn.cyclomaticComplexity >= errorThreshold ? "error" : "warning";
|
|
134
|
+
|
|
135
|
+
const relPath = relative(workspaceRoot, filePath);
|
|
136
|
+
|
|
137
|
+
sarifResults.push({
|
|
138
|
+
ruleId: RULE_ID,
|
|
139
|
+
level,
|
|
140
|
+
message: {
|
|
141
|
+
text: `Function '${fn.name}' has cyclomatic complexity ${fn.cyclomaticComplexity} (threshold: ${threshold})`,
|
|
142
|
+
},
|
|
143
|
+
locations: [
|
|
144
|
+
{
|
|
145
|
+
physicalLocation: {
|
|
146
|
+
artifactLocation: { uri: relPath },
|
|
147
|
+
region: {
|
|
148
|
+
startLine: fn.startLine,
|
|
149
|
+
startColumn: 1,
|
|
150
|
+
endLine: fn.endLine,
|
|
151
|
+
endColumn: 1,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
properties: {
|
|
157
|
+
sourceTool: SOURCE_TOOL,
|
|
158
|
+
effortMinutes: estimateEffortMinutes(level),
|
|
159
|
+
cyclomaticComplexity: fn.cyclomaticComplexity,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
violations += 1;
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
logger.warn(
|
|
166
|
+
{ filePath, err: (err as Error).message },
|
|
167
|
+
"complexity-scanner: failed to analyze file, skipping",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 3. Ingest findings into the SARIF store
|
|
173
|
+
if (sarifResults.length > 0) {
|
|
174
|
+
const document = wrapResultsInSarif(
|
|
175
|
+
SOURCE_TOOL as never,
|
|
176
|
+
"0.1.0",
|
|
177
|
+
sarifResults,
|
|
178
|
+
);
|
|
179
|
+
sarifStore.ingestRun(document, SOURCE_TOOL);
|
|
180
|
+
await sarifStore.persist();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const durationMs = Date.now() - start;
|
|
184
|
+
logger.info(
|
|
185
|
+
{ filesScanned, functionsAnalyzed, violations, durationMs },
|
|
186
|
+
"complexity-scanner: analysis complete",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return { filesScanned, functionsAnalyzed, violations, durationMs };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── File walker ───────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Collect source files from the workspace that the tree-sitter engine
|
|
196
|
+
* can analyze. Skips directories in `SKIP_DIRS` and hidden directories.
|
|
197
|
+
* Only returns files whose extension maps to a supported language.
|
|
198
|
+
*/
|
|
199
|
+
async function collectSourceFiles(workspaceRoot: string): Promise<string[]> {
|
|
200
|
+
const files: string[] = [];
|
|
201
|
+
let truncated = false;
|
|
202
|
+
|
|
203
|
+
async function walk(dir: string): Promise<void> {
|
|
204
|
+
if (truncated) return;
|
|
205
|
+
let entries;
|
|
206
|
+
try {
|
|
207
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
208
|
+
} catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (truncated) return;
|
|
213
|
+
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
214
|
+
const full = join(dir, entry.name);
|
|
215
|
+
if (entry.isDirectory()) {
|
|
216
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
217
|
+
await walk(full);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!entry.isFile()) continue;
|
|
221
|
+
// Only include files the tree-sitter engine can parse
|
|
222
|
+
if (!detectLanguageFromPath(entry.name)) continue;
|
|
223
|
+
files.push(full);
|
|
224
|
+
if (files.length >= MAX_FILES) {
|
|
225
|
+
truncated = true;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await walk(workspaceRoot);
|
|
232
|
+
return files;
|
|
233
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the cyclomatic complexity scanner.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that the scanner walks a workspace, analyzes source
|
|
5
|
+
* files with tree-sitter, and emits SARIF findings for functions whose
|
|
6
|
+
* cyclomatic complexity exceeds the configured threshold.
|
|
7
|
+
*
|
|
8
|
+
* @module tests/complexity-scanner.test
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import pino from "pino";
|
|
17
|
+
|
|
18
|
+
import { scanComplexity } from "../scanner/complexity-scanner.js";
|
|
19
|
+
import { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
|
|
20
|
+
import { SarifStore } from "../sarif/sarif-store.js";
|
|
21
|
+
|
|
22
|
+
const logger = pino({ level: "silent" });
|
|
23
|
+
|
|
24
|
+
function makeTmpDir(): string {
|
|
25
|
+
return mkdtempSync(join(tmpdir(), "crap-complexity-"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Simple function: CC = 1 (straight-line). */
|
|
29
|
+
const SIMPLE_TS = `
|
|
30
|
+
export function greet(name: string): string {
|
|
31
|
+
return "hello " + name;
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
/** Function with CC = 5: 4 if-branches + 1 baseline. */
|
|
36
|
+
const COMPLEX_TS = `
|
|
37
|
+
export function classify(x: number): string {
|
|
38
|
+
if (x < 0) return "negative";
|
|
39
|
+
if (x === 0) return "zero";
|
|
40
|
+
if (x < 10) return "small";
|
|
41
|
+
if (x < 100) return "medium";
|
|
42
|
+
return "large";
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extremely complex function: many branches to push CC well above 30.
|
|
48
|
+
* Each if/else-if adds +1, plus boolean operators.
|
|
49
|
+
*/
|
|
50
|
+
const EXTREME_TS = `
|
|
51
|
+
export function extremelyComplex(a: number, b: number, c: number): string {
|
|
52
|
+
if (a > 0) { return "a1"; }
|
|
53
|
+
if (a > 1) { return "a2"; }
|
|
54
|
+
if (a > 2) { return "a3"; }
|
|
55
|
+
if (a > 3) { return "a4"; }
|
|
56
|
+
if (a > 4) { return "a5"; }
|
|
57
|
+
if (a > 5) { return "a6"; }
|
|
58
|
+
if (a > 6) { return "a7"; }
|
|
59
|
+
if (a > 7) { return "a8"; }
|
|
60
|
+
if (a > 8) { return "a9"; }
|
|
61
|
+
if (a > 9) { return "a10"; }
|
|
62
|
+
if (b > 0) { return "b1"; }
|
|
63
|
+
if (b > 1) { return "b2"; }
|
|
64
|
+
if (b > 2) { return "b3"; }
|
|
65
|
+
if (b > 3) { return "b4"; }
|
|
66
|
+
if (b > 4) { return "b5"; }
|
|
67
|
+
if (b > 5) { return "b6"; }
|
|
68
|
+
if (b > 6) { return "b7"; }
|
|
69
|
+
if (b > 7) { return "b8"; }
|
|
70
|
+
if (b > 8) { return "b9"; }
|
|
71
|
+
if (b > 9) { return "b10"; }
|
|
72
|
+
if (c > 0) { return "c1"; }
|
|
73
|
+
if (c > 1) { return "c2"; }
|
|
74
|
+
if (c > 2) { return "c3"; }
|
|
75
|
+
if (c > 3) { return "c4"; }
|
|
76
|
+
if (c > 4) { return "c5"; }
|
|
77
|
+
if (c > 5) { return "c6"; }
|
|
78
|
+
if (c > 6) { return "c7"; }
|
|
79
|
+
if (c > 7) { return "c8"; }
|
|
80
|
+
if (c > 8) { return "c9"; }
|
|
81
|
+
if (c > 9) { return "c10"; }
|
|
82
|
+
return "default";
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
describe("scanComplexity", () => {
|
|
87
|
+
const engine = new TreeSitterEngine();
|
|
88
|
+
|
|
89
|
+
it("returns zero violations for an empty workspace", async () => {
|
|
90
|
+
const dir = makeTmpDir();
|
|
91
|
+
try {
|
|
92
|
+
const store = new SarifStore({
|
|
93
|
+
workspaceRoot: dir,
|
|
94
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
95
|
+
});
|
|
96
|
+
const result = await scanComplexity(
|
|
97
|
+
dir, engine, store, { cyclomaticMax: 15 }, logger,
|
|
98
|
+
);
|
|
99
|
+
assert.equal(result.violations, 0);
|
|
100
|
+
assert.equal(result.filesScanned, 0);
|
|
101
|
+
assert.equal(result.functionsAnalyzed, 0);
|
|
102
|
+
assert.ok(result.durationMs >= 0);
|
|
103
|
+
} finally {
|
|
104
|
+
rmSync(dir, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("finds no violations when all functions are below threshold", async () => {
|
|
109
|
+
const dir = makeTmpDir();
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(join(dir, "simple.ts"), SIMPLE_TS);
|
|
112
|
+
const store = new SarifStore({
|
|
113
|
+
workspaceRoot: dir,
|
|
114
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
115
|
+
});
|
|
116
|
+
const result = await scanComplexity(
|
|
117
|
+
dir, engine, store, { cyclomaticMax: 15 }, logger,
|
|
118
|
+
);
|
|
119
|
+
assert.equal(result.violations, 0);
|
|
120
|
+
assert.equal(result.filesScanned, 1);
|
|
121
|
+
assert.ok(result.functionsAnalyzed >= 1);
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(dir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("flags functions above cyclomaticMax as warnings", async () => {
|
|
128
|
+
const dir = makeTmpDir();
|
|
129
|
+
try {
|
|
130
|
+
writeFileSync(join(dir, "complex.ts"), COMPLEX_TS);
|
|
131
|
+
const store = new SarifStore({
|
|
132
|
+
workspaceRoot: dir,
|
|
133
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
134
|
+
});
|
|
135
|
+
// Set threshold to 3 so the CC=5 function triggers
|
|
136
|
+
const result = await scanComplexity(
|
|
137
|
+
dir, engine, store, { cyclomaticMax: 3 }, logger,
|
|
138
|
+
);
|
|
139
|
+
assert.equal(result.violations, 1);
|
|
140
|
+
|
|
141
|
+
// Verify the finding is in the SARIF store
|
|
142
|
+
const findings = store.list();
|
|
143
|
+
const complexityFindings = findings.filter(
|
|
144
|
+
(f) => f.ruleId === "complexity/cyclomatic-max",
|
|
145
|
+
);
|
|
146
|
+
assert.equal(complexityFindings.length, 1);
|
|
147
|
+
assert.equal(complexityFindings[0]!.level, "warning");
|
|
148
|
+
assert.equal(complexityFindings[0]!.sourceTool, "complexity");
|
|
149
|
+
assert.ok(complexityFindings[0]!.message.includes("classify"));
|
|
150
|
+
} finally {
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("emits error level for functions at >= 2x threshold", async () => {
|
|
156
|
+
const dir = makeTmpDir();
|
|
157
|
+
try {
|
|
158
|
+
writeFileSync(join(dir, "extreme.ts"), EXTREME_TS);
|
|
159
|
+
const store = new SarifStore({
|
|
160
|
+
workspaceRoot: dir,
|
|
161
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
162
|
+
});
|
|
163
|
+
// threshold=15, CC=31 → 31 >= 30 (2x15) → error
|
|
164
|
+
const result = await scanComplexity(
|
|
165
|
+
dir, engine, store, { cyclomaticMax: 15 }, logger,
|
|
166
|
+
);
|
|
167
|
+
assert.equal(result.violations, 1);
|
|
168
|
+
|
|
169
|
+
const findings = store.list();
|
|
170
|
+
const complexityFindings = findings.filter(
|
|
171
|
+
(f) => f.ruleId === "complexity/cyclomatic-max",
|
|
172
|
+
);
|
|
173
|
+
assert.equal(complexityFindings.length, 1);
|
|
174
|
+
assert.equal(complexityFindings[0]!.level, "error");
|
|
175
|
+
} finally {
|
|
176
|
+
rmSync(dir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("skips directories in SKIP_DIRS", async () => {
|
|
181
|
+
const dir = makeTmpDir();
|
|
182
|
+
try {
|
|
183
|
+
// Put a complex file inside node_modules — should be skipped
|
|
184
|
+
const nmDir = join(dir, "node_modules", "pkg");
|
|
185
|
+
mkdirSync(nmDir, { recursive: true });
|
|
186
|
+
writeFileSync(join(nmDir, "index.ts"), COMPLEX_TS);
|
|
187
|
+
|
|
188
|
+
const store = new SarifStore({
|
|
189
|
+
workspaceRoot: dir,
|
|
190
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
191
|
+
});
|
|
192
|
+
const result = await scanComplexity(
|
|
193
|
+
dir, engine, store, { cyclomaticMax: 3 }, logger,
|
|
194
|
+
);
|
|
195
|
+
assert.equal(result.filesScanned, 0);
|
|
196
|
+
assert.equal(result.violations, 0);
|
|
197
|
+
} finally {
|
|
198
|
+
rmSync(dir, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("skips unsupported file extensions", async () => {
|
|
203
|
+
const dir = makeTmpDir();
|
|
204
|
+
try {
|
|
205
|
+
writeFileSync(join(dir, "data.json"), '{"key": "value"}');
|
|
206
|
+
writeFileSync(join(dir, "readme.md"), "# Hello");
|
|
207
|
+
writeFileSync(join(dir, "styles.css"), "body {}");
|
|
208
|
+
|
|
209
|
+
const store = new SarifStore({
|
|
210
|
+
workspaceRoot: dir,
|
|
211
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
212
|
+
});
|
|
213
|
+
const result = await scanComplexity(
|
|
214
|
+
dir, engine, store, { cyclomaticMax: 15 }, logger,
|
|
215
|
+
);
|
|
216
|
+
assert.equal(result.filesScanned, 0);
|
|
217
|
+
} finally {
|
|
218
|
+
rmSync(dir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("respects custom cyclomaticMax threshold", async () => {
|
|
223
|
+
const dir = makeTmpDir();
|
|
224
|
+
try {
|
|
225
|
+
writeFileSync(join(dir, "complex.ts"), COMPLEX_TS);
|
|
226
|
+
const store = new SarifStore({
|
|
227
|
+
workspaceRoot: dir,
|
|
228
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
229
|
+
});
|
|
230
|
+
// CC=5, threshold=10 → no violation
|
|
231
|
+
const result = await scanComplexity(
|
|
232
|
+
dir, engine, store, { cyclomaticMax: 10 }, logger,
|
|
233
|
+
);
|
|
234
|
+
assert.equal(result.violations, 0);
|
|
235
|
+
} finally {
|
|
236
|
+
rmSync(dir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("sets effortMinutes and cyclomaticComplexity in finding properties", async () => {
|
|
241
|
+
const dir = makeTmpDir();
|
|
242
|
+
try {
|
|
243
|
+
writeFileSync(join(dir, "complex.ts"), COMPLEX_TS);
|
|
244
|
+
const store = new SarifStore({
|
|
245
|
+
workspaceRoot: dir,
|
|
246
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
247
|
+
});
|
|
248
|
+
const result = await scanComplexity(
|
|
249
|
+
dir, engine, store, { cyclomaticMax: 3 }, logger,
|
|
250
|
+
);
|
|
251
|
+
assert.equal(result.violations, 1);
|
|
252
|
+
|
|
253
|
+
const findings = store.list();
|
|
254
|
+
const finding = findings.find((f) => f.ruleId === "complexity/cyclomatic-max");
|
|
255
|
+
assert.ok(finding);
|
|
256
|
+
assert.equal(typeof finding.properties?.effortMinutes, "number");
|
|
257
|
+
assert.ok((finding.properties?.effortMinutes as number) > 0);
|
|
258
|
+
assert.equal(typeof finding.properties?.cyclomaticComplexity, "number");
|
|
259
|
+
} finally {
|
|
260
|
+
rmSync(dir, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|