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
|
@@ -2977,7 +2977,7 @@ var require_compile = __commonJS({
|
|
|
2977
2977
|
const schOrFunc = root.refs[ref];
|
|
2978
2978
|
if (schOrFunc)
|
|
2979
2979
|
return schOrFunc;
|
|
2980
|
-
let _sch =
|
|
2980
|
+
let _sch = resolve7.call(this, root, ref);
|
|
2981
2981
|
if (_sch === void 0) {
|
|
2982
2982
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2983
2983
|
const { schemaId } = this.opts;
|
|
@@ -3004,7 +3004,7 @@ var require_compile = __commonJS({
|
|
|
3004
3004
|
function sameSchemaEnv(s1, s2) {
|
|
3005
3005
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3006
3006
|
}
|
|
3007
|
-
function
|
|
3007
|
+
function resolve7(root, ref) {
|
|
3008
3008
|
let sch;
|
|
3009
3009
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3010
3010
|
ref = sch;
|
|
@@ -3579,55 +3579,55 @@ var require_fast_uri = __commonJS({
|
|
|
3579
3579
|
}
|
|
3580
3580
|
return uri;
|
|
3581
3581
|
}
|
|
3582
|
-
function
|
|
3582
|
+
function resolve7(baseURI, relativeURI, options) {
|
|
3583
3583
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3584
3584
|
const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3585
3585
|
schemelessOptions.skipEscape = true;
|
|
3586
3586
|
return serialize(resolved, schemelessOptions);
|
|
3587
3587
|
}
|
|
3588
|
-
function resolveComponent(base,
|
|
3588
|
+
function resolveComponent(base, relative4, options, skipNormalization) {
|
|
3589
3589
|
const target = {};
|
|
3590
3590
|
if (!skipNormalization) {
|
|
3591
3591
|
base = parse(serialize(base, options), options);
|
|
3592
|
-
|
|
3592
|
+
relative4 = parse(serialize(relative4, options), options);
|
|
3593
3593
|
}
|
|
3594
3594
|
options = options || {};
|
|
3595
|
-
if (!options.tolerant &&
|
|
3596
|
-
target.scheme =
|
|
3597
|
-
target.userinfo =
|
|
3598
|
-
target.host =
|
|
3599
|
-
target.port =
|
|
3600
|
-
target.path = removeDotSegments(
|
|
3601
|
-
target.query =
|
|
3595
|
+
if (!options.tolerant && relative4.scheme) {
|
|
3596
|
+
target.scheme = relative4.scheme;
|
|
3597
|
+
target.userinfo = relative4.userinfo;
|
|
3598
|
+
target.host = relative4.host;
|
|
3599
|
+
target.port = relative4.port;
|
|
3600
|
+
target.path = removeDotSegments(relative4.path || "");
|
|
3601
|
+
target.query = relative4.query;
|
|
3602
3602
|
} else {
|
|
3603
|
-
if (
|
|
3604
|
-
target.userinfo =
|
|
3605
|
-
target.host =
|
|
3606
|
-
target.port =
|
|
3607
|
-
target.path = removeDotSegments(
|
|
3608
|
-
target.query =
|
|
3603
|
+
if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
|
|
3604
|
+
target.userinfo = relative4.userinfo;
|
|
3605
|
+
target.host = relative4.host;
|
|
3606
|
+
target.port = relative4.port;
|
|
3607
|
+
target.path = removeDotSegments(relative4.path || "");
|
|
3608
|
+
target.query = relative4.query;
|
|
3609
3609
|
} else {
|
|
3610
|
-
if (!
|
|
3610
|
+
if (!relative4.path) {
|
|
3611
3611
|
target.path = base.path;
|
|
3612
|
-
if (
|
|
3613
|
-
target.query =
|
|
3612
|
+
if (relative4.query !== void 0) {
|
|
3613
|
+
target.query = relative4.query;
|
|
3614
3614
|
} else {
|
|
3615
3615
|
target.query = base.query;
|
|
3616
3616
|
}
|
|
3617
3617
|
} else {
|
|
3618
|
-
if (
|
|
3619
|
-
target.path = removeDotSegments(
|
|
3618
|
+
if (relative4.path[0] === "/") {
|
|
3619
|
+
target.path = removeDotSegments(relative4.path);
|
|
3620
3620
|
} else {
|
|
3621
3621
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3622
|
-
target.path = "/" +
|
|
3622
|
+
target.path = "/" + relative4.path;
|
|
3623
3623
|
} else if (!base.path) {
|
|
3624
|
-
target.path =
|
|
3624
|
+
target.path = relative4.path;
|
|
3625
3625
|
} else {
|
|
3626
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3626
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
|
|
3627
3627
|
}
|
|
3628
3628
|
target.path = removeDotSegments(target.path);
|
|
3629
3629
|
}
|
|
3630
|
-
target.query =
|
|
3630
|
+
target.query = relative4.query;
|
|
3631
3631
|
}
|
|
3632
3632
|
target.userinfo = base.userinfo;
|
|
3633
3633
|
target.host = base.host;
|
|
@@ -3635,7 +3635,7 @@ var require_fast_uri = __commonJS({
|
|
|
3635
3635
|
}
|
|
3636
3636
|
target.scheme = base.scheme;
|
|
3637
3637
|
}
|
|
3638
|
-
target.fragment =
|
|
3638
|
+
target.fragment = relative4.fragment;
|
|
3639
3639
|
return target;
|
|
3640
3640
|
}
|
|
3641
3641
|
function equal(uriA, uriB, options) {
|
|
@@ -3806,7 +3806,7 @@ var require_fast_uri = __commonJS({
|
|
|
3806
3806
|
var fastUri = {
|
|
3807
3807
|
SCHEMES,
|
|
3808
3808
|
normalize,
|
|
3809
|
-
resolve:
|
|
3809
|
+
resolve: resolve7,
|
|
3810
3810
|
resolveComponent,
|
|
3811
3811
|
equal,
|
|
3812
3812
|
serialize,
|
|
@@ -6853,6 +6853,84 @@ function buildSarifResult3(opts) {
|
|
|
6853
6853
|
};
|
|
6854
6854
|
}
|
|
6855
6855
|
|
|
6856
|
+
// src/adapters/dart-analyzer.ts
|
|
6857
|
+
function mapSeverity3(dartSeverity) {
|
|
6858
|
+
switch (dartSeverity.toUpperCase()) {
|
|
6859
|
+
case "ERROR":
|
|
6860
|
+
return "error";
|
|
6861
|
+
case "WARNING":
|
|
6862
|
+
return "warning";
|
|
6863
|
+
case "INFO":
|
|
6864
|
+
return "note";
|
|
6865
|
+
default:
|
|
6866
|
+
return "warning";
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
var EFFORT_BY_SEVERITY = {
|
|
6870
|
+
error: 30,
|
|
6871
|
+
warning: 15,
|
|
6872
|
+
note: 5,
|
|
6873
|
+
none: 0
|
|
6874
|
+
};
|
|
6875
|
+
function adaptDartAnalyzer(rawOutput) {
|
|
6876
|
+
let parsed;
|
|
6877
|
+
if (typeof rawOutput === "string") {
|
|
6878
|
+
try {
|
|
6879
|
+
parsed = JSON.parse(rawOutput);
|
|
6880
|
+
} catch {
|
|
6881
|
+
throw new Error("[dart-analyzer adapter] rawOutput is not valid JSON");
|
|
6882
|
+
}
|
|
6883
|
+
} else if (rawOutput && typeof rawOutput === "object" && "diagnostics" in rawOutput) {
|
|
6884
|
+
parsed = rawOutput;
|
|
6885
|
+
} else {
|
|
6886
|
+
throw new Error(
|
|
6887
|
+
"[dart-analyzer adapter] rawOutput must be a JSON string or an object with a 'diagnostics' array"
|
|
6888
|
+
);
|
|
6889
|
+
}
|
|
6890
|
+
if (!Array.isArray(parsed.diagnostics)) {
|
|
6891
|
+
throw new Error("[dart-analyzer adapter] 'diagnostics' must be an array");
|
|
6892
|
+
}
|
|
6893
|
+
const results = [];
|
|
6894
|
+
let totalEffortMinutes = 0;
|
|
6895
|
+
for (const diag of parsed.diagnostics) {
|
|
6896
|
+
const level = mapSeverity3(diag.severity);
|
|
6897
|
+
const effort = EFFORT_BY_SEVERITY[level] ?? estimateEffortMinutes(level);
|
|
6898
|
+
totalEffortMinutes += effort;
|
|
6899
|
+
results.push({
|
|
6900
|
+
ruleId: diag.code,
|
|
6901
|
+
level,
|
|
6902
|
+
message: {
|
|
6903
|
+
text: diag.problemMessage + (diag.correctionMessage ? ` ${diag.correctionMessage}` : "")
|
|
6904
|
+
},
|
|
6905
|
+
locations: [
|
|
6906
|
+
{
|
|
6907
|
+
physicalLocation: {
|
|
6908
|
+
artifactLocation: {
|
|
6909
|
+
uri: diag.location.file
|
|
6910
|
+
},
|
|
6911
|
+
region: {
|
|
6912
|
+
startLine: diag.location.range.start.line,
|
|
6913
|
+
startColumn: diag.location.range.start.column,
|
|
6914
|
+
endLine: diag.location.range.end.line,
|
|
6915
|
+
endColumn: diag.location.range.end.column
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
],
|
|
6920
|
+
properties: {
|
|
6921
|
+
effortMinutes: effort,
|
|
6922
|
+
...diag.documentation ? { helpUri: diag.documentation } : {}
|
|
6923
|
+
}
|
|
6924
|
+
});
|
|
6925
|
+
}
|
|
6926
|
+
return {
|
|
6927
|
+
document: wrapResultsInSarif("dart_analyze", "1.0.0", results),
|
|
6928
|
+
sourceTool: "dart_analyze",
|
|
6929
|
+
findingCount: parsed.diagnostics.length,
|
|
6930
|
+
totalEffortMinutes
|
|
6931
|
+
};
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6856
6934
|
// src/adapters/index.ts
|
|
6857
6935
|
function adaptScannerOutput(scanner, rawOutput) {
|
|
6858
6936
|
switch (scanner) {
|
|
@@ -6864,6 +6942,8 @@ function adaptScannerOutput(scanner, rawOutput) {
|
|
|
6864
6942
|
return adaptBandit(rawOutput);
|
|
6865
6943
|
case "stryker":
|
|
6866
6944
|
return adaptStryker(rawOutput);
|
|
6945
|
+
case "dart_analyze":
|
|
6946
|
+
return adaptDartAnalyzer(rawOutput);
|
|
6867
6947
|
default: {
|
|
6868
6948
|
const exhaustive = scanner;
|
|
6869
6949
|
throw new Error(`[adapters] Unknown scanner: ${String(exhaustive)}`);
|
|
@@ -7041,6 +7121,15 @@ var LANGUAGE_TABLE = {
|
|
|
7041
7121
|
python: PYTHON,
|
|
7042
7122
|
java: JAVA
|
|
7043
7123
|
};
|
|
7124
|
+
function detectLanguageFromPath(filePath) {
|
|
7125
|
+
const lower = filePath.toLowerCase();
|
|
7126
|
+
for (const config of Object.values(LANGUAGE_TABLE)) {
|
|
7127
|
+
for (const ext of config.extensions) {
|
|
7128
|
+
if (lower.endsWith(ext)) return config.id;
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
return null;
|
|
7132
|
+
}
|
|
7044
7133
|
|
|
7045
7134
|
// src/ast/tree-sitter-engine.ts
|
|
7046
7135
|
var TreeSitterEngine = class {
|
|
@@ -7235,12 +7324,106 @@ function loadConfig() {
|
|
|
7235
7324
|
}
|
|
7236
7325
|
|
|
7237
7326
|
// src/dashboard/server.ts
|
|
7238
|
-
import { promises as
|
|
7239
|
-
import { dirname as dirname2, join as join2, resolve as
|
|
7327
|
+
import { promises as fs3, existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
7328
|
+
import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
|
|
7240
7329
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7241
7330
|
import Fastify from "fastify";
|
|
7242
7331
|
import fastifyStatic from "@fastify/static";
|
|
7243
7332
|
|
|
7333
|
+
// src/shared/exclusions.ts
|
|
7334
|
+
import picomatch from "picomatch";
|
|
7335
|
+
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
7336
|
+
// Package managers / vendored deps
|
|
7337
|
+
"node_modules",
|
|
7338
|
+
"vendor",
|
|
7339
|
+
// Version control
|
|
7340
|
+
".git",
|
|
7341
|
+
// Build outputs (general)
|
|
7342
|
+
"dist",
|
|
7343
|
+
"build",
|
|
7344
|
+
"bundle",
|
|
7345
|
+
"out",
|
|
7346
|
+
"target",
|
|
7347
|
+
"coverage",
|
|
7348
|
+
// Framework build outputs
|
|
7349
|
+
".next",
|
|
7350
|
+
// Next.js
|
|
7351
|
+
".nuxt",
|
|
7352
|
+
// Nuxt 2
|
|
7353
|
+
".output",
|
|
7354
|
+
// Nuxt 3
|
|
7355
|
+
".vercel",
|
|
7356
|
+
// Vercel
|
|
7357
|
+
".svelte-kit",
|
|
7358
|
+
// SvelteKit
|
|
7359
|
+
".astro",
|
|
7360
|
+
// Astro
|
|
7361
|
+
".angular",
|
|
7362
|
+
// Angular
|
|
7363
|
+
".turbo",
|
|
7364
|
+
// Turborepo
|
|
7365
|
+
".parcel-cache",
|
|
7366
|
+
// Parcel
|
|
7367
|
+
".expo",
|
|
7368
|
+
// Expo / React Native
|
|
7369
|
+
// Language-specific caches
|
|
7370
|
+
".venv",
|
|
7371
|
+
"venv",
|
|
7372
|
+
"__pycache__",
|
|
7373
|
+
".cache",
|
|
7374
|
+
".dart_tool",
|
|
7375
|
+
// Dart / Flutter
|
|
7376
|
+
".gradle",
|
|
7377
|
+
// Gradle
|
|
7378
|
+
// IDE state
|
|
7379
|
+
".idea",
|
|
7380
|
+
// Plugin state
|
|
7381
|
+
".claude-crap",
|
|
7382
|
+
".codesight"
|
|
7383
|
+
]);
|
|
7384
|
+
var DEFAULT_SKIP_PATTERNS = [
|
|
7385
|
+
"*.min.js",
|
|
7386
|
+
"*.min.css",
|
|
7387
|
+
"*.min.mjs",
|
|
7388
|
+
"*.min.cjs",
|
|
7389
|
+
"*.bundle.js",
|
|
7390
|
+
"*.chunk.js"
|
|
7391
|
+
];
|
|
7392
|
+
function createExclusionFilter(userExclusions) {
|
|
7393
|
+
const extraDirs = /* @__PURE__ */ new Set();
|
|
7394
|
+
const fileGlobs = [];
|
|
7395
|
+
for (const pattern of userExclusions ?? []) {
|
|
7396
|
+
if (pattern.endsWith("/")) {
|
|
7397
|
+
extraDirs.add(pattern.slice(0, -1));
|
|
7398
|
+
} else {
|
|
7399
|
+
fileGlobs.push(pattern);
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
const defaultFileMatchers = DEFAULT_SKIP_PATTERNS.map(
|
|
7403
|
+
(p) => picomatch(p, { dot: true })
|
|
7404
|
+
);
|
|
7405
|
+
const userFileMatchers = fileGlobs.map(
|
|
7406
|
+
(p) => picomatch(p, { dot: true })
|
|
7407
|
+
);
|
|
7408
|
+
return {
|
|
7409
|
+
shouldSkipDir(dirName) {
|
|
7410
|
+
if (dirName.startsWith(".") && dirName !== ".claude-plugin") {
|
|
7411
|
+
return DEFAULT_SKIP_DIRS.has(dirName) || true;
|
|
7412
|
+
}
|
|
7413
|
+
return DEFAULT_SKIP_DIRS.has(dirName) || extraDirs.has(dirName);
|
|
7414
|
+
},
|
|
7415
|
+
shouldSkipFile(relativePath, fileName) {
|
|
7416
|
+
for (const matcher of defaultFileMatchers) {
|
|
7417
|
+
if (matcher(fileName)) return true;
|
|
7418
|
+
}
|
|
7419
|
+
for (const matcher of userFileMatchers) {
|
|
7420
|
+
if (matcher(relativePath) || matcher(fileName)) return true;
|
|
7421
|
+
}
|
|
7422
|
+
return false;
|
|
7423
|
+
}
|
|
7424
|
+
};
|
|
7425
|
+
}
|
|
7426
|
+
|
|
7244
7427
|
// src/metrics/tdr.ts
|
|
7245
7428
|
var RATING_ORDER = ["A", "B", "C", "D", "E"];
|
|
7246
7429
|
function ratingToRank(rating) {
|
|
@@ -7403,6 +7586,105 @@ function renderProjectScoreMarkdown(score) {
|
|
|
7403
7586
|
].join("\n");
|
|
7404
7587
|
}
|
|
7405
7588
|
|
|
7589
|
+
// src/dashboard/file-detail.ts
|
|
7590
|
+
import { promises as fs2 } from "node:fs";
|
|
7591
|
+
|
|
7592
|
+
// src/workspace-guard.ts
|
|
7593
|
+
import { isAbsolute, resolve as resolve2, sep } from "node:path";
|
|
7594
|
+
function resolveWithinWorkspace(workspaceRoot, filePath) {
|
|
7595
|
+
const workspace = resolve2(workspaceRoot);
|
|
7596
|
+
const candidate = isAbsolute(filePath) ? resolve2(filePath) : resolve2(workspace, filePath);
|
|
7597
|
+
if (candidate !== workspace && !candidate.startsWith(workspace + sep)) {
|
|
7598
|
+
throw new Error(
|
|
7599
|
+
`[claude-crap] Refusing to access '${filePath}' \u2014 path escapes the workspace root`
|
|
7600
|
+
);
|
|
7601
|
+
}
|
|
7602
|
+
return candidate;
|
|
7603
|
+
}
|
|
7604
|
+
|
|
7605
|
+
// src/dashboard/file-detail.ts
|
|
7606
|
+
async function buildFileDetail(input) {
|
|
7607
|
+
const { relativePath, workspaceRoot, astEngine, sarifStore, cyclomaticMax } = input;
|
|
7608
|
+
const absolutePath = resolveWithinWorkspace(workspaceRoot, relativePath);
|
|
7609
|
+
const source = await fs2.readFile(absolutePath, "utf8");
|
|
7610
|
+
const sourceLines = source.split(/\r?\n/);
|
|
7611
|
+
if (sourceLines.length > 0 && sourceLines[sourceLines.length - 1] === "") {
|
|
7612
|
+
sourceLines.pop();
|
|
7613
|
+
}
|
|
7614
|
+
const physicalLoc = sourceLines.length;
|
|
7615
|
+
let logicalLoc = 0;
|
|
7616
|
+
for (const line of sourceLines) {
|
|
7617
|
+
if (line.trim().length > 0) logicalLoc += 1;
|
|
7618
|
+
}
|
|
7619
|
+
const language = detectLanguageFromPath(relativePath);
|
|
7620
|
+
let functions = [];
|
|
7621
|
+
if (language && astEngine) {
|
|
7622
|
+
try {
|
|
7623
|
+
const metrics = await astEngine.analyzeFile({
|
|
7624
|
+
filePath: absolutePath,
|
|
7625
|
+
language
|
|
7626
|
+
});
|
|
7627
|
+
functions = metrics.functions.map((fn) => ({
|
|
7628
|
+
name: fn.name,
|
|
7629
|
+
startLine: fn.startLine,
|
|
7630
|
+
endLine: fn.endLine,
|
|
7631
|
+
cyclomaticComplexity: fn.cyclomaticComplexity,
|
|
7632
|
+
lineCount: fn.lineCount
|
|
7633
|
+
}));
|
|
7634
|
+
} catch {
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
const allFindings = sarifStore.list();
|
|
7638
|
+
const fileFindings = allFindings.filter(
|
|
7639
|
+
(f) => f.location.uri === relativePath
|
|
7640
|
+
);
|
|
7641
|
+
const findings = fileFindings.map((f) => ({
|
|
7642
|
+
ruleId: f.ruleId,
|
|
7643
|
+
level: f.level,
|
|
7644
|
+
message: f.message,
|
|
7645
|
+
sourceTool: f.sourceTool,
|
|
7646
|
+
startLine: f.location.startLine,
|
|
7647
|
+
startColumn: f.location.startColumn,
|
|
7648
|
+
endLine: f.location.endLine ?? f.location.startLine,
|
|
7649
|
+
endColumn: f.location.endColumn ?? 0,
|
|
7650
|
+
effortMinutes: typeof f.properties?.effortMinutes === "number" ? f.properties.effortMinutes : 0
|
|
7651
|
+
}));
|
|
7652
|
+
let errorCount = 0;
|
|
7653
|
+
let warningCount = 0;
|
|
7654
|
+
let noteCount = 0;
|
|
7655
|
+
let totalEffortMinutes = 0;
|
|
7656
|
+
for (const f of findings) {
|
|
7657
|
+
if (f.level === "error") errorCount += 1;
|
|
7658
|
+
else if (f.level === "warning") warningCount += 1;
|
|
7659
|
+
else if (f.level === "note") noteCount += 1;
|
|
7660
|
+
totalEffortMinutes += f.effortMinutes;
|
|
7661
|
+
}
|
|
7662
|
+
const complexities = functions.map((f) => f.cyclomaticComplexity);
|
|
7663
|
+
const maxComplexity = complexities.length > 0 ? Math.max(...complexities) : 0;
|
|
7664
|
+
const avgComplexity = complexities.length > 0 ? Math.round(
|
|
7665
|
+
complexities.reduce((a, b) => a + b, 0) / complexities.length * 100
|
|
7666
|
+
) / 100 : 0;
|
|
7667
|
+
return {
|
|
7668
|
+
filePath: relativePath,
|
|
7669
|
+
language,
|
|
7670
|
+
physicalLoc,
|
|
7671
|
+
logicalLoc,
|
|
7672
|
+
cyclomaticMax,
|
|
7673
|
+
sourceLines,
|
|
7674
|
+
functions,
|
|
7675
|
+
findings,
|
|
7676
|
+
summary: {
|
|
7677
|
+
totalFindings: findings.length,
|
|
7678
|
+
errorCount,
|
|
7679
|
+
warningCount,
|
|
7680
|
+
noteCount,
|
|
7681
|
+
totalEffortMinutes,
|
|
7682
|
+
avgComplexity,
|
|
7683
|
+
maxComplexity
|
|
7684
|
+
}
|
|
7685
|
+
};
|
|
7686
|
+
}
|
|
7687
|
+
|
|
7406
7688
|
// src/dashboard/server.ts
|
|
7407
7689
|
async function startDashboard(options) {
|
|
7408
7690
|
const { config, sarifStore, workspaceStatsProvider, logger: logger2 } = options;
|
|
@@ -7416,13 +7698,45 @@ async function startDashboard(options) {
|
|
|
7416
7698
|
root: publicRoot,
|
|
7417
7699
|
prefix: "/"
|
|
7418
7700
|
});
|
|
7419
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.
|
|
7701
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.8" }));
|
|
7420
7702
|
fastify.get("/api/score", async () => {
|
|
7421
7703
|
const stats = await workspaceStatsProvider();
|
|
7422
7704
|
const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
|
|
7423
7705
|
return score;
|
|
7424
7706
|
});
|
|
7425
7707
|
fastify.get("/api/sarif", async () => sarifStore.toSarifDocument());
|
|
7708
|
+
fastify.get("/api/complexity", async () => {
|
|
7709
|
+
if (!options.astEngine) {
|
|
7710
|
+
return { threshold: config.cyclomaticMax, totalFunctions: 0, violationCount: 0, topFunctions: [] };
|
|
7711
|
+
}
|
|
7712
|
+
return buildComplexityReport(config, options.astEngine, logger2, options.exclude);
|
|
7713
|
+
});
|
|
7714
|
+
fastify.get("/api/file-detail", async (request, reply) => {
|
|
7715
|
+
const { path: filePath } = request.query;
|
|
7716
|
+
if (!filePath) {
|
|
7717
|
+
return reply.status(400).send({ error: "Missing required query parameter: path" });
|
|
7718
|
+
}
|
|
7719
|
+
try {
|
|
7720
|
+
const detail = await buildFileDetail({
|
|
7721
|
+
relativePath: filePath,
|
|
7722
|
+
workspaceRoot: config.pluginRoot,
|
|
7723
|
+
astEngine: options.astEngine,
|
|
7724
|
+
sarifStore,
|
|
7725
|
+
cyclomaticMax: config.cyclomaticMax
|
|
7726
|
+
});
|
|
7727
|
+
return detail;
|
|
7728
|
+
} catch (err) {
|
|
7729
|
+
const msg = err.message;
|
|
7730
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
7731
|
+
return reply.status(404).send({ error: `File not found: ${filePath}` });
|
|
7732
|
+
}
|
|
7733
|
+
if (msg.includes("escapes the workspace")) {
|
|
7734
|
+
return reply.status(400).send({ error: msg });
|
|
7735
|
+
}
|
|
7736
|
+
logger2.error({ err: msg, filePath }, "file-detail endpoint error");
|
|
7737
|
+
return reply.status(500).send({ error: "Internal server error" });
|
|
7738
|
+
}
|
|
7739
|
+
});
|
|
7426
7740
|
fastify.get("/", async (_request, reply) => {
|
|
7427
7741
|
return reply.sendFile("index.html");
|
|
7428
7742
|
});
|
|
@@ -7444,19 +7758,19 @@ async function resolvePublicRoot(logger2) {
|
|
|
7444
7758
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
7445
7759
|
const candidates = [
|
|
7446
7760
|
// 0. Bundled layout: plugin/bundle/mcp-server.mjs → ./dashboard/public
|
|
7447
|
-
|
|
7761
|
+
resolve3(here, "dashboard", "public"),
|
|
7448
7762
|
// 1. Compiled layout: dist/dashboard/server.js → ./public next to it
|
|
7449
7763
|
// (only present if a build step copies the assets — not used
|
|
7450
7764
|
// today, but accepted so a future copy step does not break us).
|
|
7451
|
-
|
|
7765
|
+
resolve3(here, "public"),
|
|
7452
7766
|
// 2. Source-relative layout: dist/dashboard/server.js → ../../src/dashboard/public
|
|
7453
7767
|
// This is the default — no copy step required because we resolve
|
|
7454
7768
|
// upward from `dist/` into `src/` at runtime.
|
|
7455
|
-
|
|
7769
|
+
resolve3(here, "..", "..", "src", "dashboard", "public")
|
|
7456
7770
|
];
|
|
7457
7771
|
for (const candidate of candidates) {
|
|
7458
7772
|
try {
|
|
7459
|
-
await
|
|
7773
|
+
await fs3.access(resolve3(candidate, "index.html"));
|
|
7460
7774
|
return candidate;
|
|
7461
7775
|
} catch {
|
|
7462
7776
|
}
|
|
@@ -7541,6 +7855,57 @@ async function killStaleDashboard(pidFilePath, port, logger2) {
|
|
|
7541
7855
|
removePidFile(pidFilePath);
|
|
7542
7856
|
await new Promise((r) => setTimeout(r, 300));
|
|
7543
7857
|
}
|
|
7858
|
+
async function buildComplexityReport(config, engine, logger2, exclude) {
|
|
7859
|
+
const threshold = config.cyclomaticMax;
|
|
7860
|
+
const filter = createExclusionFilter(exclude);
|
|
7861
|
+
const allFunctions = [];
|
|
7862
|
+
let totalFunctions = 0;
|
|
7863
|
+
async function walk2(dir) {
|
|
7864
|
+
let entries;
|
|
7865
|
+
try {
|
|
7866
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
7867
|
+
} catch {
|
|
7868
|
+
return;
|
|
7869
|
+
}
|
|
7870
|
+
for (const entry of entries) {
|
|
7871
|
+
const full = join2(dir, entry.name);
|
|
7872
|
+
if (entry.isDirectory()) {
|
|
7873
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
7874
|
+
await walk2(full);
|
|
7875
|
+
continue;
|
|
7876
|
+
}
|
|
7877
|
+
if (!entry.isFile()) continue;
|
|
7878
|
+
const language = detectLanguageFromPath(entry.name);
|
|
7879
|
+
if (!language) continue;
|
|
7880
|
+
try {
|
|
7881
|
+
const metrics = await engine.analyzeFile({ filePath: full, language });
|
|
7882
|
+
for (const fn of metrics.functions) {
|
|
7883
|
+
totalFunctions += 1;
|
|
7884
|
+
allFunctions.push({
|
|
7885
|
+
filePath: full.startsWith(config.pluginRoot) ? full.substring(config.pluginRoot.length + 1) : full,
|
|
7886
|
+
name: fn.name,
|
|
7887
|
+
cyclomaticComplexity: fn.cyclomaticComplexity,
|
|
7888
|
+
startLine: fn.startLine,
|
|
7889
|
+
endLine: fn.endLine,
|
|
7890
|
+
lineCount: fn.lineCount
|
|
7891
|
+
});
|
|
7892
|
+
}
|
|
7893
|
+
} catch (err) {
|
|
7894
|
+
logger2.warn(
|
|
7895
|
+
{ filePath: full, err: err.message },
|
|
7896
|
+
"complexity-report: failed to analyze file"
|
|
7897
|
+
);
|
|
7898
|
+
}
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
await walk2(config.pluginRoot);
|
|
7902
|
+
allFunctions.sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity);
|
|
7903
|
+
const topFunctions = allFunctions.slice(0, 20);
|
|
7904
|
+
const violationCount = allFunctions.filter(
|
|
7905
|
+
(f) => f.cyclomaticComplexity > threshold
|
|
7906
|
+
).length;
|
|
7907
|
+
return { threshold, totalFunctions, violationCount, topFunctions };
|
|
7908
|
+
}
|
|
7544
7909
|
async function buildScore(config, sarifStore, workspace, dashboardUrl) {
|
|
7545
7910
|
return computeProjectScore({
|
|
7546
7911
|
workspaceRoot: config.pluginRoot,
|
|
@@ -7583,24 +7948,8 @@ function computeCrap(input, threshold) {
|
|
|
7583
7948
|
}
|
|
7584
7949
|
|
|
7585
7950
|
// src/metrics/workspace-walker.ts
|
|
7586
|
-
import { promises as
|
|
7587
|
-
import { join as join3 } from "node:path";
|
|
7588
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
7589
|
-
"node_modules",
|
|
7590
|
-
".git",
|
|
7591
|
-
"dist",
|
|
7592
|
-
"build",
|
|
7593
|
-
"out",
|
|
7594
|
-
"target",
|
|
7595
|
-
".venv",
|
|
7596
|
-
"venv",
|
|
7597
|
-
"__pycache__",
|
|
7598
|
-
".cache",
|
|
7599
|
-
".next",
|
|
7600
|
-
".nuxt",
|
|
7601
|
-
".claude-crap",
|
|
7602
|
-
".codesight"
|
|
7603
|
-
]);
|
|
7951
|
+
import { promises as fs4 } from "node:fs";
|
|
7952
|
+
import { join as join3, relative } from "node:path";
|
|
7604
7953
|
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7605
7954
|
".ts",
|
|
7606
7955
|
".tsx",
|
|
@@ -7624,7 +7973,8 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
7624
7973
|
".vue"
|
|
7625
7974
|
]);
|
|
7626
7975
|
var MAX_FILES_WALKED = 2e4;
|
|
7627
|
-
async function estimateWorkspaceLoc(workspaceRoot) {
|
|
7976
|
+
async function estimateWorkspaceLoc(workspaceRoot, options) {
|
|
7977
|
+
const filter = createExclusionFilter(options?.exclude);
|
|
7628
7978
|
let physicalLoc = 0;
|
|
7629
7979
|
let fileCount = 0;
|
|
7630
7980
|
let truncated = false;
|
|
@@ -7632,16 +7982,15 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7632
7982
|
if (truncated) return;
|
|
7633
7983
|
let entries;
|
|
7634
7984
|
try {
|
|
7635
|
-
entries = await
|
|
7985
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
7636
7986
|
} catch {
|
|
7637
7987
|
return;
|
|
7638
7988
|
}
|
|
7639
7989
|
for (const entry of entries) {
|
|
7640
7990
|
if (truncated) return;
|
|
7641
|
-
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
7642
7991
|
const full = join3(dir, entry.name);
|
|
7643
7992
|
if (entry.isDirectory()) {
|
|
7644
|
-
if (
|
|
7993
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
7645
7994
|
await walk2(full);
|
|
7646
7995
|
continue;
|
|
7647
7996
|
}
|
|
@@ -7651,13 +8000,15 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7651
8000
|
if (dot < 0) continue;
|
|
7652
8001
|
const ext = lower.substring(dot);
|
|
7653
8002
|
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
8003
|
+
const relPath = relative(workspaceRoot, full);
|
|
8004
|
+
if (filter.shouldSkipFile(relPath, entry.name)) continue;
|
|
7654
8005
|
fileCount += 1;
|
|
7655
8006
|
if (fileCount > MAX_FILES_WALKED) {
|
|
7656
8007
|
truncated = true;
|
|
7657
8008
|
return;
|
|
7658
8009
|
}
|
|
7659
8010
|
try {
|
|
7660
|
-
const content = await
|
|
8011
|
+
const content = await fs4.readFile(full, "utf8");
|
|
7661
8012
|
if (content.length > 0) {
|
|
7662
8013
|
const lines = content.split(/\r?\n/).length;
|
|
7663
8014
|
physicalLoc += content.endsWith("\n") ? lines - 1 : lines;
|
|
@@ -7671,8 +8022,8 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7671
8022
|
}
|
|
7672
8023
|
|
|
7673
8024
|
// src/sarif/sarif-store.ts
|
|
7674
|
-
import { promises as
|
|
7675
|
-
import { dirname as dirname3, isAbsolute, join as join4, resolve as
|
|
8025
|
+
import { promises as fs5 } from "node:fs";
|
|
8026
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "node:path";
|
|
7676
8027
|
|
|
7677
8028
|
// src/sarif/sarif-builder.ts
|
|
7678
8029
|
function buildSarifDocument(tool, findings) {
|
|
@@ -7734,7 +8085,7 @@ var SarifStore = class {
|
|
|
7734
8085
|
/** Tool invocations we have already ingested, for telemetry. */
|
|
7735
8086
|
toolInvocations = 0;
|
|
7736
8087
|
constructor(options) {
|
|
7737
|
-
const dir =
|
|
8088
|
+
const dir = isAbsolute2(options.outputDir) ? options.outputDir : resolve4(options.workspaceRoot, options.outputDir);
|
|
7738
8089
|
this.filePath = join4(dir, options.fileName ?? "latest.sarif");
|
|
7739
8090
|
}
|
|
7740
8091
|
/**
|
|
@@ -7759,7 +8110,7 @@ var SarifStore = class {
|
|
|
7759
8110
|
*/
|
|
7760
8111
|
async loadLatest() {
|
|
7761
8112
|
try {
|
|
7762
|
-
const raw = await
|
|
8113
|
+
const raw = await fs5.readFile(this.filePath, "utf8");
|
|
7763
8114
|
const parsed = JSON.parse(raw);
|
|
7764
8115
|
if (parsed.version !== "2.1.0") {
|
|
7765
8116
|
throw new Error(`Expected SARIF 2.1.0, got ${parsed.version}`);
|
|
@@ -7861,10 +8212,10 @@ var SarifStore = class {
|
|
|
7861
8212
|
*/
|
|
7862
8213
|
async persist() {
|
|
7863
8214
|
const doc = this.toSarifDocument();
|
|
7864
|
-
await
|
|
8215
|
+
await fs5.mkdir(dirname3(this.filePath), { recursive: true });
|
|
7865
8216
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
7866
|
-
await
|
|
7867
|
-
await
|
|
8217
|
+
await fs5.writeFile(tmp, JSON.stringify(doc, null, 2), "utf8");
|
|
8218
|
+
await fs5.rename(tmp, this.filePath);
|
|
7868
8219
|
}
|
|
7869
8220
|
/**
|
|
7870
8221
|
* Build the current consolidated SARIF document from the in-memory
|
|
@@ -8020,6 +8371,8 @@ var CrapConfigError = class extends Error {
|
|
|
8020
8371
|
}
|
|
8021
8372
|
};
|
|
8022
8373
|
function loadCrapConfig(options) {
|
|
8374
|
+
const fileResult = readFromFile(options.workspaceRoot);
|
|
8375
|
+
const exclude = fileResult?.exclude ?? [];
|
|
8023
8376
|
const envRaw = process.env["CLAUDE_CRAP_STRICTNESS"];
|
|
8024
8377
|
if (typeof envRaw === "string" && envRaw.trim() !== "") {
|
|
8025
8378
|
const normalized = envRaw.trim().toLowerCase();
|
|
@@ -8028,11 +8381,12 @@ function loadCrapConfig(options) {
|
|
|
8028
8381
|
`[crap-config] CLAUDE_CRAP_STRICTNESS="${envRaw}" is not a valid strictness. Expected one of: ${STRICTNESS_VALUES.join(", ")}.`
|
|
8029
8382
|
);
|
|
8030
8383
|
}
|
|
8031
|
-
return { strictness: normalized, strictnessSource: "env" };
|
|
8384
|
+
return { strictness: normalized, strictnessSource: "env", exclude };
|
|
8385
|
+
}
|
|
8386
|
+
if (fileResult?.strictness) {
|
|
8387
|
+
return { strictness: fileResult.strictness, strictnessSource: "file", exclude };
|
|
8032
8388
|
}
|
|
8033
|
-
|
|
8034
|
-
if (fromFile) return { strictness: fromFile, strictnessSource: "file" };
|
|
8035
|
-
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default" };
|
|
8389
|
+
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude };
|
|
8036
8390
|
}
|
|
8037
8391
|
function readFromFile(workspaceRoot) {
|
|
8038
8392
|
const filePath = join5(workspaceRoot, ".claude-crap.json");
|
|
@@ -8060,43 +8414,63 @@ function readFromFile(workspaceRoot) {
|
|
|
8060
8414
|
);
|
|
8061
8415
|
}
|
|
8062
8416
|
const doc = parsed;
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8417
|
+
let strictness = null;
|
|
8418
|
+
if ("strictness" in doc) {
|
|
8419
|
+
const value = doc["strictness"];
|
|
8420
|
+
if (typeof value !== "string") {
|
|
8421
|
+
throw new CrapConfigError(
|
|
8422
|
+
`[crap-config] ${filePath}: 'strictness' must be a string, got ${typeof value}`
|
|
8423
|
+
);
|
|
8424
|
+
}
|
|
8425
|
+
const normalized = value.trim().toLowerCase();
|
|
8426
|
+
if (!isStrictness(normalized)) {
|
|
8427
|
+
throw new CrapConfigError(
|
|
8428
|
+
`[crap-config] ${filePath}: 'strictness' is "${value}"; expected one of ${STRICTNESS_VALUES.join(", ")}.`
|
|
8429
|
+
);
|
|
8430
|
+
}
|
|
8431
|
+
strictness = normalized;
|
|
8069
8432
|
}
|
|
8070
|
-
|
|
8071
|
-
if (
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8433
|
+
let exclude = [];
|
|
8434
|
+
if ("exclude" in doc) {
|
|
8435
|
+
const raw2 = doc["exclude"];
|
|
8436
|
+
if (!Array.isArray(raw2)) {
|
|
8437
|
+
throw new CrapConfigError(
|
|
8438
|
+
`[crap-config] ${filePath}: 'exclude' must be an array of strings`
|
|
8439
|
+
);
|
|
8440
|
+
}
|
|
8441
|
+
for (const item of raw2) {
|
|
8442
|
+
if (typeof item !== "string") {
|
|
8443
|
+
throw new CrapConfigError(
|
|
8444
|
+
`[crap-config] ${filePath}: every entry in 'exclude' must be a string, got ${typeof item}`
|
|
8445
|
+
);
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
exclude = raw2;
|
|
8075
8449
|
}
|
|
8076
|
-
return
|
|
8450
|
+
return { strictness, exclude };
|
|
8077
8451
|
}
|
|
8078
8452
|
function isStrictness(value) {
|
|
8079
8453
|
return STRICTNESS_VALUES.includes(value);
|
|
8080
8454
|
}
|
|
8081
8455
|
|
|
8082
8456
|
// src/tools/test-harness.ts
|
|
8083
|
-
import { promises as
|
|
8084
|
-
import { basename, dirname as dirname4, extname, isAbsolute as
|
|
8457
|
+
import { promises as fs6 } from "node:fs";
|
|
8458
|
+
import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve5, sep as sep2 } from "node:path";
|
|
8085
8459
|
var TEST_SUFFIX_PATTERN = /\.(test|spec)\./;
|
|
8086
8460
|
function isTestFile(filePath) {
|
|
8087
8461
|
const base = basename(filePath);
|
|
8088
8462
|
if (TEST_SUFFIX_PATTERN.test(base)) return true;
|
|
8089
8463
|
if (base.startsWith("test_") && base.endsWith(".py")) return true;
|
|
8090
|
-
const parts = filePath.split(
|
|
8464
|
+
const parts = filePath.split(sep2);
|
|
8091
8465
|
return parts.includes("__tests__") || parts.includes("tests") || parts.includes("test");
|
|
8092
8466
|
}
|
|
8093
8467
|
function candidatePaths(workspaceRoot, filePath) {
|
|
8094
|
-
const absSource =
|
|
8468
|
+
const absSource = resolve5(filePath);
|
|
8095
8469
|
const ext = extname(absSource);
|
|
8096
8470
|
const base = basename(absSource, ext);
|
|
8097
8471
|
const dir = dirname4(absSource);
|
|
8098
|
-
const absWorkspace =
|
|
8099
|
-
const relFromRoot =
|
|
8472
|
+
const absWorkspace = resolve5(workspaceRoot);
|
|
8473
|
+
const relFromRoot = relative2(absWorkspace, absSource);
|
|
8100
8474
|
const relDir = dirname4(relFromRoot);
|
|
8101
8475
|
const candidates = /* @__PURE__ */ new Set();
|
|
8102
8476
|
candidates.add(join6(dir, `${base}.test${ext}`));
|
|
@@ -8128,14 +8502,14 @@ function candidatePaths(workspaceRoot, filePath) {
|
|
|
8128
8502
|
return Array.from(candidates);
|
|
8129
8503
|
}
|
|
8130
8504
|
async function findTestFile(workspaceRoot, filePath) {
|
|
8131
|
-
const absolute =
|
|
8505
|
+
const absolute = isAbsolute3(filePath) ? filePath : resolve5(workspaceRoot, filePath);
|
|
8132
8506
|
if (isTestFile(absolute)) {
|
|
8133
8507
|
return { testFile: absolute, candidates: [absolute], isTestFile: true };
|
|
8134
8508
|
}
|
|
8135
8509
|
const candidates = candidatePaths(workspaceRoot, absolute);
|
|
8136
8510
|
for (const candidate of candidates) {
|
|
8137
8511
|
try {
|
|
8138
|
-
await
|
|
8512
|
+
await fs6.access(candidate);
|
|
8139
8513
|
return { testFile: candidate, candidates, isTestFile: false };
|
|
8140
8514
|
} catch {
|
|
8141
8515
|
}
|
|
@@ -8143,26 +8517,13 @@ async function findTestFile(workspaceRoot, filePath) {
|
|
|
8143
8517
|
return { testFile: null, candidates, isTestFile: false };
|
|
8144
8518
|
}
|
|
8145
8519
|
|
|
8146
|
-
// src/workspace-guard.ts
|
|
8147
|
-
import { isAbsolute as isAbsolute3, resolve as resolve5, sep as sep2 } from "node:path";
|
|
8148
|
-
function resolveWithinWorkspace(workspaceRoot, filePath) {
|
|
8149
|
-
const workspace = resolve5(workspaceRoot);
|
|
8150
|
-
const candidate = isAbsolute3(filePath) ? resolve5(filePath) : resolve5(workspace, filePath);
|
|
8151
|
-
if (candidate !== workspace && !candidate.startsWith(workspace + sep2)) {
|
|
8152
|
-
throw new Error(
|
|
8153
|
-
`[claude-crap] Refusing to access '${filePath}' \u2014 path escapes the workspace root`
|
|
8154
|
-
);
|
|
8155
|
-
}
|
|
8156
|
-
return candidate;
|
|
8157
|
-
}
|
|
8158
|
-
|
|
8159
8520
|
// src/scanner/auto-scan.ts
|
|
8160
8521
|
import { existsSync as existsSync5 } from "node:fs";
|
|
8161
|
-
import { join as
|
|
8522
|
+
import { join as join11 } from "node:path";
|
|
8162
8523
|
|
|
8163
8524
|
// src/scanner/detector.ts
|
|
8164
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
8165
|
-
import { join as join7 } from "node:path";
|
|
8525
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "node:fs";
|
|
8526
|
+
import { join as join7, resolve as resolve6 } from "node:path";
|
|
8166
8527
|
import { execFile } from "node:child_process";
|
|
8167
8528
|
var SCANNER_SIGNALS = {
|
|
8168
8529
|
eslint: {
|
|
@@ -8211,6 +8572,14 @@ var SCANNER_SIGNALS = {
|
|
|
8211
8572
|
],
|
|
8212
8573
|
packageJsonKeys: ["@stryker-mutator/core"],
|
|
8213
8574
|
binaryNames: ["stryker"]
|
|
8575
|
+
},
|
|
8576
|
+
dart_analyze: {
|
|
8577
|
+
configFiles: [
|
|
8578
|
+
"analysis_options.yaml",
|
|
8579
|
+
"pubspec.yaml"
|
|
8580
|
+
],
|
|
8581
|
+
packageJsonKeys: [],
|
|
8582
|
+
binaryNames: ["dart"]
|
|
8214
8583
|
}
|
|
8215
8584
|
};
|
|
8216
8585
|
function probeConfigFiles(workspaceRoot, scanner) {
|
|
@@ -8241,14 +8610,14 @@ function probePackageJson(workspaceRoot, scanner) {
|
|
|
8241
8610
|
}
|
|
8242
8611
|
}
|
|
8243
8612
|
function probeBinary(binaryName) {
|
|
8244
|
-
return new Promise((
|
|
8613
|
+
return new Promise((resolve7) => {
|
|
8245
8614
|
execFile("which", [binaryName], { timeout: 5e3 }, (err) => {
|
|
8246
|
-
|
|
8615
|
+
resolve7(err === null);
|
|
8247
8616
|
});
|
|
8248
8617
|
});
|
|
8249
8618
|
}
|
|
8250
8619
|
async function detectScanners(workspaceRoot) {
|
|
8251
|
-
const scanners = ["eslint", "semgrep", "bandit", "stryker"];
|
|
8620
|
+
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze"];
|
|
8252
8621
|
const results = await Promise.all(
|
|
8253
8622
|
scanners.map(async (scanner) => {
|
|
8254
8623
|
const configProbe = probeConfigFiles(workspaceRoot, scanner);
|
|
@@ -8261,10 +8630,13 @@ async function detectScanners(workspaceRoot) {
|
|
|
8261
8630
|
};
|
|
8262
8631
|
}
|
|
8263
8632
|
if (probePackageJson(workspaceRoot, scanner)) {
|
|
8633
|
+
const binName = SCANNER_SIGNALS[scanner].binaryNames[0];
|
|
8634
|
+
const binPath = binName ? join7(workspaceRoot, "node_modules", ".bin", binName) : null;
|
|
8635
|
+
const installed = binPath !== null && existsSync2(binPath);
|
|
8264
8636
|
return {
|
|
8265
8637
|
scanner,
|
|
8266
|
-
available:
|
|
8267
|
-
reason: `found in package.json
|
|
8638
|
+
available: installed,
|
|
8639
|
+
reason: installed ? "found in package.json and installed" : `found in package.json but not installed (run \`npm install\`)`
|
|
8268
8640
|
};
|
|
8269
8641
|
}
|
|
8270
8642
|
const signals = SCANNER_SIGNALS[scanner];
|
|
@@ -8286,6 +8658,58 @@ async function detectScanners(workspaceRoot) {
|
|
|
8286
8658
|
);
|
|
8287
8659
|
return results;
|
|
8288
8660
|
}
|
|
8661
|
+
var MONOREPO_DIRS = ["apps", "packages", "libs", "modules", "services"];
|
|
8662
|
+
async function detectMonorepoScanners(workspaceRoot) {
|
|
8663
|
+
const subdirs = /* @__PURE__ */ new Set();
|
|
8664
|
+
try {
|
|
8665
|
+
const pkgPath = join7(workspaceRoot, "package.json");
|
|
8666
|
+
const raw = readFileSync3(pkgPath, "utf-8");
|
|
8667
|
+
const pkg = JSON.parse(raw);
|
|
8668
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
8669
|
+
for (const ws of pkg.workspaces) {
|
|
8670
|
+
if (typeof ws === "string" && !ws.includes("*")) {
|
|
8671
|
+
const full = resolve6(workspaceRoot, ws);
|
|
8672
|
+
if (existsSync2(full)) subdirs.add(full);
|
|
8673
|
+
}
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
} catch {
|
|
8677
|
+
}
|
|
8678
|
+
for (const dir of MONOREPO_DIRS) {
|
|
8679
|
+
const full = join7(workspaceRoot, dir);
|
|
8680
|
+
try {
|
|
8681
|
+
const entries = readdirSync(full, { withFileTypes: true });
|
|
8682
|
+
for (const entry of entries) {
|
|
8683
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
8684
|
+
subdirs.add(join7(full, entry.name));
|
|
8685
|
+
}
|
|
8686
|
+
}
|
|
8687
|
+
} catch {
|
|
8688
|
+
}
|
|
8689
|
+
}
|
|
8690
|
+
if (subdirs.size === 0) return [];
|
|
8691
|
+
const detections = [];
|
|
8692
|
+
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze"];
|
|
8693
|
+
for (const subdir of subdirs) {
|
|
8694
|
+
for (const scanner of scanners) {
|
|
8695
|
+
const configProbe = probeConfigFiles(subdir, scanner);
|
|
8696
|
+
if (!configProbe.found) continue;
|
|
8697
|
+
if (scanner === "dart_analyze") {
|
|
8698
|
+
const hasBinary = await probeBinary("dart");
|
|
8699
|
+
if (!hasBinary) continue;
|
|
8700
|
+
}
|
|
8701
|
+
const relDir = subdir.replace(workspaceRoot + "/", "");
|
|
8702
|
+
detections.push({
|
|
8703
|
+
scanner,
|
|
8704
|
+
available: true,
|
|
8705
|
+
reason: `config file found in ${relDir}/`,
|
|
8706
|
+
...configProbe.path ? { configPath: configProbe.path } : {},
|
|
8707
|
+
workingDir: subdir
|
|
8708
|
+
});
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
return detections;
|
|
8712
|
+
}
|
|
8289
8713
|
|
|
8290
8714
|
// src/scanner/runner.ts
|
|
8291
8715
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -8322,17 +8746,26 @@ function getScannerCommand(scanner, workspaceRoot) {
|
|
|
8322
8746
|
nonZeroIsNormal: false,
|
|
8323
8747
|
outputFile: join8(workspaceRoot, "reports", "mutation", "mutation.json")
|
|
8324
8748
|
};
|
|
8749
|
+
case "dart_analyze":
|
|
8750
|
+
return {
|
|
8751
|
+
command: "dart",
|
|
8752
|
+
args: ["analyze", "--format=json", "."],
|
|
8753
|
+
timeoutMs: 12e4,
|
|
8754
|
+
nonZeroIsNormal: true
|
|
8755
|
+
// exits 3 when findings exist
|
|
8756
|
+
};
|
|
8325
8757
|
}
|
|
8326
8758
|
}
|
|
8327
|
-
function runScanner(scanner, workspaceRoot) {
|
|
8759
|
+
function runScanner(scanner, workspaceRoot, options) {
|
|
8328
8760
|
const start = Date.now();
|
|
8329
|
-
const
|
|
8330
|
-
|
|
8761
|
+
const cwd = options?.workingDir ?? workspaceRoot;
|
|
8762
|
+
const cmd = getScannerCommand(scanner, cwd);
|
|
8763
|
+
return new Promise((resolve7) => {
|
|
8331
8764
|
execFile2(
|
|
8332
8765
|
cmd.command,
|
|
8333
8766
|
cmd.args,
|
|
8334
8767
|
{
|
|
8335
|
-
cwd
|
|
8768
|
+
cwd,
|
|
8336
8769
|
timeout: cmd.timeoutMs,
|
|
8337
8770
|
maxBuffer: 50 * 1024 * 1024,
|
|
8338
8771
|
// 50 MB — large codebases produce verbose output
|
|
@@ -8346,7 +8779,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8346
8779
|
if (cmd.outputFile && existsSync3(cmd.outputFile)) {
|
|
8347
8780
|
try {
|
|
8348
8781
|
const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
|
|
8349
|
-
|
|
8782
|
+
resolve7({
|
|
8350
8783
|
scanner,
|
|
8351
8784
|
success: true,
|
|
8352
8785
|
rawOutput: fileOutput,
|
|
@@ -8356,7 +8789,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8356
8789
|
} catch {
|
|
8357
8790
|
}
|
|
8358
8791
|
}
|
|
8359
|
-
|
|
8792
|
+
resolve7({
|
|
8360
8793
|
scanner,
|
|
8361
8794
|
success: false,
|
|
8362
8795
|
rawOutput: "",
|
|
@@ -8369,7 +8802,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8369
8802
|
if (existsSync3(cmd.outputFile)) {
|
|
8370
8803
|
try {
|
|
8371
8804
|
const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
|
|
8372
|
-
|
|
8805
|
+
resolve7({
|
|
8373
8806
|
scanner,
|
|
8374
8807
|
success: true,
|
|
8375
8808
|
rawOutput: fileOutput,
|
|
@@ -8377,7 +8810,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8377
8810
|
});
|
|
8378
8811
|
return;
|
|
8379
8812
|
} catch (readErr) {
|
|
8380
|
-
|
|
8813
|
+
resolve7({
|
|
8381
8814
|
scanner,
|
|
8382
8815
|
success: false,
|
|
8383
8816
|
rawOutput: "",
|
|
@@ -8387,7 +8820,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8387
8820
|
return;
|
|
8388
8821
|
}
|
|
8389
8822
|
}
|
|
8390
|
-
|
|
8823
|
+
resolve7({
|
|
8391
8824
|
scanner,
|
|
8392
8825
|
success: false,
|
|
8393
8826
|
rawOutput: "",
|
|
@@ -8398,7 +8831,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8398
8831
|
}
|
|
8399
8832
|
const output = stdout.trim();
|
|
8400
8833
|
if (!output) {
|
|
8401
|
-
|
|
8834
|
+
resolve7({
|
|
8402
8835
|
scanner,
|
|
8403
8836
|
success: true,
|
|
8404
8837
|
rawOutput: "[]",
|
|
@@ -8407,7 +8840,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8407
8840
|
});
|
|
8408
8841
|
return;
|
|
8409
8842
|
}
|
|
8410
|
-
|
|
8843
|
+
resolve7({
|
|
8411
8844
|
scanner,
|
|
8412
8845
|
success: true,
|
|
8413
8846
|
rawOutput: output,
|
|
@@ -8419,7 +8852,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8419
8852
|
}
|
|
8420
8853
|
|
|
8421
8854
|
// src/scanner/bootstrap.ts
|
|
8422
|
-
import { existsSync as existsSync4, writeFileSync as writeFileSync2, readdirSync } from "node:fs";
|
|
8855
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, readdirSync as readdirSync2 } from "node:fs";
|
|
8423
8856
|
import { join as join9 } from "node:path";
|
|
8424
8857
|
import { execFile as execFile3 } from "node:child_process";
|
|
8425
8858
|
function detectProjectType(workspaceRoot) {
|
|
@@ -8436,12 +8869,13 @@ function detectProjectType(workspaceRoot) {
|
|
|
8436
8869
|
}
|
|
8437
8870
|
if (has("Directory.Build.props")) return "csharp";
|
|
8438
8871
|
try {
|
|
8439
|
-
const entries =
|
|
8872
|
+
const entries = readdirSync2(workspaceRoot);
|
|
8440
8873
|
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
|
|
8441
8874
|
return "csharp";
|
|
8442
8875
|
}
|
|
8443
8876
|
} catch {
|
|
8444
8877
|
}
|
|
8878
|
+
if (has("pubspec.yaml")) return "dart";
|
|
8445
8879
|
return "unknown";
|
|
8446
8880
|
}
|
|
8447
8881
|
function generateEslintConfig(isTypeScript) {
|
|
@@ -8483,7 +8917,7 @@ export default [
|
|
|
8483
8917
|
`;
|
|
8484
8918
|
}
|
|
8485
8919
|
function npmInstall(workspaceRoot, packages) {
|
|
8486
|
-
return new Promise((
|
|
8920
|
+
return new Promise((resolve7) => {
|
|
8487
8921
|
execFile3(
|
|
8488
8922
|
"npm",
|
|
8489
8923
|
["install", "--save-dev", ...packages],
|
|
@@ -8494,14 +8928,14 @@ function npmInstall(workspaceRoot, packages) {
|
|
|
8494
8928
|
},
|
|
8495
8929
|
(err, stdout, stderr) => {
|
|
8496
8930
|
if (err) {
|
|
8497
|
-
|
|
8931
|
+
resolve7({
|
|
8498
8932
|
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8499
8933
|
success: false,
|
|
8500
8934
|
detail: stderr || err.message
|
|
8501
8935
|
});
|
|
8502
8936
|
return;
|
|
8503
8937
|
}
|
|
8504
|
-
|
|
8938
|
+
resolve7({
|
|
8505
8939
|
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8506
8940
|
success: true,
|
|
8507
8941
|
detail: `installed ${packages.join(", ")}`
|
|
@@ -8556,6 +8990,12 @@ function getRecommendation(projectType) {
|
|
|
8556
8990
|
canAutoInstall: false,
|
|
8557
8991
|
installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
|
|
8558
8992
|
};
|
|
8993
|
+
case "dart":
|
|
8994
|
+
return {
|
|
8995
|
+
scanner: "dart_analyze",
|
|
8996
|
+
canAutoInstall: false,
|
|
8997
|
+
installInstructions: "Install the Dart SDK: https://dart.dev/get-dart (or Flutter SDK which includes Dart)"
|
|
8998
|
+
};
|
|
8559
8999
|
case "unknown":
|
|
8560
9000
|
return {
|
|
8561
9001
|
scanner: "semgrep",
|
|
@@ -8700,6 +9140,121 @@ function buildResult(projectType, steps, autoScanResult, recommendation) {
|
|
|
8700
9140
|
};
|
|
8701
9141
|
}
|
|
8702
9142
|
|
|
9143
|
+
// src/scanner/complexity-scanner.ts
|
|
9144
|
+
import { promises as fs7 } from "node:fs";
|
|
9145
|
+
import { join as join10, relative as relative3 } from "node:path";
|
|
9146
|
+
var MAX_FILES = 2e4;
|
|
9147
|
+
var RULE_ID = "complexity/cyclomatic-max";
|
|
9148
|
+
var SOURCE_TOOL = "complexity";
|
|
9149
|
+
async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2) {
|
|
9150
|
+
const start = Date.now();
|
|
9151
|
+
const threshold = config.cyclomaticMax;
|
|
9152
|
+
const errorThreshold = threshold * 2;
|
|
9153
|
+
const filter = createExclusionFilter(config.exclude);
|
|
9154
|
+
const files = await collectSourceFiles(workspaceRoot, filter);
|
|
9155
|
+
logger2.info(
|
|
9156
|
+
{ fileCount: files.length, threshold },
|
|
9157
|
+
"complexity-scanner: starting analysis"
|
|
9158
|
+
);
|
|
9159
|
+
const sarifResults = [];
|
|
9160
|
+
let filesScanned = 0;
|
|
9161
|
+
let functionsAnalyzed = 0;
|
|
9162
|
+
let violations = 0;
|
|
9163
|
+
for (const filePath of files) {
|
|
9164
|
+
const language = detectLanguageFromPath(filePath);
|
|
9165
|
+
if (!language) continue;
|
|
9166
|
+
try {
|
|
9167
|
+
const metrics = await engine.analyzeFile({ filePath, language });
|
|
9168
|
+
filesScanned += 1;
|
|
9169
|
+
functionsAnalyzed += metrics.functions.length;
|
|
9170
|
+
for (const fn of metrics.functions) {
|
|
9171
|
+
if (fn.cyclomaticComplexity <= threshold) continue;
|
|
9172
|
+
const level = fn.cyclomaticComplexity >= errorThreshold ? "error" : "warning";
|
|
9173
|
+
const relPath = relative3(workspaceRoot, filePath);
|
|
9174
|
+
sarifResults.push({
|
|
9175
|
+
ruleId: RULE_ID,
|
|
9176
|
+
level,
|
|
9177
|
+
message: {
|
|
9178
|
+
text: `Function '${fn.name}' has cyclomatic complexity ${fn.cyclomaticComplexity} (threshold: ${threshold})`
|
|
9179
|
+
},
|
|
9180
|
+
locations: [
|
|
9181
|
+
{
|
|
9182
|
+
physicalLocation: {
|
|
9183
|
+
artifactLocation: { uri: relPath },
|
|
9184
|
+
region: {
|
|
9185
|
+
startLine: fn.startLine,
|
|
9186
|
+
startColumn: 1,
|
|
9187
|
+
endLine: fn.endLine,
|
|
9188
|
+
endColumn: 1
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
}
|
|
9192
|
+
],
|
|
9193
|
+
properties: {
|
|
9194
|
+
sourceTool: SOURCE_TOOL,
|
|
9195
|
+
effortMinutes: estimateEffortMinutes(level),
|
|
9196
|
+
cyclomaticComplexity: fn.cyclomaticComplexity
|
|
9197
|
+
}
|
|
9198
|
+
});
|
|
9199
|
+
violations += 1;
|
|
9200
|
+
}
|
|
9201
|
+
} catch (err) {
|
|
9202
|
+
logger2.warn(
|
|
9203
|
+
{ filePath, err: err.message },
|
|
9204
|
+
"complexity-scanner: failed to analyze file, skipping"
|
|
9205
|
+
);
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
if (sarifResults.length > 0) {
|
|
9209
|
+
const document = wrapResultsInSarif(
|
|
9210
|
+
SOURCE_TOOL,
|
|
9211
|
+
"0.1.0",
|
|
9212
|
+
sarifResults
|
|
9213
|
+
);
|
|
9214
|
+
sarifStore.ingestRun(document, SOURCE_TOOL);
|
|
9215
|
+
await sarifStore.persist();
|
|
9216
|
+
}
|
|
9217
|
+
const durationMs = Date.now() - start;
|
|
9218
|
+
logger2.info(
|
|
9219
|
+
{ filesScanned, functionsAnalyzed, violations, durationMs },
|
|
9220
|
+
"complexity-scanner: analysis complete"
|
|
9221
|
+
);
|
|
9222
|
+
return { filesScanned, functionsAnalyzed, violations, durationMs };
|
|
9223
|
+
}
|
|
9224
|
+
async function collectSourceFiles(workspaceRoot, filter) {
|
|
9225
|
+
const files = [];
|
|
9226
|
+
let truncated = false;
|
|
9227
|
+
async function walk2(dir) {
|
|
9228
|
+
if (truncated) return;
|
|
9229
|
+
let entries;
|
|
9230
|
+
try {
|
|
9231
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
9232
|
+
} catch {
|
|
9233
|
+
return;
|
|
9234
|
+
}
|
|
9235
|
+
for (const entry of entries) {
|
|
9236
|
+
if (truncated) return;
|
|
9237
|
+
const full = join10(dir, entry.name);
|
|
9238
|
+
if (entry.isDirectory()) {
|
|
9239
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
9240
|
+
await walk2(full);
|
|
9241
|
+
continue;
|
|
9242
|
+
}
|
|
9243
|
+
if (!entry.isFile()) continue;
|
|
9244
|
+
if (!detectLanguageFromPath(entry.name)) continue;
|
|
9245
|
+
const relPath = relative3(workspaceRoot, full);
|
|
9246
|
+
if (filter.shouldSkipFile(relPath, entry.name)) continue;
|
|
9247
|
+
files.push(full);
|
|
9248
|
+
if (files.length >= MAX_FILES) {
|
|
9249
|
+
truncated = true;
|
|
9250
|
+
return;
|
|
9251
|
+
}
|
|
9252
|
+
}
|
|
9253
|
+
}
|
|
9254
|
+
await walk2(workspaceRoot);
|
|
9255
|
+
return files;
|
|
9256
|
+
}
|
|
9257
|
+
|
|
8703
9258
|
// src/scanner/auto-scan.ts
|
|
8704
9259
|
function ingestScannerRun(scanner, rawOutput, sarifStore) {
|
|
8705
9260
|
let parsed;
|
|
@@ -8712,13 +9267,21 @@ function ingestScannerRun(scanner, rawOutput, sarifStore) {
|
|
|
8712
9267
|
const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
|
|
8713
9268
|
return { accepted: stats.accepted };
|
|
8714
9269
|
}
|
|
8715
|
-
async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
9270
|
+
async function autoScan(workspaceRoot, sarifStore, logger2, options) {
|
|
8716
9271
|
const start = Date.now();
|
|
8717
9272
|
const detected = await detectScanners(workspaceRoot);
|
|
9273
|
+
const monorepoDetected = await detectMonorepoScanners(workspaceRoot);
|
|
9274
|
+
const rootScannerSet = new Set(detected.filter((d) => d.available).map((d) => d.scanner));
|
|
9275
|
+
for (const md of monorepoDetected) {
|
|
9276
|
+
if (!rootScannerSet.has(md.scanner)) {
|
|
9277
|
+
detected.push(md);
|
|
9278
|
+
}
|
|
9279
|
+
}
|
|
8718
9280
|
const available = detected.filter((d) => d.available);
|
|
8719
9281
|
logger2.info(
|
|
8720
9282
|
{
|
|
8721
9283
|
detected: detected.map((d) => `${d.scanner}:${d.available}`),
|
|
9284
|
+
monorepo: monorepoDetected.length,
|
|
8722
9285
|
available: available.length
|
|
8723
9286
|
},
|
|
8724
9287
|
"auto-scan: detection complete"
|
|
@@ -8737,7 +9300,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
|
8737
9300
|
".eslintrc.json"
|
|
8738
9301
|
];
|
|
8739
9302
|
const eslintDetected = available.some((d) => d.scanner === "eslint");
|
|
8740
|
-
const hasEslintConfig = eslintConfigFiles.some((f) => existsSync5(
|
|
9303
|
+
const hasEslintConfig = eslintConfigFiles.some((f) => existsSync5(join11(workspaceRoot, f)));
|
|
8741
9304
|
if (eslintDetected && !hasEslintConfig) {
|
|
8742
9305
|
logger2.info("auto-scan: ESLint detected but no config \u2014 running bootstrap");
|
|
8743
9306
|
try {
|
|
@@ -8773,7 +9336,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
|
8773
9336
|
};
|
|
8774
9337
|
}
|
|
8775
9338
|
const runResults = await Promise.allSettled(
|
|
8776
|
-
available.map((d) => runScanner(d.scanner, workspaceRoot))
|
|
9339
|
+
available.map((d) => runScanner(d.scanner, workspaceRoot, d.workingDir ? { workingDir: d.workingDir } : void 0))
|
|
8777
9340
|
);
|
|
8778
9341
|
const results = [];
|
|
8779
9342
|
let totalFindings = 0;
|
|
@@ -8847,11 +9410,30 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
|
8847
9410
|
if (persistNeeded) {
|
|
8848
9411
|
await sarifStore.persist();
|
|
8849
9412
|
}
|
|
9413
|
+
let complexityScan;
|
|
9414
|
+
if (options?.engine) {
|
|
9415
|
+
try {
|
|
9416
|
+
complexityScan = await scanComplexity(
|
|
9417
|
+
workspaceRoot,
|
|
9418
|
+
options.engine,
|
|
9419
|
+
sarifStore,
|
|
9420
|
+
{ cyclomaticMax: options.cyclomaticMax ?? 15, ...options.exclude ? { exclude: options.exclude } : {} },
|
|
9421
|
+
logger2
|
|
9422
|
+
);
|
|
9423
|
+
totalFindings += complexityScan.violations;
|
|
9424
|
+
} catch (err) {
|
|
9425
|
+
logger2.warn(
|
|
9426
|
+
{ err: err.message },
|
|
9427
|
+
"auto-scan: complexity scanner failed \u2014 continuing without it"
|
|
9428
|
+
);
|
|
9429
|
+
}
|
|
9430
|
+
}
|
|
8850
9431
|
return {
|
|
8851
9432
|
detected,
|
|
8852
9433
|
results,
|
|
8853
9434
|
totalFindings,
|
|
8854
|
-
totalDurationMs: Date.now() - start
|
|
9435
|
+
totalDurationMs: Date.now() - start,
|
|
9436
|
+
...complexityScan ? { complexityScan } : {}
|
|
8855
9437
|
};
|
|
8856
9438
|
}
|
|
8857
9439
|
|
|
@@ -8971,7 +9553,7 @@ var ingestScannerOutputSchema = {
|
|
|
8971
9553
|
properties: {
|
|
8972
9554
|
scanner: {
|
|
8973
9555
|
type: "string",
|
|
8974
|
-
enum: ["semgrep", "eslint", "bandit", "stryker"],
|
|
9556
|
+
enum: ["semgrep", "eslint", "bandit", "stryker", "dart_analyze"],
|
|
8975
9557
|
description: "Identifier of the producing scanner."
|
|
8976
9558
|
},
|
|
8977
9559
|
rawOutput: {
|
|
@@ -9031,6 +9613,15 @@ async function main() {
|
|
|
9031
9613
|
{ config: { ...config, pluginRoot: "<redacted>" } },
|
|
9032
9614
|
"claude-crap MCP server starting"
|
|
9033
9615
|
);
|
|
9616
|
+
let userExclusions = [];
|
|
9617
|
+
try {
|
|
9618
|
+
const crapConfig = loadCrapConfig({ workspaceRoot: config.pluginRoot });
|
|
9619
|
+
userExclusions = crapConfig.exclude;
|
|
9620
|
+
if (userExclusions.length > 0) {
|
|
9621
|
+
logger.info({ exclude: userExclusions }, "user exclusions loaded from .claude-crap.json");
|
|
9622
|
+
}
|
|
9623
|
+
} catch {
|
|
9624
|
+
}
|
|
9034
9625
|
const astEngine = new TreeSitterEngine();
|
|
9035
9626
|
const sarifStore = new SarifStore({
|
|
9036
9627
|
workspaceRoot: config.pluginRoot,
|
|
@@ -9046,8 +9637,10 @@ async function main() {
|
|
|
9046
9637
|
dashboard = await startDashboard({
|
|
9047
9638
|
config,
|
|
9048
9639
|
sarifStore,
|
|
9049
|
-
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot),
|
|
9050
|
-
logger
|
|
9640
|
+
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot, { exclude: userExclusions }),
|
|
9641
|
+
logger,
|
|
9642
|
+
astEngine,
|
|
9643
|
+
exclude: userExclusions
|
|
9051
9644
|
});
|
|
9052
9645
|
} catch (err) {
|
|
9053
9646
|
logger.warn(
|
|
@@ -9217,7 +9810,7 @@ async function main() {
|
|
|
9217
9810
|
const typed = args ?? {};
|
|
9218
9811
|
const format = typed.format ?? "both";
|
|
9219
9812
|
try {
|
|
9220
|
-
const workspace = await estimateWorkspaceLoc(config.pluginRoot);
|
|
9813
|
+
const workspace = await estimateWorkspaceLoc(config.pluginRoot, { exclude: userExclusions });
|
|
9221
9814
|
const score = computeProjectScore({
|
|
9222
9815
|
workspaceRoot: config.pluginRoot,
|
|
9223
9816
|
minutesPerLoc: config.minutesPerLoc,
|
|
@@ -9438,7 +10031,11 @@ async function main() {
|
|
|
9438
10031
|
case "auto_scan": {
|
|
9439
10032
|
logger.info({ tool: "auto_scan" }, "Tool call received");
|
|
9440
10033
|
try {
|
|
9441
|
-
const result = await autoScan(config.pluginRoot, sarifStore, logger
|
|
10034
|
+
const result = await autoScan(config.pluginRoot, sarifStore, logger, {
|
|
10035
|
+
engine: astEngine,
|
|
10036
|
+
cyclomaticMax: config.cyclomaticMax,
|
|
10037
|
+
exclude: userExclusions
|
|
10038
|
+
});
|
|
9442
10039
|
const markdown = renderAutoScanMarkdown(result);
|
|
9443
10040
|
return {
|
|
9444
10041
|
content: [
|
|
@@ -9514,7 +10111,11 @@ async function main() {
|
|
|
9514
10111
|
const transport = new StdioServerTransport();
|
|
9515
10112
|
await server.connect(transport);
|
|
9516
10113
|
logger.info("claude-crap MCP server ready (stdio)");
|
|
9517
|
-
autoScan(config.pluginRoot, sarifStore, logger
|
|
10114
|
+
autoScan(config.pluginRoot, sarifStore, logger, {
|
|
10115
|
+
engine: astEngine,
|
|
10116
|
+
cyclomaticMax: config.cyclomaticMax,
|
|
10117
|
+
exclude: userExclusions
|
|
10118
|
+
}).then((result) => {
|
|
9518
10119
|
const scanners = result.results.filter((r) => r.success).map((r) => r.scanner);
|
|
9519
10120
|
logger.info(
|
|
9520
10121
|
{
|
|
@@ -9581,6 +10182,15 @@ function renderAutoScanMarkdown(result) {
|
|
|
9581
10182
|
}
|
|
9582
10183
|
lines.push("");
|
|
9583
10184
|
}
|
|
10185
|
+
if (result.complexityScan) {
|
|
10186
|
+
const cs = result.complexityScan;
|
|
10187
|
+
lines.push("### Cyclomatic complexity scan\n");
|
|
10188
|
+
lines.push(`- Files scanned: **${cs.filesScanned}**`);
|
|
10189
|
+
lines.push(`- Functions analyzed: **${cs.functionsAnalyzed}**`);
|
|
10190
|
+
lines.push(`- Violations: **${cs.violations}**`);
|
|
10191
|
+
lines.push(`- Duration: ${(cs.durationMs / 1e3).toFixed(1)}s`);
|
|
10192
|
+
lines.push("");
|
|
10193
|
+
}
|
|
9584
10194
|
lines.push(
|
|
9585
10195
|
`**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1e3).toFixed(1)}s`
|
|
9586
10196
|
);
|