stopslop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +427 -0
- package/dist/index.d.ts +292 -0
- package/dist/index.js +2 -0
- package/dist/src.js +0 -0
- package/package.json +77 -0
- package/schema.json +1488 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as loadConfig, d as readBaseline, f as writeBaseline, l as applyBaseline, n as analyzeGitBase, t as analyze, u as fingerprint } from "./src.js";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
//#region src/report/terminal.ts
|
|
7
|
+
const KIND_LABEL = {
|
|
8
|
+
"god-module": "god module",
|
|
9
|
+
"god-class": "god class",
|
|
10
|
+
"cognitive-complexity": "complex fn",
|
|
11
|
+
"multiple-exported-classes": "multi-class file",
|
|
12
|
+
"duplicate-code": "clone",
|
|
13
|
+
"unused-export": "unused export",
|
|
14
|
+
"unused-dependency": "unused dep",
|
|
15
|
+
"unused-file": "unused file",
|
|
16
|
+
"orphan-feature": "orphan feature"
|
|
17
|
+
};
|
|
18
|
+
const KIND_ORDER = Object.keys(KIND_LABEL);
|
|
19
|
+
/** Findings per kind shown before collapsing into a "+N more" line. */
|
|
20
|
+
const MAX_PER_KIND = 12;
|
|
21
|
+
function renderTerminal(result, details) {
|
|
22
|
+
const lines = [];
|
|
23
|
+
const { findings } = result;
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(pc.bold(pc.cyan(" stopslop")) + pc.dim(` ${result.fileCount} files ${formatKloc(result.loc)} ${result.elapsedMs}ms`));
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push(renderScore(result));
|
|
28
|
+
if (result.gitBase !== void 0) lines.push(" " + pc.dim(`git base: ${result.gitBase.ref} (${result.gitBase.commit.slice(0, 12)}) · ${result.gitBase.suppressed} existing · ${findings.length} new`));
|
|
29
|
+
else if (result.baselineSuppressed !== void 0) lines.push(" " + pc.dim(`baseline: ${result.baselineSuppressed} accepted · ${findings.length} new`));
|
|
30
|
+
lines.push("");
|
|
31
|
+
if (findings.length === 0) {
|
|
32
|
+
const msg = result.gitBase !== void 0 ? `✓ no new slop since ${result.gitBase.ref}` : result.baselineSuppressed !== void 0 ? "✓ no new slop since baseline" : "✓ no slop signals";
|
|
33
|
+
lines.push(" " + pc.green(msg));
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push(renderFooter(result).join("\n"));
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
const counts = countByKind(findings);
|
|
39
|
+
const chips = KIND_ORDER.filter((k) => counts[k] > 0).map((k) => `${pc.bold(String(counts[k]))} ${pluralize(KIND_LABEL[k], counts[k])}`);
|
|
40
|
+
lines.push(" " + chips.join(pc.dim(" · ")));
|
|
41
|
+
lines.push("");
|
|
42
|
+
const shown = {};
|
|
43
|
+
for (const f of findings) if ((shown[f.kind] = (shown[f.kind] ?? 0) + 1) <= MAX_PER_KIND) lines.push(renderFinding(f, details));
|
|
44
|
+
for (const kind of KIND_ORDER) {
|
|
45
|
+
const hidden = counts[kind] - MAX_PER_KIND;
|
|
46
|
+
if (hidden > 0) lines.push(" " + pc.dim(`… ${hidden} more ${pluralize(KIND_LABEL[kind], hidden)} (--json for all)`));
|
|
47
|
+
}
|
|
48
|
+
lines.push(...renderFooter(result));
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
/** The headline: one comparable number, with the densities it came from. */
|
|
52
|
+
function renderScore(result) {
|
|
53
|
+
const { score, level, densities, signalCount, totalSignals, percentile } = result.slop;
|
|
54
|
+
const paint = LEVEL_COLOR$1[level];
|
|
55
|
+
const partial = signalCount < totalSignals ? pc.yellow(` — ${signalCount}/${totalSignals} signals, see notes`) : "";
|
|
56
|
+
const anchor = percentile !== void 0 ? pc.dim(` · worse than ${percentile}% of reference repos`) : "";
|
|
57
|
+
const head = ` slop level: ${paint(pc.bold(level))} ${pc.dim(`(${score.toFixed(1)}/100)`)}${anchor}${partial}`;
|
|
58
|
+
const parts = [];
|
|
59
|
+
if (densities.god !== void 0) parts.push(`god units ${densities.god.toFixed(2)}/KLOC`);
|
|
60
|
+
if (densities.complexity !== void 0) parts.push(`complex fns ${densities.complexity.toFixed(2)}/KLOC`);
|
|
61
|
+
if (densities.duplication !== void 0) parts.push(`duplication ${(densities.duplication * 100).toFixed(1)}%`);
|
|
62
|
+
if (densities.deadCode !== void 0) parts.push(`dead code ${densities.deadCode.toFixed(2)}/KLOC`);
|
|
63
|
+
return parts.length > 0 ? head + "\n " + pc.dim(parts.join(" · ")) : head;
|
|
64
|
+
}
|
|
65
|
+
const LEVEL_COLOR$1 = {
|
|
66
|
+
clean: pc.green,
|
|
67
|
+
low: pc.green,
|
|
68
|
+
moderate: pc.yellow,
|
|
69
|
+
high: pc.red,
|
|
70
|
+
"legacy-grade": pc.red
|
|
71
|
+
};
|
|
72
|
+
function renderFooter(result) {
|
|
73
|
+
const lines = [""];
|
|
74
|
+
for (const note of result.notes) lines.push(" " + pc.dim(note));
|
|
75
|
+
if (result.config.preliminary) lines.push(" " + pc.yellow("thresholds are preliminary (pre-calibration) — see docs/thresholds.md"));
|
|
76
|
+
if (result.errors.length > 0) lines.push(" " + pc.dim(`${result.errors.length} file(s) skipped on error`));
|
|
77
|
+
lines.push(" " + pc.dim("clones: Clone Alert · dead code: Knip"));
|
|
78
|
+
lines.push("");
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
function renderFinding(f, details) {
|
|
82
|
+
const tag = tagFor(f);
|
|
83
|
+
const loc = pc.dim(`${f.file}:${f.line}`);
|
|
84
|
+
const out = [` ${tag} ${f.message}`, ` ${loc}`];
|
|
85
|
+
if (f.kind === "duplicate-code") {
|
|
86
|
+
const rest = (f.data?.occurrences ?? []).slice(1);
|
|
87
|
+
const list = details ? rest : rest.slice(0, 3);
|
|
88
|
+
for (const o of list) out.push(" " + pc.dim(`also ${o.file}:${o.startLine}-${o.endLine}`));
|
|
89
|
+
if (!details && rest.length > list.length) out.push(" " + pc.dim(`also +${rest.length - list.length} more`));
|
|
90
|
+
return out.join("\n");
|
|
91
|
+
}
|
|
92
|
+
if (details && (f.kind === "god-module" || f.kind === "god-class")) {
|
|
93
|
+
const axis = f.data?.axis;
|
|
94
|
+
const groups = f.data?.groups ?? [];
|
|
95
|
+
const standalone = f.data?.standalone ?? [];
|
|
96
|
+
const hubs = f.data?.hubs ?? [];
|
|
97
|
+
if (hubs.length > 0) out.push(" " + pc.dim("cohesion held together by: ") + pc.yellow(hubs.join(", ")));
|
|
98
|
+
if (axis === "size" && groups.length < 3) {
|
|
99
|
+
out.push(" " + pc.dim("densely coupled — no clean split boundaries; decompose by responsibility manually"));
|
|
100
|
+
if (groups.length >= 2) {
|
|
101
|
+
out.push(" " + pc.dim("partial grouping:"));
|
|
102
|
+
groups.forEach((c, i) => out.push(" " + pc.dim(`${i + 1}.`) + " " + c.join(", ")));
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
out.push(" " + pc.dim(`${groups.length} responsibility groups (suggested split boundaries):`));
|
|
106
|
+
groups.forEach((c, i) => {
|
|
107
|
+
out.push(" " + pc.dim(`${i + 1}.`) + " " + c.join(", "));
|
|
108
|
+
});
|
|
109
|
+
if (standalone.length > 0) out.push(" " + pc.dim(`+ ${standalone.length} standalone: `) + pc.dim(previewList(standalone)));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out.join("\n");
|
|
113
|
+
}
|
|
114
|
+
function formatKloc(loc) {
|
|
115
|
+
return loc >= 1e3 ? `${(loc / 1e3).toFixed(1)} KLOC` : `${loc} LOC`;
|
|
116
|
+
}
|
|
117
|
+
function pluralize(label, n) {
|
|
118
|
+
if (n === 1) return label;
|
|
119
|
+
return /(s|x|ch|sh)$/.test(label) ? `${label}es` : `${label}s`;
|
|
120
|
+
}
|
|
121
|
+
function previewList(names, max = 8) {
|
|
122
|
+
if (names.length <= max) return names.join(", ");
|
|
123
|
+
return names.slice(0, max).join(", ") + `, …+${names.length - max}`;
|
|
124
|
+
}
|
|
125
|
+
function tagFor(f) {
|
|
126
|
+
const label = ` ${KIND_LABEL[f.kind]} `;
|
|
127
|
+
switch (f.kind) {
|
|
128
|
+
case "god-module":
|
|
129
|
+
case "god-class": return pc.bgRed(pc.white(pc.bold(label)));
|
|
130
|
+
case "cognitive-complexity":
|
|
131
|
+
case "duplicate-code": return pc.bgYellow(pc.black(label));
|
|
132
|
+
case "unused-export":
|
|
133
|
+
case "unused-dependency":
|
|
134
|
+
case "unused-file": return pc.bgMagenta(pc.white(label));
|
|
135
|
+
case "orphan-feature": return pc.bgRed(pc.white(pc.bold(label)));
|
|
136
|
+
default: return pc.bgBlue(pc.white(label));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function countByKind(findings) {
|
|
140
|
+
const counts = Object.fromEntries(KIND_ORDER.map((k) => [k, 0]));
|
|
141
|
+
for (const f of findings) counts[f.kind]++;
|
|
142
|
+
return counts;
|
|
143
|
+
}
|
|
144
|
+
const KINDS = [
|
|
145
|
+
"god-module",
|
|
146
|
+
"god-class",
|
|
147
|
+
"duplicate-code",
|
|
148
|
+
"cognitive-complexity",
|
|
149
|
+
"unused-export",
|
|
150
|
+
"unused-dependency",
|
|
151
|
+
"unused-file",
|
|
152
|
+
"orphan-feature",
|
|
153
|
+
"multiple-exported-classes"
|
|
154
|
+
];
|
|
155
|
+
/** Stable, versioned JSON for agents/CI. Sets replaced with sorted arrays. */
|
|
156
|
+
function toJson(result) {
|
|
157
|
+
return JSON.stringify({
|
|
158
|
+
schemaVersion: 6,
|
|
159
|
+
tool: "stopslop",
|
|
160
|
+
engines: {
|
|
161
|
+
duplicates: "clone-alert",
|
|
162
|
+
deadCode: "knip"
|
|
163
|
+
},
|
|
164
|
+
root: result.root,
|
|
165
|
+
fileCount: result.fileCount,
|
|
166
|
+
loc: result.loc,
|
|
167
|
+
elapsedMs: result.elapsedMs,
|
|
168
|
+
preliminaryThresholds: result.config.preliminary,
|
|
169
|
+
slop: result.slop,
|
|
170
|
+
...result.baselineSuppressed === void 0 ? {} : { baseline: {
|
|
171
|
+
suppressed: result.baselineSuppressed,
|
|
172
|
+
new: result.findings.length
|
|
173
|
+
} },
|
|
174
|
+
...result.gitBase === void 0 ? {} : { gitBase: {
|
|
175
|
+
...result.gitBase,
|
|
176
|
+
new: result.findings.length
|
|
177
|
+
} },
|
|
178
|
+
thresholds: {
|
|
179
|
+
cognitiveComplexity: result.config.cognitiveComplexity,
|
|
180
|
+
godModule: result.config.godModule,
|
|
181
|
+
godClass: result.config.godClass,
|
|
182
|
+
multipleExportedClasses: result.config.multipleExportedClasses,
|
|
183
|
+
duplicates: result.config.duplicates,
|
|
184
|
+
deadCode: result.config.deadCode,
|
|
185
|
+
knip: result.config.knip,
|
|
186
|
+
score: result.config.score,
|
|
187
|
+
hubFanInRatio: result.config.hubFanInRatio
|
|
188
|
+
},
|
|
189
|
+
summary: summarize(result),
|
|
190
|
+
findings: result.findings,
|
|
191
|
+
notes: result.notes,
|
|
192
|
+
errors: result.errors
|
|
193
|
+
}, null, 2);
|
|
194
|
+
}
|
|
195
|
+
function summarize(result) {
|
|
196
|
+
const counts = Object.fromEntries(KINDS.map((k) => [k, 0]));
|
|
197
|
+
for (const f of result.findings) counts[f.kind] = (counts[f.kind] ?? 0) + 1;
|
|
198
|
+
return counts;
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/report/sarif.ts
|
|
202
|
+
/** GitHub Code Scanning-compatible SARIF 2.1.0 report. */
|
|
203
|
+
function toSarif(result) {
|
|
204
|
+
const rules = [];
|
|
205
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
206
|
+
for (const finding of result.findings) {
|
|
207
|
+
if (seenRules.has(finding.kind)) continue;
|
|
208
|
+
seenRules.add(finding.kind);
|
|
209
|
+
rules.push({
|
|
210
|
+
id: finding.kind,
|
|
211
|
+
shortDescription: { text: describeKind(finding.kind) },
|
|
212
|
+
defaultConfiguration: { level: sarifLevel(finding.severity) }
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const sarif = {
|
|
216
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
217
|
+
version: "2.1.0",
|
|
218
|
+
runs: [{
|
|
219
|
+
tool: { driver: {
|
|
220
|
+
name: "stopslop",
|
|
221
|
+
rules
|
|
222
|
+
} },
|
|
223
|
+
results: result.findings.map((finding) => ({
|
|
224
|
+
ruleId: finding.kind,
|
|
225
|
+
level: sarifLevel(finding.severity),
|
|
226
|
+
message: { text: finding.message },
|
|
227
|
+
locations: locationsFor(result.root, finding),
|
|
228
|
+
partialFingerprints: { primaryLocationLineHash: createHash("sha256").update(fingerprint(finding)).digest("hex") }
|
|
229
|
+
}))
|
|
230
|
+
}]
|
|
231
|
+
};
|
|
232
|
+
return `${JSON.stringify(sarif, null, 2)}\n`;
|
|
233
|
+
}
|
|
234
|
+
function sarifLevel(severity) {
|
|
235
|
+
return severity === "warn" ? "warning" : "note";
|
|
236
|
+
}
|
|
237
|
+
function describeKind(kind) {
|
|
238
|
+
const description = kind.replaceAll("-", " ");
|
|
239
|
+
return `${description.charAt(0).toUpperCase()}${description.slice(1)}`;
|
|
240
|
+
}
|
|
241
|
+
function locationsFor(root, finding) {
|
|
242
|
+
const primaryUri = artifactUri(root, finding.file);
|
|
243
|
+
if (!primaryUri || !isLine(finding.line)) return [];
|
|
244
|
+
const occurrences = Array.isArray(finding.data?.occurrences) ? finding.data.occurrences.flatMap((value) => {
|
|
245
|
+
const parsed = parseOccurrence(root, value);
|
|
246
|
+
return parsed ? [parsed] : [];
|
|
247
|
+
}) : [];
|
|
248
|
+
const primary = occurrences.find((occurrence) => occurrence.uri === primaryUri && occurrence.startLine === finding.line);
|
|
249
|
+
const locations = [location(primaryUri, finding.line, primary?.endLine)];
|
|
250
|
+
const seen = new Set([JSON.stringify([primaryUri, finding.line])]);
|
|
251
|
+
for (const occurrence of occurrences) {
|
|
252
|
+
const key = JSON.stringify([occurrence.uri, occurrence.startLine]);
|
|
253
|
+
if (seen.has(key)) continue;
|
|
254
|
+
seen.add(key);
|
|
255
|
+
locations.push(location(occurrence.uri, occurrence.startLine, occurrence.endLine));
|
|
256
|
+
}
|
|
257
|
+
return locations;
|
|
258
|
+
}
|
|
259
|
+
function parseOccurrence(root, value) {
|
|
260
|
+
if (!value || typeof value !== "object") return void 0;
|
|
261
|
+
const { file, startLine, endLine } = value;
|
|
262
|
+
if (typeof file !== "string" || !isLine(startLine)) return void 0;
|
|
263
|
+
const uri = artifactUri(root, file);
|
|
264
|
+
if (!uri) return void 0;
|
|
265
|
+
return {
|
|
266
|
+
uri,
|
|
267
|
+
startLine,
|
|
268
|
+
...isLine(endLine) && endLine >= startLine ? { endLine } : {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function location(uri, startLine, endLine) {
|
|
272
|
+
return { physicalLocation: {
|
|
273
|
+
artifactLocation: { uri },
|
|
274
|
+
region: {
|
|
275
|
+
startLine,
|
|
276
|
+
...endLine === void 0 ? {} : { endLine }
|
|
277
|
+
}
|
|
278
|
+
} };
|
|
279
|
+
}
|
|
280
|
+
function artifactUri(root, file) {
|
|
281
|
+
const normalizedRoot = root.replaceAll("\\", "/");
|
|
282
|
+
const normalizedFile = file.replaceAll("\\", "/");
|
|
283
|
+
const relative = normalizedFile.startsWith("/") || /^[A-Za-z]:\//.test(normalizedFile) ? posix.relative(normalizedRoot, normalizedFile) : normalizedFile;
|
|
284
|
+
const normalized = posix.normalize(relative).replace(/^\.\//, "");
|
|
285
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z][A-Za-z\d+.-]*:/.test(normalized)) return;
|
|
286
|
+
return normalized.split("/").map(encodeURIComponent).join("/");
|
|
287
|
+
}
|
|
288
|
+
function isLine(value) {
|
|
289
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/report/badge.ts
|
|
293
|
+
const LEVEL_COLOR = {
|
|
294
|
+
clean: "brightgreen",
|
|
295
|
+
low: "green",
|
|
296
|
+
moderate: "yellow",
|
|
297
|
+
high: "orange",
|
|
298
|
+
"legacy-grade": "red"
|
|
299
|
+
};
|
|
300
|
+
/** shields.io endpoint JSON for a slop badge. */
|
|
301
|
+
function toBadge(result) {
|
|
302
|
+
const { score, level, signalCount, totalSignals } = result.slop;
|
|
303
|
+
const partial = signalCount < totalSignals ? "*" : "";
|
|
304
|
+
const payload = {
|
|
305
|
+
schemaVersion: 1,
|
|
306
|
+
label: "slop",
|
|
307
|
+
message: score === 0 ? "0 slop" : `${level} (${score.toFixed(0)}/100)${partial}`,
|
|
308
|
+
color: LEVEL_COLOR[level]
|
|
309
|
+
};
|
|
310
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/cli.ts
|
|
314
|
+
function parseArgs(argv) {
|
|
315
|
+
const args = {
|
|
316
|
+
path: ".",
|
|
317
|
+
format: "text",
|
|
318
|
+
details: false,
|
|
319
|
+
fast: false,
|
|
320
|
+
updateBaseline: false,
|
|
321
|
+
help: false
|
|
322
|
+
};
|
|
323
|
+
const positional = [];
|
|
324
|
+
for (let i = 0; i < argv.length; i++) {
|
|
325
|
+
const a = argv[i];
|
|
326
|
+
if (a === "--json") args.format = "json";
|
|
327
|
+
else if (a === "--format") args.format = parseFormat(optionValue("--format", argv[++i]));
|
|
328
|
+
else if (a.startsWith("--format=")) args.format = parseFormat(optionValue("--format", a.slice(9)));
|
|
329
|
+
else if (a === "--details" || a === "-d") args.details = true;
|
|
330
|
+
else if (a === "--fast") args.fast = true;
|
|
331
|
+
else if (a === "--help" || a === "-h") args.help = true;
|
|
332
|
+
else if (a === "--config") args.config = optionValue("--config", argv[++i]);
|
|
333
|
+
else if (a.startsWith("--config=")) args.config = optionValue("--config", a.slice(9));
|
|
334
|
+
else if (a === "--base") args.base = optionValue("--base", argv[++i]);
|
|
335
|
+
else if (a.startsWith("--base=")) args.base = optionValue("--base", a.slice(7));
|
|
336
|
+
else if (a === "--baseline") args.baseline = optionValue("--baseline", argv[++i]);
|
|
337
|
+
else if (a.startsWith("--baseline=")) args.baseline = optionValue("--baseline", a.slice(11));
|
|
338
|
+
else if (a === "--update-baseline") args.updateBaseline = true;
|
|
339
|
+
else if (a.startsWith("-")) throw new Error(`unknown option '${a}'`);
|
|
340
|
+
else positional.push(a);
|
|
341
|
+
}
|
|
342
|
+
if (positional.length > 1) throw new Error("expected at most one path");
|
|
343
|
+
if (positional[0]) args.path = positional[0];
|
|
344
|
+
return args;
|
|
345
|
+
}
|
|
346
|
+
function optionValue(option, raw) {
|
|
347
|
+
if (!raw || raw.startsWith("-")) throw new Error(`${option} requires a value`);
|
|
348
|
+
return raw;
|
|
349
|
+
}
|
|
350
|
+
function parseFormat(raw) {
|
|
351
|
+
if (raw === "text" || raw === "json" || raw === "sarif" || raw === "shields") return raw;
|
|
352
|
+
throw new Error(`unknown --format '${raw ?? ""}' (expected text, json, sarif, or shields)`);
|
|
353
|
+
}
|
|
354
|
+
const HELP = `stopslop — find AI-agent slop that linters miss
|
|
355
|
+
|
|
356
|
+
Usage:
|
|
357
|
+
stopslop [path] analyze a directory or file (default: .)
|
|
358
|
+
|
|
359
|
+
Options:
|
|
360
|
+
--format <fmt> text (default) · json · sarif · shields (badge JSON)
|
|
361
|
+
--json alias for --format json
|
|
362
|
+
--details, -d show cluster composition, hubs, all clone sites
|
|
363
|
+
--fast AST checks only: skip clones and dead code
|
|
364
|
+
--config <file> path to a stopslop.json config
|
|
365
|
+
--base <ref> gate only on findings absent from this Git revision
|
|
366
|
+
--baseline <file> gate only on findings not in this baseline file
|
|
367
|
+
--update-baseline write the current findings to --baseline and exit 0
|
|
368
|
+
--help, -h show this help
|
|
369
|
+
|
|
370
|
+
Checks: complexity, god module/class (own) · clones (Clone Alert) · dead code (Knip)
|
|
371
|
+
Exit codes: 0 clean · 1 findings · 2 error
|
|
372
|
+
|
|
373
|
+
Baseline: adopt an existing repo's findings, then fail only on new ones.
|
|
374
|
+
--base is mutually exclusive with --baseline and --update-baseline.
|
|
375
|
+
stopslop . --base main # gate vs Git
|
|
376
|
+
stopslop . --baseline .stopslop-baseline.json --update-baseline # record
|
|
377
|
+
stopslop . --baseline .stopslop-baseline.json # gate on new`;
|
|
378
|
+
async function main() {
|
|
379
|
+
const args = parseArgs(process.argv.slice(2));
|
|
380
|
+
if (args.help) {
|
|
381
|
+
process.stdout.write(HELP + "\n");
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
validateArgs(args);
|
|
386
|
+
const config = loadConfig(args.path, args.config);
|
|
387
|
+
if (args.fast) {
|
|
388
|
+
config.duplicates.enabled = false;
|
|
389
|
+
config.deadCode.enabled = false;
|
|
390
|
+
}
|
|
391
|
+
const result = args.base ? await analyzeGitBase(args.path, args.base, config) : await analyze(args.path, config);
|
|
392
|
+
if (args.updateBaseline) {
|
|
393
|
+
writeBaseline(args.baseline, result.findings);
|
|
394
|
+
process.stderr.write(`stopslop: baseline written to ${args.baseline} (${result.findings.length} findings accepted)\n`);
|
|
395
|
+
process.exit(0);
|
|
396
|
+
}
|
|
397
|
+
let reported = result;
|
|
398
|
+
if (args.baseline) {
|
|
399
|
+
const { kept, suppressed } = applyBaseline(result.findings, readBaseline(args.baseline));
|
|
400
|
+
reported = {
|
|
401
|
+
...result,
|
|
402
|
+
findings: kept,
|
|
403
|
+
baselineSuppressed: suppressed
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (args.format === "json") process.stdout.write(toJson(reported) + "\n");
|
|
407
|
+
else if (args.format === "sarif") process.stdout.write(toSarif(reported));
|
|
408
|
+
else if (args.format === "shields") {
|
|
409
|
+
process.stdout.write(toBadge(result));
|
|
410
|
+
process.exit(0);
|
|
411
|
+
} else process.stdout.write(renderTerminal(reported, args.details) + "\n");
|
|
412
|
+
process.exit(reported.findings.length > 0 ? 1 : 0);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
process.stderr.write("stopslop: " + err.message + "\n");
|
|
415
|
+
process.exit(2);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function validateArgs(args) {
|
|
419
|
+
if (args.base && (args.baseline || args.updateBaseline)) throw new Error("--base is mutually exclusive with --baseline and --update-baseline");
|
|
420
|
+
if (args.updateBaseline && !args.baseline) throw new Error("--update-baseline requires --baseline <file>");
|
|
421
|
+
}
|
|
422
|
+
main().catch((err) => {
|
|
423
|
+
process.stderr.write("stopslop: " + err.message + "\n");
|
|
424
|
+
process.exit(2);
|
|
425
|
+
});
|
|
426
|
+
//#endregion
|
|
427
|
+
export {};
|