@wbern/obscene 0.1.1 → 0.2.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/dist/cli.js +114 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/analyze.ts
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
8
9
|
var DEFAULT_EXCLUDES = [
|
|
9
10
|
/\.test\./,
|
|
10
11
|
/\.spec\./,
|
|
@@ -64,31 +65,123 @@ function runScc(excludes = []) {
|
|
|
64
65
|
}
|
|
65
66
|
return files.sort((a, b) => b.complexity - a.complexity);
|
|
66
67
|
}
|
|
68
|
+
function gitFileCount(gitArgs, errorMessage) {
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = execSync(gitArgs, {
|
|
72
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(errorMessage);
|
|
77
|
+
}
|
|
78
|
+
const counts = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const line of raw.toString().split("\n")) {
|
|
80
|
+
const trimmed = normalizePath(line.trim());
|
|
81
|
+
if (!trimmed) continue;
|
|
82
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
83
|
+
}
|
|
84
|
+
return counts;
|
|
85
|
+
}
|
|
67
86
|
function getChurn(months) {
|
|
87
|
+
return gitFileCount(
|
|
88
|
+
`git log --since="${months} months ago" --format="" --name-only`,
|
|
89
|
+
"Not a git repository or git is not installed."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function getDefects(months) {
|
|
93
|
+
return gitFileCount(
|
|
94
|
+
`git log --since="${months} months ago" --grep="^fix" --format="" --name-only`,
|
|
95
|
+
"Not a git repository or git is not installed."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
function getAuthors(months) {
|
|
68
99
|
let raw;
|
|
69
100
|
try {
|
|
70
101
|
raw = execSync(
|
|
71
|
-
`git log --since="${months} months ago" --format="" --name-only`,
|
|
102
|
+
`git log --since="${months} months ago" --format="COMMIT_SEP%n%aN" --name-only`,
|
|
72
103
|
{ maxBuffer: 50 * 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
|
|
73
104
|
);
|
|
74
105
|
} catch {
|
|
75
106
|
throw new Error("Not a git repository or git is not installed.");
|
|
76
107
|
}
|
|
108
|
+
const authorSets = /* @__PURE__ */ new Map();
|
|
109
|
+
const blocks = raw.toString().split("COMMIT_SEP\n");
|
|
110
|
+
for (const block of blocks) {
|
|
111
|
+
if (!block.trim()) continue;
|
|
112
|
+
const lines = block.split("\n");
|
|
113
|
+
const author = lines[0].trim();
|
|
114
|
+
if (!author) continue;
|
|
115
|
+
for (let i = 1; i < lines.length; i++) {
|
|
116
|
+
const file = normalizePath(lines[i].trim());
|
|
117
|
+
if (!file) continue;
|
|
118
|
+
let set = authorSets.get(file);
|
|
119
|
+
if (!set) {
|
|
120
|
+
set = /* @__PURE__ */ new Set();
|
|
121
|
+
authorSets.set(file, set);
|
|
122
|
+
}
|
|
123
|
+
set.add(author);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
77
126
|
const counts = /* @__PURE__ */ new Map();
|
|
78
|
-
for (const
|
|
79
|
-
|
|
80
|
-
if (!trimmed) continue;
|
|
81
|
-
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
127
|
+
for (const [file, set] of authorSets) {
|
|
128
|
+
counts.set(file, set.size);
|
|
82
129
|
}
|
|
83
130
|
return counts;
|
|
84
131
|
}
|
|
85
|
-
function
|
|
132
|
+
function getNestingDepths(filePaths) {
|
|
133
|
+
const depths = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const filePath of filePaths) {
|
|
135
|
+
let content;
|
|
136
|
+
try {
|
|
137
|
+
content = readFileSync(filePath, "utf-8");
|
|
138
|
+
} catch {
|
|
139
|
+
depths.set(filePath, 0);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let minSpaces = Number.POSITIVE_INFINITY;
|
|
143
|
+
const leadings = [];
|
|
144
|
+
for (const line of content.split("\n")) {
|
|
145
|
+
if (!line.trim()) continue;
|
|
146
|
+
const match = line.match(/^(\s+)/);
|
|
147
|
+
if (!match) continue;
|
|
148
|
+
const leading = match[1];
|
|
149
|
+
leadings.push(leading);
|
|
150
|
+
const spaceCount = (leading.match(/ /g) ?? []).length;
|
|
151
|
+
if (spaceCount > 0 && !leading.includes(" ") && spaceCount < minSpaces) {
|
|
152
|
+
minSpaces = spaceCount;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const indentUnit = minSpaces === Number.POSITIVE_INFINITY ? 4 : minSpaces;
|
|
156
|
+
let maxDepth = 0;
|
|
157
|
+
for (const leading of leadings) {
|
|
158
|
+
let depth = 0;
|
|
159
|
+
for (const ch of leading) {
|
|
160
|
+
if (ch === " ") {
|
|
161
|
+
depth += 1;
|
|
162
|
+
} else if (ch === " ") {
|
|
163
|
+
depth += 1 / indentUnit;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
depth = Math.floor(depth);
|
|
167
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
168
|
+
}
|
|
169
|
+
depths.set(filePath, maxDepth);
|
|
170
|
+
}
|
|
171
|
+
return depths;
|
|
172
|
+
}
|
|
173
|
+
function computeHotspots(files, churn, defects = /* @__PURE__ */ new Map(), nestingDepths = /* @__PURE__ */ new Map(), authors = /* @__PURE__ */ new Map()) {
|
|
86
174
|
const scored = files.map((f) => {
|
|
87
175
|
const fileChurn = churn.get(f.file) ?? 0;
|
|
176
|
+
const fileDefects = defects.get(f.file) ?? 0;
|
|
88
177
|
return {
|
|
89
178
|
...f,
|
|
90
179
|
churn: fileChurn,
|
|
91
|
-
hotspotScore: f.complexity * fileChurn
|
|
180
|
+
hotspotScore: f.complexity * fileChurn,
|
|
181
|
+
defects: fileDefects,
|
|
182
|
+
defectDensity: f.code > 0 ? Math.round(fileDefects / f.code * 1e4) / 1e4 : 0,
|
|
183
|
+
maxNesting: nestingDepths.get(f.file) ?? 0,
|
|
184
|
+
authors: authors.get(f.file) ?? 0
|
|
92
185
|
};
|
|
93
186
|
}).filter((h) => h.hotspotScore > 0).sort((a, b) => b.hotspotScore - a.hotspotScore);
|
|
94
187
|
const totalScore = scored.reduce((sum, h) => sum + h.hotspotScore, 0);
|
|
@@ -144,13 +237,13 @@ function formatHotspotsTable(output) {
|
|
|
144
237
|
lines.push(`Showing: ${output.showing} of ${output.totalHotspots}`);
|
|
145
238
|
lines.push("");
|
|
146
239
|
lines.push(
|
|
147
|
-
padRight("File", 50) + padLeft("Score", 8) + padLeft("%", 7) + padLeft("Churn", 7) + padLeft("Cmplx", 7) + padLeft("
|
|
240
|
+
padRight("File", 50) + padLeft("Score", 8) + padLeft("%", 7) + padLeft("Churn", 7) + padLeft("Cmplx", 7) + padLeft("Dens", 7) + padLeft("Dfcts", 6) + padLeft("Nest", 6) + padLeft("Auth", 6) + padLeft("Tier", 8)
|
|
148
241
|
);
|
|
149
|
-
lines.push("\u2500".repeat(
|
|
242
|
+
lines.push("\u2500".repeat(112));
|
|
150
243
|
for (const h of hotspots) {
|
|
151
244
|
const tierLabel = h.tier === "danger" ? "DANGER" : h.tier === "watch" ? "WATCH" : "stable";
|
|
152
245
|
lines.push(
|
|
153
|
-
padRight(truncate(h.file, 48), 50) + padLeft(h.hotspotScore.toLocaleString(), 8) + padLeft(h.percentOfTotal.toFixed(1), 7) + padLeft(String(h.churn), 7) + padLeft(String(h.complexity), 7) + padLeft(h.complexityDensity.toFixed(2),
|
|
246
|
+
padRight(truncate(h.file, 48), 50) + padLeft(h.hotspotScore.toLocaleString(), 8) + padLeft(h.percentOfTotal.toFixed(1), 7) + padLeft(String(h.churn), 7) + padLeft(String(h.complexity), 7) + padLeft(h.complexityDensity.toFixed(2), 7) + padLeft(String(h.defects), 6) + padLeft(String(h.maxNesting), 6) + padLeft(String(h.authors), 6) + padLeft(tierLabel, 8)
|
|
154
247
|
);
|
|
155
248
|
}
|
|
156
249
|
return lines.join("\n");
|
|
@@ -167,7 +260,7 @@ function truncate(s, max) {
|
|
|
167
260
|
|
|
168
261
|
// src/cli.ts
|
|
169
262
|
var program = new Command();
|
|
170
|
-
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("0.
|
|
263
|
+
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("0.2.0");
|
|
171
264
|
function addSharedOptions(cmd) {
|
|
172
265
|
return cmd.option("--top <n>", "limit to top N entries (0 = all)", "20").option("--format <type>", "output format: json | table", "json").option(
|
|
173
266
|
"--exclude <patterns...>",
|
|
@@ -227,7 +320,16 @@ function runHotspots(opts) {
|
|
|
227
320
|
const months = parseInt(opts.months, 10);
|
|
228
321
|
const files = runScc(opts.exclude);
|
|
229
322
|
const churn = getChurn(months);
|
|
230
|
-
const
|
|
323
|
+
const defects = getDefects(months);
|
|
324
|
+
const authors = getAuthors(months);
|
|
325
|
+
const nestingDepths = getNestingDepths(files.map((f) => f.file));
|
|
326
|
+
const hotspots = computeHotspots(
|
|
327
|
+
files,
|
|
328
|
+
churn,
|
|
329
|
+
defects,
|
|
330
|
+
nestingDepths,
|
|
331
|
+
authors
|
|
332
|
+
);
|
|
231
333
|
const limited = top > 0 ? hotspots.slice(0, top) : hotspots;
|
|
232
334
|
const tierCounts = { danger: 0, watch: 0, stable: 0 };
|
|
233
335
|
for (const h of hotspots) {
|