@wbern/obscene 1.4.0 → 1.5.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 +138 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { existsSync, writeFileSync } from "fs";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/analyze.ts
|
|
@@ -385,6 +386,106 @@ function getNestingDepths(filePaths) {
|
|
|
385
386
|
}
|
|
386
387
|
return depths;
|
|
387
388
|
}
|
|
389
|
+
var INIT_DIR_RULES = [
|
|
390
|
+
{
|
|
391
|
+
dir: ".github",
|
|
392
|
+
pattern: ".github/**",
|
|
393
|
+
comment: "GitHub Actions and workflows"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
dir: ".circleci",
|
|
397
|
+
pattern: ".circleci/**",
|
|
398
|
+
comment: "CircleCI configuration"
|
|
399
|
+
},
|
|
400
|
+
{ dir: ".husky", pattern: ".husky/**", comment: "Git hooks" },
|
|
401
|
+
{ dir: ".vscode", pattern: ".vscode/**", comment: "VS Code settings" },
|
|
402
|
+
{ dir: ".idea", pattern: ".idea/**", comment: "JetBrains settings" },
|
|
403
|
+
{
|
|
404
|
+
dir: "scripts",
|
|
405
|
+
pattern: "scripts/**",
|
|
406
|
+
comment: "Build and utility scripts"
|
|
407
|
+
},
|
|
408
|
+
{ dir: "docs", pattern: "docs/**", comment: "Documentation" },
|
|
409
|
+
{ dir: "docker", pattern: "docker/**", comment: "Docker configuration" },
|
|
410
|
+
{
|
|
411
|
+
dir: "fixtures",
|
|
412
|
+
pattern: "fixtures/**",
|
|
413
|
+
comment: "Test fixtures"
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
dir: "vendor",
|
|
417
|
+
pattern: "vendor/**",
|
|
418
|
+
comment: "Vendored dependencies"
|
|
419
|
+
}
|
|
420
|
+
];
|
|
421
|
+
var INIT_FILE_RULES = [
|
|
422
|
+
{
|
|
423
|
+
test: /\.generated\./,
|
|
424
|
+
pattern: "*.generated.*",
|
|
425
|
+
comment: "Generated code"
|
|
426
|
+
},
|
|
427
|
+
{ test: /\.gen\.[^.]+$/, pattern: "*.gen.*", comment: "Generated code" },
|
|
428
|
+
{
|
|
429
|
+
test: /\.config\.\w/,
|
|
430
|
+
pattern: "*.config.*",
|
|
431
|
+
comment: "Configuration files"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
test: /(?:^|\/)\.gitlab-ci/,
|
|
435
|
+
pattern: ".gitlab-ci*",
|
|
436
|
+
comment: "GitLab CI configuration"
|
|
437
|
+
}
|
|
438
|
+
];
|
|
439
|
+
function detectIgnorePatterns() {
|
|
440
|
+
let raw;
|
|
441
|
+
try {
|
|
442
|
+
raw = execSync("git ls-files", {
|
|
443
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
444
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
445
|
+
});
|
|
446
|
+
} catch {
|
|
447
|
+
throw new Error("Not a git repository or git is not installed.");
|
|
448
|
+
}
|
|
449
|
+
const trackedFiles = raw.toString().split("\n").map((l) => normalizePath(l.trim())).filter(Boolean);
|
|
450
|
+
const patterns = [];
|
|
451
|
+
const topDirs = /* @__PURE__ */ new Set();
|
|
452
|
+
for (const f of trackedFiles) {
|
|
453
|
+
const slash = f.indexOf("/");
|
|
454
|
+
if (slash > 0) topDirs.add(f.slice(0, slash));
|
|
455
|
+
}
|
|
456
|
+
for (const rule of INIT_DIR_RULES) {
|
|
457
|
+
if (topDirs.has(rule.dir)) {
|
|
458
|
+
patterns.push({ pattern: rule.pattern, comment: rule.comment });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
for (const rule of INIT_FILE_RULES) {
|
|
462
|
+
if (trackedFiles.some((f) => rule.test.test(f))) {
|
|
463
|
+
patterns.push({ pattern: rule.pattern, comment: rule.comment });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return patterns;
|
|
467
|
+
}
|
|
468
|
+
function formatIgnoreFile(patterns) {
|
|
469
|
+
const lines = [
|
|
470
|
+
"# Generated by obscene init",
|
|
471
|
+
"# Edit this file to customize which files are excluded from analysis.",
|
|
472
|
+
"# Patterns use glob syntax (same as .gitignore).",
|
|
473
|
+
"# See: https://github.com/wbern/obscene#ignore-files",
|
|
474
|
+
""
|
|
475
|
+
];
|
|
476
|
+
if (patterns.length === 0) {
|
|
477
|
+
lines.push("# No project-specific patterns detected.");
|
|
478
|
+
lines.push("# Add glob patterns here, one per line.");
|
|
479
|
+
lines.push("");
|
|
480
|
+
} else {
|
|
481
|
+
for (const p of patterns) {
|
|
482
|
+
lines.push(`# ${p.comment}`);
|
|
483
|
+
lines.push(p.pattern);
|
|
484
|
+
lines.push("");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return lines.join("\n");
|
|
488
|
+
}
|
|
388
489
|
var RRF_K = 10;
|
|
389
490
|
function computeComposite(rankings, churn, top) {
|
|
390
491
|
const totalDimensions = Object.keys(rankings).length;
|
|
@@ -756,7 +857,7 @@ function formatCompositeTable(output) {
|
|
|
756
857
|
|
|
757
858
|
// src/cli.ts
|
|
758
859
|
var program = new Command();
|
|
759
|
-
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.
|
|
860
|
+
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.5.0");
|
|
760
861
|
var REPORT_GUIDE = {
|
|
761
862
|
complexity: "Cyclomatic complexity (branch/loop count). NOT a quality judgment \u2014 a 500-line parser will naturally score high. Compare density, not raw values.",
|
|
762
863
|
complexityDensity: "Complexity per line of code. Normalizes for file size. >0.25 suggests dense logic worth reviewing; <0.10 is typical for straightforward code.",
|
|
@@ -812,6 +913,13 @@ addSharedOptions(
|
|
|
812
913
|
exitWithError(err);
|
|
813
914
|
}
|
|
814
915
|
});
|
|
916
|
+
program.command("init").description("generate a starter .obsignore based on project structure").action(() => {
|
|
917
|
+
try {
|
|
918
|
+
runInit();
|
|
919
|
+
} catch (err) {
|
|
920
|
+
exitWithError(err);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
815
923
|
function resolveExcludes(cliExcludes) {
|
|
816
924
|
return [...readIgnoreFile(), ...cliExcludes ?? []];
|
|
817
925
|
}
|
|
@@ -929,6 +1037,35 @@ function runCoupling(opts) {
|
|
|
929
1037
|
`);
|
|
930
1038
|
}
|
|
931
1039
|
}
|
|
1040
|
+
function runInit() {
|
|
1041
|
+
if (existsSync(".obsignore")) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
".obsignore already exists. Remove it first to regenerate."
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
if (existsSync(".obsceneignore")) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
".obsceneignore already exists. Remove it first to regenerate."
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
const patterns = detectIgnorePatterns();
|
|
1052
|
+
const content = formatIgnoreFile(patterns);
|
|
1053
|
+
writeFileSync(".obsignore", content);
|
|
1054
|
+
if (patterns.length === 0) {
|
|
1055
|
+
process.stderr.write(
|
|
1056
|
+
"Created .obsignore (no project-specific patterns detected)\n"
|
|
1057
|
+
);
|
|
1058
|
+
} else {
|
|
1059
|
+
process.stderr.write(
|
|
1060
|
+
`Created .obsignore with ${patterns.length} patterns:
|
|
1061
|
+
`
|
|
1062
|
+
);
|
|
1063
|
+
for (const p of patterns) {
|
|
1064
|
+
process.stderr.write(` ${p.pattern.padEnd(20)} ${p.comment}
|
|
1065
|
+
`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
932
1069
|
function exitWithError(err) {
|
|
933
1070
|
const message = err instanceof Error ? err.message : String(err);
|
|
934
1071
|
process.stderr.write(`Error: ${message}
|