claude-crap 0.3.6 → 0.3.8
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/README.md +25 -0
- 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/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/crap-config.d.ts +2 -0
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +36 -28
- package/dist/crap-config.js.map +1 -1
- 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 +5 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +103 -1
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +36 -4
- 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/scanner/auto-scan.d.ts +9 -1
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +27 -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 +9 -0
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +56 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -0
- package/dist/scanner/complexity-scanner.js +161 -0
- package/dist/scanner/complexity-scanner.js.map +1 -0
- package/dist/scanner/detector.d.ts +24 -4
- package/dist/scanner/detector.d.ts.map +1 -1
- package/dist/scanner/detector.js +105 -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 +12 -3
- package/dist/scanner/runner.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +1 -1
- package/dist/schemas/tool-schemas.js +1 -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/bundle/dashboard/public/index.html +432 -12
- package/plugin/bundle/mcp-server.mjs +747 -137
- 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/index.ts +4 -0
- package/src/crap-config.ts +55 -18
- package/src/dashboard/file-detail.ts +195 -0
- package/src/dashboard/public/index.html +432 -12
- package/src/dashboard/server.ts +140 -1
- package/src/index.ts +37 -4
- package/src/metrics/workspace-walker.ts +15 -27
- package/src/scanner/auto-scan.ts +41 -4
- package/src/scanner/bootstrap.ts +11 -0
- package/src/scanner/complexity-scanner.ts +222 -0
- package/src/scanner/detector.ts +114 -10
- package/src/scanner/runner.ts +12 -2
- package/src/schemas/tool-schemas.ts +1 -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/complexity-scanner.test.ts +263 -0
- package/src/tests/exclusions.test.ts +117 -0
- package/src/tests/file-detail-api.test.ts +258 -0
- package/src/tests/scanner-detector.test.ts +31 -11
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the /api/file-detail endpoint logic.
|
|
3
|
+
*
|
|
4
|
+
* These tests exercise the file detail builder function directly,
|
|
5
|
+
* without spawning the full MCP server. They verify that source lines,
|
|
6
|
+
* function metrics, and filtered findings are returned correctly.
|
|
7
|
+
*
|
|
8
|
+
* @module tests/file-detail-api.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
|
+
|
|
17
|
+
import { buildFileDetail } from "../dashboard/file-detail.js";
|
|
18
|
+
import { TreeSitterEngine } from "../ast/tree-sitter-engine.js";
|
|
19
|
+
import { SarifStore } from "../sarif/sarif-store.js";
|
|
20
|
+
import { wrapResultsInSarif } from "../adapters/common.js";
|
|
21
|
+
|
|
22
|
+
function makeTmpDir(): string {
|
|
23
|
+
return mkdtempSync(join(tmpdir(), "crap-file-detail-"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SAMPLE_TS = `export function greet(name: string): string {
|
|
27
|
+
return "hello " + name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function classify(x: number): string {
|
|
31
|
+
if (x < 0) return "negative";
|
|
32
|
+
if (x === 0) return "zero";
|
|
33
|
+
if (x < 10) return "small";
|
|
34
|
+
if (x < 100) return "medium";
|
|
35
|
+
return "large";
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
describe("buildFileDetail", () => {
|
|
40
|
+
const engine = new TreeSitterEngine();
|
|
41
|
+
|
|
42
|
+
it("returns source lines for a valid file", async () => {
|
|
43
|
+
const dir = makeTmpDir();
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
|
|
46
|
+
const store = new SarifStore({
|
|
47
|
+
workspaceRoot: dir,
|
|
48
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
49
|
+
});
|
|
50
|
+
const result = await buildFileDetail({
|
|
51
|
+
relativePath: "hello.ts",
|
|
52
|
+
workspaceRoot: dir,
|
|
53
|
+
astEngine: engine,
|
|
54
|
+
sarifStore: store,
|
|
55
|
+
cyclomaticMax: 15,
|
|
56
|
+
});
|
|
57
|
+
assert.equal(result.filePath, "hello.ts");
|
|
58
|
+
assert.ok(result.sourceLines.length > 0);
|
|
59
|
+
assert.ok(result.sourceLines[0]!.includes("export function greet"));
|
|
60
|
+
} finally {
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns function metrics for supported languages", async () => {
|
|
66
|
+
const dir = makeTmpDir();
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
|
|
69
|
+
const store = new SarifStore({
|
|
70
|
+
workspaceRoot: dir,
|
|
71
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
72
|
+
});
|
|
73
|
+
const result = await buildFileDetail({
|
|
74
|
+
relativePath: "hello.ts",
|
|
75
|
+
workspaceRoot: dir,
|
|
76
|
+
astEngine: engine,
|
|
77
|
+
sarifStore: store,
|
|
78
|
+
cyclomaticMax: 15,
|
|
79
|
+
});
|
|
80
|
+
assert.equal(result.language, "typescript");
|
|
81
|
+
assert.ok(result.functions.length >= 2);
|
|
82
|
+
const greet = result.functions.find((f) => f.name === "greet");
|
|
83
|
+
assert.ok(greet);
|
|
84
|
+
assert.equal(greet.cyclomaticComplexity, 1);
|
|
85
|
+
const classify = result.functions.find((f) => f.name === "classify");
|
|
86
|
+
assert.ok(classify);
|
|
87
|
+
assert.ok(classify.cyclomaticComplexity >= 4);
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("filters findings to the requested file only", async () => {
|
|
94
|
+
const dir = makeTmpDir();
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(join(dir, "a.ts"), 'export const a = 1;\n');
|
|
97
|
+
writeFileSync(join(dir, "b.ts"), 'export const b = 2;\n');
|
|
98
|
+
|
|
99
|
+
const store = new SarifStore({
|
|
100
|
+
workspaceRoot: dir,
|
|
101
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Ingest findings for both files
|
|
105
|
+
const doc = wrapResultsInSarif("eslint" as never, "0.1.0", [
|
|
106
|
+
{
|
|
107
|
+
ruleId: "no-unused-vars",
|
|
108
|
+
level: "warning",
|
|
109
|
+
message: { text: "unused in a" },
|
|
110
|
+
locations: [{
|
|
111
|
+
physicalLocation: {
|
|
112
|
+
artifactLocation: { uri: "a.ts" },
|
|
113
|
+
region: { startLine: 1, startColumn: 1 },
|
|
114
|
+
},
|
|
115
|
+
}],
|
|
116
|
+
properties: { sourceTool: "eslint", effortMinutes: 30 },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
ruleId: "no-unused-vars",
|
|
120
|
+
level: "warning",
|
|
121
|
+
message: { text: "unused in b" },
|
|
122
|
+
locations: [{
|
|
123
|
+
physicalLocation: {
|
|
124
|
+
artifactLocation: { uri: "b.ts" },
|
|
125
|
+
region: { startLine: 1, startColumn: 1 },
|
|
126
|
+
},
|
|
127
|
+
}],
|
|
128
|
+
properties: { sourceTool: "eslint", effortMinutes: 30 },
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
store.ingestRun(doc, "eslint");
|
|
132
|
+
|
|
133
|
+
const result = await buildFileDetail({
|
|
134
|
+
relativePath: "a.ts",
|
|
135
|
+
workspaceRoot: dir,
|
|
136
|
+
astEngine: engine,
|
|
137
|
+
sarifStore: store,
|
|
138
|
+
cyclomaticMax: 15,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
assert.equal(result.findings.length, 1);
|
|
142
|
+
assert.ok(result.findings[0]!.message.includes("unused in a"));
|
|
143
|
+
assert.equal(result.summary.totalFindings, 1);
|
|
144
|
+
} finally {
|
|
145
|
+
rmSync(dir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("throws for non-existent file", async () => {
|
|
150
|
+
const dir = makeTmpDir();
|
|
151
|
+
try {
|
|
152
|
+
const store = new SarifStore({
|
|
153
|
+
workspaceRoot: dir,
|
|
154
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
155
|
+
});
|
|
156
|
+
await assert.rejects(
|
|
157
|
+
buildFileDetail({
|
|
158
|
+
relativePath: "nonexistent.ts",
|
|
159
|
+
workspaceRoot: dir,
|
|
160
|
+
astEngine: engine,
|
|
161
|
+
sarifStore: store,
|
|
162
|
+
cyclomaticMax: 15,
|
|
163
|
+
}),
|
|
164
|
+
/not found|ENOENT/i,
|
|
165
|
+
);
|
|
166
|
+
} finally {
|
|
167
|
+
rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("rejects path traversal attempts", async () => {
|
|
172
|
+
const dir = makeTmpDir();
|
|
173
|
+
try {
|
|
174
|
+
const store = new SarifStore({
|
|
175
|
+
workspaceRoot: dir,
|
|
176
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
177
|
+
});
|
|
178
|
+
await assert.rejects(
|
|
179
|
+
buildFileDetail({
|
|
180
|
+
relativePath: "../../etc/passwd",
|
|
181
|
+
workspaceRoot: dir,
|
|
182
|
+
astEngine: engine,
|
|
183
|
+
sarifStore: store,
|
|
184
|
+
cyclomaticMax: 15,
|
|
185
|
+
}),
|
|
186
|
+
/escapes the workspace/i,
|
|
187
|
+
);
|
|
188
|
+
} finally {
|
|
189
|
+
rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns empty functions for unsupported languages", async () => {
|
|
194
|
+
const dir = makeTmpDir();
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(join(dir, "data.json"), '{"key": "value"}\n');
|
|
197
|
+
const store = new SarifStore({
|
|
198
|
+
workspaceRoot: dir,
|
|
199
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
200
|
+
});
|
|
201
|
+
const result = await buildFileDetail({
|
|
202
|
+
relativePath: "data.json",
|
|
203
|
+
workspaceRoot: dir,
|
|
204
|
+
astEngine: engine,
|
|
205
|
+
sarifStore: store,
|
|
206
|
+
cyclomaticMax: 15,
|
|
207
|
+
});
|
|
208
|
+
assert.equal(result.language, null);
|
|
209
|
+
assert.equal(result.functions.length, 0);
|
|
210
|
+
} finally {
|
|
211
|
+
rmSync(dir, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("computes summary with correct effort and complexity stats", async () => {
|
|
216
|
+
const dir = makeTmpDir();
|
|
217
|
+
try {
|
|
218
|
+
writeFileSync(join(dir, "hello.ts"), SAMPLE_TS);
|
|
219
|
+
const store = new SarifStore({
|
|
220
|
+
workspaceRoot: dir,
|
|
221
|
+
outputDir: join(dir, ".claude-crap/reports"),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const doc = wrapResultsInSarif("eslint" as never, "0.1.0", [
|
|
225
|
+
{
|
|
226
|
+
ruleId: "no-magic-numbers",
|
|
227
|
+
level: "warning",
|
|
228
|
+
message: { text: "magic number" },
|
|
229
|
+
locations: [{
|
|
230
|
+
physicalLocation: {
|
|
231
|
+
artifactLocation: { uri: "hello.ts" },
|
|
232
|
+
region: { startLine: 6, startColumn: 7 },
|
|
233
|
+
},
|
|
234
|
+
}],
|
|
235
|
+
properties: { sourceTool: "eslint", effortMinutes: 30 },
|
|
236
|
+
},
|
|
237
|
+
]);
|
|
238
|
+
store.ingestRun(doc, "eslint");
|
|
239
|
+
|
|
240
|
+
const result = await buildFileDetail({
|
|
241
|
+
relativePath: "hello.ts",
|
|
242
|
+
workspaceRoot: dir,
|
|
243
|
+
astEngine: engine,
|
|
244
|
+
sarifStore: store,
|
|
245
|
+
cyclomaticMax: 15,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
assert.equal(result.summary.totalFindings, 1);
|
|
249
|
+
assert.equal(result.summary.warningCount, 1);
|
|
250
|
+
assert.equal(result.summary.errorCount, 0);
|
|
251
|
+
assert.ok(result.summary.totalEffortMinutes >= 30);
|
|
252
|
+
assert.ok(result.summary.maxComplexity >= 4); // classify has CC>=4
|
|
253
|
+
assert.ok(result.summary.avgComplexity > 0);
|
|
254
|
+
} finally {
|
|
255
|
+
rmSync(dir, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -90,24 +90,34 @@ describe("detectScanners", () => {
|
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
it("detects eslint from package.json
|
|
93
|
+
it("detects eslint from package.json — not installed vs installed", async () => {
|
|
94
94
|
const dir = makeTmpDir();
|
|
95
95
|
try {
|
|
96
96
|
writeFileSync(
|
|
97
97
|
join(dir, "package.json"),
|
|
98
98
|
JSON.stringify({ devDependencies: { eslint: "^9.0.0" } }),
|
|
99
99
|
);
|
|
100
|
+
// Without node_modules/.bin/eslint — declared but not installed
|
|
100
101
|
const results = await detectScanners(dir);
|
|
101
102
|
const eslint = results.find((r) => r.scanner === "eslint");
|
|
102
103
|
assert.ok(eslint);
|
|
103
|
-
assert.equal(eslint.available,
|
|
104
|
-
assert.ok(eslint.reason.includes("
|
|
104
|
+
assert.equal(eslint.available, false);
|
|
105
|
+
assert.ok(eslint.reason.includes("not installed"));
|
|
106
|
+
|
|
107
|
+
// With binary present — installed
|
|
108
|
+
mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true });
|
|
109
|
+
writeFileSync(join(dir, "node_modules", ".bin", "eslint"), "");
|
|
110
|
+
const results2 = await detectScanners(dir);
|
|
111
|
+
const eslint2 = results2.find((r) => r.scanner === "eslint");
|
|
112
|
+
assert.ok(eslint2);
|
|
113
|
+
assert.equal(eslint2.available, true);
|
|
114
|
+
assert.ok(eslint2.reason.includes("installed"));
|
|
105
115
|
} finally {
|
|
106
116
|
rmSync(dir, { recursive: true, force: true });
|
|
107
117
|
}
|
|
108
118
|
});
|
|
109
119
|
|
|
110
|
-
it("detects stryker from package.json
|
|
120
|
+
it("detects stryker from package.json — not installed vs installed", async () => {
|
|
111
121
|
const dir = makeTmpDir();
|
|
112
122
|
try {
|
|
113
123
|
writeFileSync(
|
|
@@ -116,11 +126,21 @@ describe("detectScanners", () => {
|
|
|
116
126
|
devDependencies: { "@stryker-mutator/core": "^7.0.0" },
|
|
117
127
|
}),
|
|
118
128
|
);
|
|
129
|
+
// Without binary — declared but not installed
|
|
119
130
|
const results = await detectScanners(dir);
|
|
120
131
|
const stryker = results.find((r) => r.scanner === "stryker");
|
|
121
132
|
assert.ok(stryker);
|
|
122
|
-
assert.equal(stryker.available,
|
|
123
|
-
assert.ok(stryker.reason.includes("
|
|
133
|
+
assert.equal(stryker.available, false);
|
|
134
|
+
assert.ok(stryker.reason.includes("not installed"));
|
|
135
|
+
|
|
136
|
+
// With binary present — installed
|
|
137
|
+
mkdirSync(join(dir, "node_modules", ".bin"), { recursive: true });
|
|
138
|
+
writeFileSync(join(dir, "node_modules", ".bin", "stryker"), "");
|
|
139
|
+
const results2 = await detectScanners(dir);
|
|
140
|
+
const stryker2 = results2.find((r) => r.scanner === "stryker");
|
|
141
|
+
assert.ok(stryker2);
|
|
142
|
+
assert.equal(stryker2.available, true);
|
|
143
|
+
assert.ok(stryker2.reason.includes("installed"));
|
|
124
144
|
} finally {
|
|
125
145
|
rmSync(dir, { recursive: true, force: true });
|
|
126
146
|
}
|
|
@@ -133,9 +153,9 @@ describe("detectScanners", () => {
|
|
|
133
153
|
// Config and package.json probes will all fail.
|
|
134
154
|
// Binary probe results depend on the host — don't assert on those,
|
|
135
155
|
// but do assert the structure is correct.
|
|
136
|
-
assert.equal(results.length,
|
|
156
|
+
assert.equal(results.length, 5);
|
|
137
157
|
for (const r of results) {
|
|
138
|
-
assert.ok(["eslint", "semgrep", "bandit", "stryker"].includes(r.scanner));
|
|
158
|
+
assert.ok(["eslint", "semgrep", "bandit", "stryker", "dart_analyze"].includes(r.scanner));
|
|
139
159
|
assert.equal(typeof r.available, "boolean");
|
|
140
160
|
assert.equal(typeof r.reason, "string");
|
|
141
161
|
}
|
|
@@ -150,7 +170,7 @@ describe("detectScanners", () => {
|
|
|
150
170
|
writeFileSync(join(dir, "package.json"), "not json at all");
|
|
151
171
|
// Should not throw — just skip the package.json probe
|
|
152
172
|
const results = await detectScanners(dir);
|
|
153
|
-
assert.equal(results.length,
|
|
173
|
+
assert.equal(results.length, 5);
|
|
154
174
|
} finally {
|
|
155
175
|
rmSync(dir, { recursive: true, force: true });
|
|
156
176
|
}
|
|
@@ -172,10 +192,10 @@ describe("detectScanners", () => {
|
|
|
172
192
|
}
|
|
173
193
|
});
|
|
174
194
|
|
|
175
|
-
it("SCANNER_SIGNALS covers all
|
|
195
|
+
it("SCANNER_SIGNALS covers all supported scanners", () => {
|
|
176
196
|
assert.deepEqual(
|
|
177
197
|
Object.keys(SCANNER_SIGNALS).sort(),
|
|
178
|
-
["bandit", "eslint", "semgrep", "stryker"],
|
|
198
|
+
["bandit", "dart_analyze", "eslint", "semgrep", "stryker"],
|
|
179
199
|
);
|
|
180
200
|
});
|
|
181
201
|
});
|