claude-crap 0.1.2
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 +308 -0
- package/LICENSE +21 -0
- package/README.md +550 -0
- package/bin/claude-crap.mjs +141 -0
- package/dist/adapters/bandit.d.ts +48 -0
- package/dist/adapters/bandit.d.ts.map +1 -0
- package/dist/adapters/bandit.js +145 -0
- package/dist/adapters/bandit.js.map +1 -0
- package/dist/adapters/common.d.ts +73 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +78 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/eslint.d.ts +52 -0
- package/dist/adapters/eslint.d.ts.map +1 -0
- package/dist/adapters/eslint.js +142 -0
- package/dist/adapters/eslint.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +64 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/semgrep.d.ts +30 -0
- package/dist/adapters/semgrep.d.ts.map +1 -0
- package/dist/adapters/semgrep.js +130 -0
- package/dist/adapters/semgrep.js.map +1 -0
- package/dist/adapters/stryker.d.ts +55 -0
- package/dist/adapters/stryker.d.ts.map +1 -0
- package/dist/adapters/stryker.js +165 -0
- package/dist/adapters/stryker.js.map +1 -0
- package/dist/ast/cyclomatic.d.ts +48 -0
- package/dist/ast/cyclomatic.d.ts.map +1 -0
- package/dist/ast/cyclomatic.js +106 -0
- package/dist/ast/cyclomatic.js.map +1 -0
- package/dist/ast/index.d.ts +26 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +23 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/ast/language-config.d.ts +70 -0
- package/dist/ast/language-config.d.ts.map +1 -0
- package/dist/ast/language-config.js +192 -0
- package/dist/ast/language-config.js.map +1 -0
- package/dist/ast/tree-sitter-engine.d.ts +133 -0
- package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
- package/dist/ast/tree-sitter-engine.js +270 -0
- package/dist/ast/tree-sitter-engine.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/crap-config.d.ts +97 -0
- package/dist/crap-config.d.ts.map +1 -0
- package/dist/crap-config.js +144 -0
- package/dist/crap-config.js.map +1 -0
- package/dist/dashboard/server.d.ts +65 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +147 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +574 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/crap.d.ts +71 -0
- package/dist/metrics/crap.d.ts.map +1 -0
- package/dist/metrics/crap.js +67 -0
- package/dist/metrics/crap.js.map +1 -0
- package/dist/metrics/index.d.ts +31 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +27 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/score.d.ts +143 -0
- package/dist/metrics/score.d.ts.map +1 -0
- package/dist/metrics/score.js +224 -0
- package/dist/metrics/score.js.map +1 -0
- package/dist/metrics/tdr.d.ts +106 -0
- package/dist/metrics/tdr.d.ts.map +1 -0
- package/dist/metrics/tdr.js +117 -0
- package/dist/metrics/tdr.js.map +1 -0
- package/dist/metrics/workspace-walker.d.ts +43 -0
- package/dist/metrics/workspace-walker.d.ts.map +1 -0
- package/dist/metrics/workspace-walker.js +137 -0
- package/dist/metrics/workspace-walker.js.map +1 -0
- package/dist/sarif/index.d.ts +21 -0
- package/dist/sarif/index.d.ts.map +1 -0
- package/dist/sarif/index.js +19 -0
- package/dist/sarif/index.js.map +1 -0
- package/dist/sarif/sarif-builder.d.ts +128 -0
- package/dist/sarif/sarif-builder.d.ts.map +1 -0
- package/dist/sarif/sarif-builder.js +79 -0
- package/dist/sarif/sarif-builder.js.map +1 -0
- package/dist/sarif/sarif-store.d.ts +205 -0
- package/dist/sarif/sarif-store.d.ts.map +1 -0
- package/dist/sarif/sarif-store.js +246 -0
- package/dist/sarif/sarif-store.js.map +1 -0
- package/dist/sarif/sarif-validator.d.ts +45 -0
- package/dist/sarif/sarif-validator.d.ts.map +1 -0
- package/dist/sarif/sarif-validator.js +138 -0
- package/dist/sarif/sarif-validator.js.map +1 -0
- package/dist/schemas/tool-schemas.d.ts +216 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -0
- package/dist/schemas/tool-schemas.js +208 -0
- package/dist/schemas/tool-schemas.js.map +1 -0
- package/dist/sdk.d.ts +45 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +44 -0
- package/dist/sdk.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/test-harness.d.ts +75 -0
- package/dist/tools/test-harness.d.ts.map +1 -0
- package/dist/tools/test-harness.js +137 -0
- package/dist/tools/test-harness.js.map +1 -0
- package/dist/workspace-guard.d.ts +53 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +61 -0
- package/dist/workspace-guard.js.map +1 -0
- package/package.json +133 -0
- package/plugin/.claude-plugin/plugin.json +29 -0
- package/plugin/.mcp.json +18 -0
- package/plugin/CLAUDE.md +143 -0
- package/plugin/bundle/dashboard/public/index.html +368 -0
- package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/plugin/bundle/mcp-server.mjs +8718 -0
- package/plugin/bundle/mcp-server.mjs.map +7 -0
- package/plugin/bundle/tdr-engine.mjs +50 -0
- package/plugin/bundle/tdr-engine.mjs.map +7 -0
- package/plugin/hooks/hooks.json +62 -0
- package/plugin/hooks/lib/crap-config.mjs +152 -0
- package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
- package/plugin/hooks/lib/hook-io.mjs +151 -0
- package/plugin/hooks/lib/quality-gate.mjs +329 -0
- package/plugin/hooks/lib/test-harness.mjs +152 -0
- package/plugin/hooks/post-tool-use.mjs +245 -0
- package/plugin/hooks/pre-tool-use.mjs +290 -0
- package/plugin/hooks/session-start.mjs +109 -0
- package/plugin/hooks/stop-quality-gate.mjs +226 -0
- package/plugin/package.json +18 -0
- package/plugin/skills/adopt/SKILL.md +74 -0
- package/plugin/skills/analyze/SKILL.md +77 -0
- package/plugin/skills/check-test/SKILL.md +50 -0
- package/plugin/skills/score/SKILL.md +31 -0
- package/scripts/bug-report.mjs +328 -0
- package/scripts/build-fast.mjs +130 -0
- package/scripts/bundle-plugin.mjs +74 -0
- package/scripts/doctor.mjs +320 -0
- package/scripts/install.mjs +192 -0
- package/scripts/lib/cli-ui.mjs +122 -0
- package/scripts/postinstall.mjs +127 -0
- package/scripts/run-tests.mjs +95 -0
- package/scripts/status.mjs +110 -0
- package/scripts/uninstall.mjs +72 -0
- package/src/adapters/bandit.ts +191 -0
- package/src/adapters/common.ts +133 -0
- package/src/adapters/eslint.ts +187 -0
- package/src/adapters/index.ts +78 -0
- package/src/adapters/semgrep.ts +150 -0
- package/src/adapters/stryker.ts +218 -0
- package/src/ast/cyclomatic.ts +131 -0
- package/src/ast/index.ts +33 -0
- package/src/ast/language-config.ts +231 -0
- package/src/ast/tree-sitter-engine.ts +385 -0
- package/src/config.ts +109 -0
- package/src/crap-config.ts +196 -0
- package/src/dashboard/public/index.html +368 -0
- package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/src/dashboard/server.ts +205 -0
- package/src/index.ts +696 -0
- package/src/metrics/crap.ts +101 -0
- package/src/metrics/index.ts +51 -0
- package/src/metrics/score.ts +329 -0
- package/src/metrics/tdr.ts +155 -0
- package/src/metrics/workspace-walker.ts +146 -0
- package/src/sarif/index.ts +31 -0
- package/src/sarif/sarif-builder.ts +139 -0
- package/src/sarif/sarif-store.ts +347 -0
- package/src/sarif/sarif-validator.ts +145 -0
- package/src/schemas/tool-schemas.ts +225 -0
- package/src/sdk.ts +110 -0
- package/src/tests/adapters/bandit.test.ts +111 -0
- package/src/tests/adapters/dispatch.test.ts +100 -0
- package/src/tests/adapters/eslint.test.ts +138 -0
- package/src/tests/adapters/semgrep.test.ts +125 -0
- package/src/tests/adapters/stryker.test.ts +103 -0
- package/src/tests/crap-config.test.ts +228 -0
- package/src/tests/crap.test.ts +59 -0
- package/src/tests/cyclomatic.test.ts +87 -0
- package/src/tests/dashboard-http.test.ts +108 -0
- package/src/tests/dashboard-integrity.test.ts +128 -0
- package/src/tests/integration/mcp-server.integration.test.ts +352 -0
- package/src/tests/pre-tool-use-hook.test.ts +178 -0
- package/src/tests/sarif-store.test.ts +241 -0
- package/src/tests/sarif-validator.test.ts +164 -0
- package/src/tests/score.test.ts +260 -0
- package/src/tests/skills-frontmatter.test.ts +172 -0
- package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
- package/src/tests/tdr.test.ts +86 -0
- package/src/tests/test-harness.test.ts +153 -0
- package/src/tests/workspace-guard.test.ts +111 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/test-harness.ts +158 -0
- package/src/workspace-guard.ts +64 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stryker (JavaScript mutation testing) adapter.
|
|
3
|
+
*
|
|
4
|
+
* Stryker emits a JSON report under `reports/mutation/mutation.json`.
|
|
5
|
+
* The report shape is documented at
|
|
6
|
+
* https://stryker-mutator.io/docs/mutation-testing-elements/mutant-result-schema/ :
|
|
7
|
+
*
|
|
8
|
+
* {
|
|
9
|
+
* "schemaVersion": "1.0",
|
|
10
|
+
* "thresholds": { ... },
|
|
11
|
+
* "files": {
|
|
12
|
+
* "src/foo.ts": {
|
|
13
|
+
* "language": "typescript",
|
|
14
|
+
* "source": "...",
|
|
15
|
+
* "mutants": [
|
|
16
|
+
* {
|
|
17
|
+
* "id": "1",
|
|
18
|
+
* "mutatorName": "ConditionalExpression",
|
|
19
|
+
* "replacement": "false",
|
|
20
|
+
* "location": {
|
|
21
|
+
* "start": { "line": 10, "column": 5 },
|
|
22
|
+
* "end": { "line": 10, "column": 15 }
|
|
23
|
+
* },
|
|
24
|
+
* "status": "Survived", // Killed | Survived | Timeout | NoCoverage | RuntimeError | CompileError | Ignored
|
|
25
|
+
* "statusReason": "..."
|
|
26
|
+
* }
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* This adapter treats every **surviving mutant** as a SARIF
|
|
33
|
+
* `error`-level finding — surviving mutants are exactly the ones the
|
|
34
|
+
* Golden Rule forbids, because they prove the test suite does not
|
|
35
|
+
* pin the code's behavior tightly enough to notice a change.
|
|
36
|
+
*
|
|
37
|
+
* Mutants with status `NoCoverage` become `warning`-level findings
|
|
38
|
+
* (not blocking the Stop gate by themselves, but still ingested so
|
|
39
|
+
* the dashboard can surface uncovered lines). All other statuses are
|
|
40
|
+
* ignored — they do not represent defects.
|
|
41
|
+
*
|
|
42
|
+
* @module adapters/stryker
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import type { SarifLevel } from "../sarif/sarif-builder.js";
|
|
46
|
+
import {
|
|
47
|
+
estimateEffortMinutes,
|
|
48
|
+
wrapResultsInSarif,
|
|
49
|
+
type AdapterResult,
|
|
50
|
+
type KnownScanner,
|
|
51
|
+
} from "./common.js";
|
|
52
|
+
|
|
53
|
+
const STRYKER: KnownScanner = "stryker";
|
|
54
|
+
|
|
55
|
+
interface StrykerReport {
|
|
56
|
+
readonly schemaVersion?: string;
|
|
57
|
+
readonly files?: Record<string, StrykerFileReport>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface StrykerFileReport {
|
|
61
|
+
readonly language?: string;
|
|
62
|
+
readonly mutants?: ReadonlyArray<StrykerMutant>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface StrykerMutant {
|
|
66
|
+
readonly id?: string;
|
|
67
|
+
readonly mutatorName?: string;
|
|
68
|
+
readonly replacement?: string;
|
|
69
|
+
readonly location?: {
|
|
70
|
+
readonly start?: { readonly line?: number; readonly column?: number };
|
|
71
|
+
readonly end?: { readonly line?: number; readonly column?: number };
|
|
72
|
+
};
|
|
73
|
+
readonly status?: string;
|
|
74
|
+
readonly statusReason?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Accept a Stryker JSON mutation report and return a normalized
|
|
79
|
+
* `PersistedSarif` document with one finding per surviving mutant
|
|
80
|
+
* and per uncovered mutant.
|
|
81
|
+
*
|
|
82
|
+
* @param input Raw Stryker report (string or parsed object).
|
|
83
|
+
* @returns Adapter result.
|
|
84
|
+
* @throws When the input is not a Stryker mutation report.
|
|
85
|
+
*/
|
|
86
|
+
export function adaptStryker(input: unknown): AdapterResult {
|
|
87
|
+
const parsed = typeof input === "string" ? (JSON.parse(input) as unknown) : input;
|
|
88
|
+
if (!parsed || typeof parsed !== "object") {
|
|
89
|
+
throw new Error(`[adapter:stryker] expected a JSON object`);
|
|
90
|
+
}
|
|
91
|
+
const report = parsed as StrykerReport;
|
|
92
|
+
if (!report.files || typeof report.files !== "object") {
|
|
93
|
+
throw new Error(`[adapter:stryker] report is missing a files{} map`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const results: Array<ReturnType<typeof buildSarifResult>> = [];
|
|
97
|
+
let totalEffortMinutes = 0;
|
|
98
|
+
|
|
99
|
+
for (const [filename, fileReport] of Object.entries(report.files)) {
|
|
100
|
+
const mutants = Array.isArray(fileReport?.mutants) ? fileReport.mutants : [];
|
|
101
|
+
for (const mutant of mutants) {
|
|
102
|
+
const level = classifyMutant(mutant.status);
|
|
103
|
+
if (level === null) continue; // Killed / Ignored / CompileError — not a defect
|
|
104
|
+
const startLine = mutant.location?.start?.line ?? 1;
|
|
105
|
+
const startColumn = mutant.location?.start?.column ?? 1;
|
|
106
|
+
const endLine = mutant.location?.end?.line;
|
|
107
|
+
const endColumn = mutant.location?.end?.column;
|
|
108
|
+
|
|
109
|
+
// Surviving mutants cost more to fix than the default error
|
|
110
|
+
// budget because the agent has to first write a killing test,
|
|
111
|
+
// THEN possibly rewrite the code.
|
|
112
|
+
const effortOverride = level === "error" ? 45 : 15;
|
|
113
|
+
const effort = estimateEffortMinutes(level, effortOverride);
|
|
114
|
+
totalEffortMinutes += effort;
|
|
115
|
+
|
|
116
|
+
const mutator = mutant.mutatorName ?? "Unknown";
|
|
117
|
+
const ruleId = `stryker.${mutator}`;
|
|
118
|
+
const statusText = mutant.status ?? "Unknown";
|
|
119
|
+
const message =
|
|
120
|
+
`${statusText}: ${mutator} mutant on '${mutant.replacement ?? "?"}'` +
|
|
121
|
+
(mutant.statusReason ? ` — ${mutant.statusReason}` : "");
|
|
122
|
+
|
|
123
|
+
results.push(
|
|
124
|
+
buildSarifResult({
|
|
125
|
+
ruleId,
|
|
126
|
+
level,
|
|
127
|
+
message,
|
|
128
|
+
uri: filename,
|
|
129
|
+
startLine,
|
|
130
|
+
startColumn,
|
|
131
|
+
endLine: typeof endLine === "number" ? endLine : undefined,
|
|
132
|
+
endColumn: typeof endColumn === "number" ? endColumn : undefined,
|
|
133
|
+
effortMinutes: effort,
|
|
134
|
+
mutantId: mutant.id,
|
|
135
|
+
mutator,
|
|
136
|
+
mutantStatus: statusText,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
document: wrapResultsInSarif(STRYKER, String(report.schemaVersion ?? "unknown"), results),
|
|
144
|
+
sourceTool: STRYKER,
|
|
145
|
+
findingCount: results.length,
|
|
146
|
+
totalEffortMinutes,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Classify a Stryker mutant status into a SARIF level, or `null` when
|
|
152
|
+
* the status represents a mutant that was correctly handled by the
|
|
153
|
+
* test suite and should not produce a finding.
|
|
154
|
+
*/
|
|
155
|
+
function classifyMutant(status: string | undefined): SarifLevel | null {
|
|
156
|
+
switch ((status ?? "").toLowerCase()) {
|
|
157
|
+
case "survived":
|
|
158
|
+
return "error";
|
|
159
|
+
case "nocoverage":
|
|
160
|
+
return "warning";
|
|
161
|
+
case "timeout":
|
|
162
|
+
// Timeout mutants are suspicious but not proof of defect —
|
|
163
|
+
// they deserve a note so the dashboard highlights them.
|
|
164
|
+
return "note";
|
|
165
|
+
case "killed":
|
|
166
|
+
case "ignored":
|
|
167
|
+
case "compileerror":
|
|
168
|
+
case "runtimeerror":
|
|
169
|
+
default:
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build the SARIF `result` object for a single Stryker mutant. We
|
|
176
|
+
* propagate the mutant id, mutator name, and raw status into
|
|
177
|
+
* `properties` so the dashboard can display them as tags.
|
|
178
|
+
*/
|
|
179
|
+
function buildSarifResult(opts: {
|
|
180
|
+
ruleId: string;
|
|
181
|
+
level: SarifLevel;
|
|
182
|
+
message: string;
|
|
183
|
+
uri: string;
|
|
184
|
+
startLine: number;
|
|
185
|
+
startColumn: number;
|
|
186
|
+
endLine?: number | undefined;
|
|
187
|
+
endColumn?: number | undefined;
|
|
188
|
+
effortMinutes: number;
|
|
189
|
+
mutantId?: string | undefined;
|
|
190
|
+
mutator?: string | undefined;
|
|
191
|
+
mutantStatus?: string | undefined;
|
|
192
|
+
}) {
|
|
193
|
+
return {
|
|
194
|
+
ruleId: opts.ruleId,
|
|
195
|
+
level: opts.level,
|
|
196
|
+
message: { text: opts.message },
|
|
197
|
+
locations: [
|
|
198
|
+
{
|
|
199
|
+
physicalLocation: {
|
|
200
|
+
artifactLocation: { uri: opts.uri },
|
|
201
|
+
region: {
|
|
202
|
+
startLine: opts.startLine,
|
|
203
|
+
startColumn: opts.startColumn,
|
|
204
|
+
...(opts.endLine !== undefined ? { endLine: opts.endLine } : {}),
|
|
205
|
+
...(opts.endColumn !== undefined ? { endColumn: opts.endColumn } : {}),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
properties: {
|
|
211
|
+
sourceTool: STRYKER,
|
|
212
|
+
effortMinutes: opts.effortMinutes,
|
|
213
|
+
...(opts.mutantId ? { mutantId: opts.mutantId } : {}),
|
|
214
|
+
...(opts.mutator ? { mutator: opts.mutator } : {}),
|
|
215
|
+
...(opts.mutantStatus ? { mutantStatus: opts.mutantStatus } : {}),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic cyclomatic complexity walker.
|
|
3
|
+
*
|
|
4
|
+
* Given a tree-sitter subtree rooted at a function node, this module walks
|
|
5
|
+
* the tree and returns the McCabe cyclomatic complexity number. The
|
|
6
|
+
* algorithm is the standard "1 + (branching nodes + short-circuit operators)"
|
|
7
|
+
* formulation:
|
|
8
|
+
*
|
|
9
|
+
* CC = 1
|
|
10
|
+
* + count of branching-statement nodes (if, while, for, case, catch, ...)
|
|
11
|
+
* + count of short-circuit operators (&&, ||, ??, `and`, `or`)
|
|
12
|
+
* + count of ternary expressions (counted as branching nodes)
|
|
13
|
+
*
|
|
14
|
+
* The baseline of 1 represents the straight-line path through the function.
|
|
15
|
+
* Every additional branching point multiplies the set of reachable paths.
|
|
16
|
+
*
|
|
17
|
+
* The walker is deliberately language-agnostic: it consults the
|
|
18
|
+
* {@link LanguageConfig} passed in to decide which node types to count.
|
|
19
|
+
* Nested functions inside the subtree are **skipped** — each function's
|
|
20
|
+
* complexity is reported independently by {@link TreeSitterEngine}.
|
|
21
|
+
*
|
|
22
|
+
* @module ast/cyclomatic
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { LanguageConfig } from "./language-config.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Minimal structural contract of a tree-sitter node. We intentionally
|
|
29
|
+
* avoid importing `web-tree-sitter` types here so this module stays
|
|
30
|
+
* unit-testable with a hand-rolled mock tree.
|
|
31
|
+
*/
|
|
32
|
+
export interface AstNode {
|
|
33
|
+
/** Node type name from the grammar (e.g. `"if_statement"`). */
|
|
34
|
+
readonly type: string;
|
|
35
|
+
/** Raw source text for operator detection. May be large — do not log. */
|
|
36
|
+
readonly text: string;
|
|
37
|
+
/** Zero-based child count. */
|
|
38
|
+
readonly childCount: number;
|
|
39
|
+
/** Retrieve a child by index. Returns `null` if out of range. */
|
|
40
|
+
child(index: number): AstNode | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute the cyclomatic complexity of a function subtree.
|
|
45
|
+
*
|
|
46
|
+
* @param root Node rooted at the function (method, arrow, lambda, ...).
|
|
47
|
+
* @param languageConfig Language classification tables for node types.
|
|
48
|
+
* @returns The McCabe cyclomatic complexity (always ≥ 1).
|
|
49
|
+
*/
|
|
50
|
+
export function computeCyclomaticComplexity(root: AstNode, languageConfig: LanguageConfig): number {
|
|
51
|
+
let complexity = 1;
|
|
52
|
+
walk(root, languageConfig, true, (node) => {
|
|
53
|
+
if (languageConfig.branchingNodeTypes.has(node.type)) {
|
|
54
|
+
complexity += 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Boolean / short-circuit operators are usually represented as
|
|
58
|
+
// "binary_expression" nodes with an operator token child. To avoid
|
|
59
|
+
// coupling to a specific grammar's node shape, we inspect the raw
|
|
60
|
+
// text of the node's direct operator child.
|
|
61
|
+
if (isBooleanExpression(node, languageConfig)) {
|
|
62
|
+
complexity += 1;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return complexity;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Depth-first walk that skips any nested function subtree so that its
|
|
71
|
+
* complexity is not attributed to the enclosing function.
|
|
72
|
+
*
|
|
73
|
+
* @param node Current node being visited.
|
|
74
|
+
* @param languageConfig Language tables used to detect nested functions.
|
|
75
|
+
* @param isRoot `true` for the starting node (we do not skip it).
|
|
76
|
+
* @param visit Callback invoked for every non-function descendant.
|
|
77
|
+
*/
|
|
78
|
+
function walk(
|
|
79
|
+
node: AstNode,
|
|
80
|
+
languageConfig: LanguageConfig,
|
|
81
|
+
isRoot: boolean,
|
|
82
|
+
visit: (n: AstNode) => void,
|
|
83
|
+
): void {
|
|
84
|
+
if (!isRoot && languageConfig.functionNodeTypes.has(node.type)) {
|
|
85
|
+
// Nested function — stop the walk here. Its complexity is reported
|
|
86
|
+
// separately when the engine iterates the top-level function list.
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
visit(node);
|
|
90
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
91
|
+
const child = node.child(i);
|
|
92
|
+
if (child) walk(child, languageConfig, false, visit);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return `true` when `node` is a boolean / short-circuit expression that
|
|
98
|
+
* should add one to the cyclomatic complexity. We inspect the node's
|
|
99
|
+
* immediate children for an operator token whose text matches one of the
|
|
100
|
+
* language's short-circuit operators.
|
|
101
|
+
*
|
|
102
|
+
* This is a heuristic — grammars differ in how they represent operators
|
|
103
|
+
* — but it is stable enough for the five supported languages because:
|
|
104
|
+
*
|
|
105
|
+
* - JavaScript / TypeScript / Java: binary expression with `"&&"` or `"||"` token child.
|
|
106
|
+
* - Python: `boolean_operator` node with `"and"` / `"or"` token child.
|
|
107
|
+
* - C#: binary expression with `"&&"`, `"||"`, or `"??"` token child.
|
|
108
|
+
*
|
|
109
|
+
* @param node Candidate node.
|
|
110
|
+
* @param languageConfig Tables with the language's boolean operator set.
|
|
111
|
+
* @returns `true` when the node is a counted boolean expression.
|
|
112
|
+
*/
|
|
113
|
+
function isBooleanExpression(node: AstNode, languageConfig: LanguageConfig): boolean {
|
|
114
|
+
// Common type names across supported grammars. We check the type first
|
|
115
|
+
// to avoid scanning text for every node in the tree.
|
|
116
|
+
if (
|
|
117
|
+
node.type !== "binary_expression" &&
|
|
118
|
+
node.type !== "boolean_operator" &&
|
|
119
|
+
node.type !== "logical_expression"
|
|
120
|
+
) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
124
|
+
const child = node.child(i);
|
|
125
|
+
if (!child) continue;
|
|
126
|
+
if (languageConfig.booleanOperators.includes(child.text)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
package/src/ast/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public SDK entry point for the tree-sitter based AST engine and the
|
|
3
|
+
* cyclomatic complexity walker.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import {
|
|
9
|
+
* TreeSitterEngine,
|
|
10
|
+
* computeCyclomaticComplexity,
|
|
11
|
+
* detectLanguageFromPath,
|
|
12
|
+
* type FileMetrics,
|
|
13
|
+
* type FunctionMetrics,
|
|
14
|
+
* type SupportedLanguage,
|
|
15
|
+
* } from "claude-crap/ast";
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @module ast
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export { TreeSitterEngine } from "./tree-sitter-engine.js";
|
|
22
|
+
export type {
|
|
23
|
+
AnalyzeFileRequest,
|
|
24
|
+
FileMetrics,
|
|
25
|
+
FunctionMetrics,
|
|
26
|
+
TreeSitterEngineOptions,
|
|
27
|
+
} from "./tree-sitter-engine.js";
|
|
28
|
+
|
|
29
|
+
export { computeCyclomaticComplexity } from "./cyclomatic.js";
|
|
30
|
+
export type { AstNode } from "./cyclomatic.js";
|
|
31
|
+
|
|
32
|
+
export { LANGUAGE_TABLE, detectLanguageFromPath } from "./language-config.js";
|
|
33
|
+
export type { LanguageConfig, SupportedLanguage } from "./language-config.js";
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-language tree-sitter node classification tables.
|
|
3
|
+
*
|
|
4
|
+
* Every language grammar exposes a different set of node type names. To
|
|
5
|
+
* keep the AST engine language-agnostic we encode, for each supported
|
|
6
|
+
* language, three sets:
|
|
7
|
+
*
|
|
8
|
+
* - `functionNodeTypes` — nodes that represent a function/method/lambda.
|
|
9
|
+
* These are the units we report metrics for.
|
|
10
|
+
* - `branchingNodeTypes` — nodes that add one independent path through
|
|
11
|
+
* the function. Used to compute cyclomatic
|
|
12
|
+
* complexity by counting occurrences.
|
|
13
|
+
* - `nameField` — the tree-sitter field name that holds the
|
|
14
|
+
* function's identifier, used to extract the
|
|
15
|
+
* function name for reporting.
|
|
16
|
+
*
|
|
17
|
+
* We also define which WASM grammar file to load per language. The paths
|
|
18
|
+
* are resolved at runtime against the `tree-sitter-wasms` package, but
|
|
19
|
+
* can be overridden via the engine constructor if you want to ship your
|
|
20
|
+
* own grammars.
|
|
21
|
+
*
|
|
22
|
+
* @module ast/language-config
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Languages currently supported by the AST engine. This is the same
|
|
27
|
+
* `enum` that appears in the `analyze_file_ast` tool schema — keep them
|
|
28
|
+
* in sync when adding a new language.
|
|
29
|
+
*/
|
|
30
|
+
export type SupportedLanguage = "csharp" | "javascript" | "typescript" | "python" | "java";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-language classification record. Immutable by convention.
|
|
34
|
+
*/
|
|
35
|
+
export interface LanguageConfig {
|
|
36
|
+
/** Canonical language identifier (stable across releases). */
|
|
37
|
+
readonly id: SupportedLanguage;
|
|
38
|
+
/** WASM grammar filename inside `tree-sitter-wasms/out/`. */
|
|
39
|
+
readonly wasmName: string;
|
|
40
|
+
/** File extensions that should map to this language. */
|
|
41
|
+
readonly extensions: ReadonlyArray<string>;
|
|
42
|
+
/** Tree-sitter node types that represent callable units. */
|
|
43
|
+
readonly functionNodeTypes: ReadonlySet<string>;
|
|
44
|
+
/** Tree-sitter node types that add +1 to cyclomatic complexity. */
|
|
45
|
+
readonly branchingNodeTypes: ReadonlySet<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Boolean / short-circuit operator node types. These are counted only
|
|
48
|
+
* when the node is an `"&&"`, `"||"`, `"??"` (etc.) operator, so the
|
|
49
|
+
* walker inspects the operator text on top of the node type.
|
|
50
|
+
*/
|
|
51
|
+
readonly booleanOperators: ReadonlyArray<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Child-field names we try in order to extract the function name. The
|
|
54
|
+
* walker reads the first non-empty match.
|
|
55
|
+
*/
|
|
56
|
+
readonly nameFieldCandidates: ReadonlyArray<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// -----------------------------------------------------------------------------
|
|
60
|
+
// C#
|
|
61
|
+
// -----------------------------------------------------------------------------
|
|
62
|
+
// Grammar: https://github.com/tree-sitter/tree-sitter-c-sharp
|
|
63
|
+
const CSHARP: LanguageConfig = {
|
|
64
|
+
id: "csharp",
|
|
65
|
+
wasmName: "tree-sitter-c_sharp.wasm",
|
|
66
|
+
extensions: [".cs"],
|
|
67
|
+
functionNodeTypes: new Set([
|
|
68
|
+
"method_declaration",
|
|
69
|
+
"local_function_statement",
|
|
70
|
+
"lambda_expression",
|
|
71
|
+
"anonymous_method_expression",
|
|
72
|
+
"constructor_declaration",
|
|
73
|
+
"destructor_declaration",
|
|
74
|
+
"operator_declaration",
|
|
75
|
+
"conversion_operator_declaration",
|
|
76
|
+
"accessor_declaration",
|
|
77
|
+
]),
|
|
78
|
+
branchingNodeTypes: new Set([
|
|
79
|
+
"if_statement",
|
|
80
|
+
"else_clause",
|
|
81
|
+
"while_statement",
|
|
82
|
+
"do_statement",
|
|
83
|
+
"for_statement",
|
|
84
|
+
"for_each_statement",
|
|
85
|
+
"case_switch_label",
|
|
86
|
+
"case_pattern_switch_label",
|
|
87
|
+
"switch_expression_arm",
|
|
88
|
+
"catch_clause",
|
|
89
|
+
"conditional_expression",
|
|
90
|
+
"conditional_access_expression",
|
|
91
|
+
"when_clause",
|
|
92
|
+
]),
|
|
93
|
+
booleanOperators: ["&&", "||", "??"],
|
|
94
|
+
nameFieldCandidates: ["name"],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// -----------------------------------------------------------------------------
|
|
98
|
+
// JavaScript
|
|
99
|
+
// -----------------------------------------------------------------------------
|
|
100
|
+
// Grammar: https://github.com/tree-sitter/tree-sitter-javascript
|
|
101
|
+
const JAVASCRIPT: LanguageConfig = {
|
|
102
|
+
id: "javascript",
|
|
103
|
+
wasmName: "tree-sitter-javascript.wasm",
|
|
104
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
105
|
+
functionNodeTypes: new Set([
|
|
106
|
+
"function_declaration",
|
|
107
|
+
"function_expression",
|
|
108
|
+
"arrow_function",
|
|
109
|
+
"method_definition",
|
|
110
|
+
"generator_function",
|
|
111
|
+
"generator_function_declaration",
|
|
112
|
+
]),
|
|
113
|
+
branchingNodeTypes: new Set([
|
|
114
|
+
"if_statement",
|
|
115
|
+
"else_clause",
|
|
116
|
+
"while_statement",
|
|
117
|
+
"do_statement",
|
|
118
|
+
"for_statement",
|
|
119
|
+
"for_in_statement",
|
|
120
|
+
"for_of_statement",
|
|
121
|
+
"switch_case",
|
|
122
|
+
"catch_clause",
|
|
123
|
+
"ternary_expression",
|
|
124
|
+
]),
|
|
125
|
+
booleanOperators: ["&&", "||", "??"],
|
|
126
|
+
nameFieldCandidates: ["name"],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// -----------------------------------------------------------------------------
|
|
130
|
+
// TypeScript
|
|
131
|
+
// -----------------------------------------------------------------------------
|
|
132
|
+
// Grammar: https://github.com/tree-sitter/tree-sitter-typescript
|
|
133
|
+
// The TypeScript grammar inherits most node types from JavaScript, so we
|
|
134
|
+
// extend the JS tables rather than re-declaring them from scratch.
|
|
135
|
+
const TYPESCRIPT: LanguageConfig = {
|
|
136
|
+
id: "typescript",
|
|
137
|
+
wasmName: "tree-sitter-typescript.wasm",
|
|
138
|
+
extensions: [".ts", ".tsx", ".mts", ".cts"],
|
|
139
|
+
functionNodeTypes: new Set([
|
|
140
|
+
...JAVASCRIPT.functionNodeTypes,
|
|
141
|
+
"function_signature",
|
|
142
|
+
"method_signature",
|
|
143
|
+
"abstract_method_signature",
|
|
144
|
+
]),
|
|
145
|
+
branchingNodeTypes: new Set([...JAVASCRIPT.branchingNodeTypes]),
|
|
146
|
+
booleanOperators: [...JAVASCRIPT.booleanOperators],
|
|
147
|
+
nameFieldCandidates: ["name"],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// -----------------------------------------------------------------------------
|
|
151
|
+
// Python
|
|
152
|
+
// -----------------------------------------------------------------------------
|
|
153
|
+
// Grammar: https://github.com/tree-sitter/tree-sitter-python
|
|
154
|
+
const PYTHON: LanguageConfig = {
|
|
155
|
+
id: "python",
|
|
156
|
+
wasmName: "tree-sitter-python.wasm",
|
|
157
|
+
extensions: [".py", ".pyi"],
|
|
158
|
+
functionNodeTypes: new Set(["function_definition", "lambda"]),
|
|
159
|
+
branchingNodeTypes: new Set([
|
|
160
|
+
"if_statement",
|
|
161
|
+
"elif_clause",
|
|
162
|
+
"else_clause",
|
|
163
|
+
"while_statement",
|
|
164
|
+
"for_statement",
|
|
165
|
+
"try_statement",
|
|
166
|
+
"except_clause",
|
|
167
|
+
"conditional_expression",
|
|
168
|
+
"match_statement",
|
|
169
|
+
"case_clause",
|
|
170
|
+
]),
|
|
171
|
+
booleanOperators: ["and", "or"],
|
|
172
|
+
nameFieldCandidates: ["name"],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// -----------------------------------------------------------------------------
|
|
176
|
+
// Java
|
|
177
|
+
// -----------------------------------------------------------------------------
|
|
178
|
+
// Grammar: https://github.com/tree-sitter/tree-sitter-java
|
|
179
|
+
const JAVA: LanguageConfig = {
|
|
180
|
+
id: "java",
|
|
181
|
+
wasmName: "tree-sitter-java.wasm",
|
|
182
|
+
extensions: [".java"],
|
|
183
|
+
functionNodeTypes: new Set([
|
|
184
|
+
"method_declaration",
|
|
185
|
+
"constructor_declaration",
|
|
186
|
+
"lambda_expression",
|
|
187
|
+
]),
|
|
188
|
+
branchingNodeTypes: new Set([
|
|
189
|
+
"if_statement",
|
|
190
|
+
"while_statement",
|
|
191
|
+
"do_statement",
|
|
192
|
+
"for_statement",
|
|
193
|
+
"enhanced_for_statement",
|
|
194
|
+
"switch_label",
|
|
195
|
+
"switch_rule",
|
|
196
|
+
"catch_clause",
|
|
197
|
+
"ternary_expression",
|
|
198
|
+
]),
|
|
199
|
+
booleanOperators: ["&&", "||"],
|
|
200
|
+
nameFieldCandidates: ["name"],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Complete language table. Look up by {@link SupportedLanguage} identifier.
|
|
205
|
+
*/
|
|
206
|
+
export const LANGUAGE_TABLE: Readonly<Record<SupportedLanguage, LanguageConfig>> = {
|
|
207
|
+
csharp: CSHARP,
|
|
208
|
+
javascript: JAVASCRIPT,
|
|
209
|
+
typescript: TYPESCRIPT,
|
|
210
|
+
python: PYTHON,
|
|
211
|
+
java: JAVA,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Infer a {@link SupportedLanguage} from a file path by matching its
|
|
216
|
+
* extension. Returns `null` when no known language matches. Useful when
|
|
217
|
+
* the caller does not already know the language and wants the engine to
|
|
218
|
+
* pick one automatically.
|
|
219
|
+
*
|
|
220
|
+
* @param filePath File path (absolute or relative).
|
|
221
|
+
* @returns The detected language or `null`.
|
|
222
|
+
*/
|
|
223
|
+
export function detectLanguageFromPath(filePath: string): SupportedLanguage | null {
|
|
224
|
+
const lower = filePath.toLowerCase();
|
|
225
|
+
for (const config of Object.values(LANGUAGE_TABLE)) {
|
|
226
|
+
for (const ext of config.extensions) {
|
|
227
|
+
if (lower.endsWith(ext)) return config.id;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|