@wbern/obscene 1.4.0 → 2.0.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/README.md +18 -2
- package/dist/cli.js +183 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -252,9 +252,25 @@ Docs: https://github.com/wbern/obscene#metrics
|
|
|
252
252
|
|
|
253
253
|
Any language [scc supports](https://github.com/boyter/scc#features) — 200+ languages including C, C++, Go, Java, JavaScript, TypeScript, Python, Rust, Ruby, PHP, Swift, Kotlin, and many more. No configuration needed; scc auto-detects languages from file extensions.
|
|
254
254
|
|
|
255
|
-
##
|
|
255
|
+
## Exclusions
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
All exclusions are opt-in. Run `obscene init` to generate a `.obsignore` file with recommended patterns for your project:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
obscene init
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
This creates a `.obsignore` containing:
|
|
264
|
+
- **Universal exclusions** — test files (`*.test.*`, `*.spec.*`, `__tests__/`, etc.), lock files (`package-lock.json`, `pnpm-lock.yaml`, etc.), and package manifests (`package.json`)
|
|
265
|
+
- **Detected project patterns** — CI directories (`.github/`), config files (`*.config.*`), vendored code, etc., based on your project structure
|
|
266
|
+
|
|
267
|
+
If no `.obsignore` or `.obsceneignore` exists, obscene prints a hint to stderr:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
hint: no .obsignore found — run `obscene init` to generate one with recommended exclusions
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
scc also skips generated files by default (`--no-gen`).
|
|
258
274
|
|
|
259
275
|
## Ignore files
|
|
260
276
|
|
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
|
|
@@ -21,22 +22,32 @@ function readIgnoreFile() {
|
|
|
21
22
|
}
|
|
22
23
|
return [];
|
|
23
24
|
}
|
|
24
|
-
var
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
var UNIVERSAL_IGNORE_GROUPS = [
|
|
26
|
+
{
|
|
27
|
+
title: "Test files and test infrastructure",
|
|
28
|
+
patterns: [
|
|
29
|
+
{ pattern: "*.test.*", comment: "Unit test files" },
|
|
30
|
+
{ pattern: "*.spec.*", comment: "Spec test files" },
|
|
31
|
+
{ pattern: "*.integration.test.*", comment: "Integration tests" },
|
|
32
|
+
{ pattern: "test-setup.*", comment: "Test setup files" },
|
|
33
|
+
{ pattern: "test-utils.*", comment: "Test utility files" },
|
|
34
|
+
{ pattern: "test-helpers.*", comment: "Test helper files" },
|
|
35
|
+
{ pattern: "__tests__/**", comment: "Test directories" },
|
|
36
|
+
{ pattern: "__mocks__/**", comment: "Mock directories" },
|
|
37
|
+
{ pattern: "*.stories.*", comment: "Storybook stories" },
|
|
38
|
+
{ pattern: "*.d.ts", comment: "TypeScript declaration files" }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "Lock files and package manifests",
|
|
43
|
+
patterns: [
|
|
44
|
+
{ pattern: "package.json", comment: "npm package manifest" },
|
|
45
|
+
{ pattern: "package-lock.json", comment: "npm lock file" },
|
|
46
|
+
{ pattern: "pnpm-lock.yaml", comment: "pnpm lock file" },
|
|
47
|
+
{ pattern: "yarn.lock", comment: "Yarn lock file" },
|
|
48
|
+
{ pattern: "bun.lock", comment: "Bun lock file" }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
40
51
|
];
|
|
41
52
|
var HOT_CUMULATIVE = 0.5;
|
|
42
53
|
var WARM_CUMULATIVE = 0.8;
|
|
@@ -54,7 +65,7 @@ function normalizePath(p) {
|
|
|
54
65
|
return forwardSlash.startsWith("./") ? forwardSlash.slice(2) : forwardSlash;
|
|
55
66
|
}
|
|
56
67
|
function runScc(excludes = []) {
|
|
57
|
-
const patterns =
|
|
68
|
+
const patterns = excludes.map(globToRegex);
|
|
58
69
|
let raw;
|
|
59
70
|
try {
|
|
60
71
|
raw = execSync("scc --by-file --format json --no-cocomo --no-gen", {
|
|
@@ -153,7 +164,7 @@ function getAuthors(months) {
|
|
|
153
164
|
}
|
|
154
165
|
var MAX_FILES_PER_COMMIT = 20;
|
|
155
166
|
function getCoChanges(months, excludes = []) {
|
|
156
|
-
const patterns =
|
|
167
|
+
const patterns = excludes.map(globToRegex);
|
|
157
168
|
let raw;
|
|
158
169
|
try {
|
|
159
170
|
raw = execSync(
|
|
@@ -385,6 +396,110 @@ function getNestingDepths(filePaths) {
|
|
|
385
396
|
}
|
|
386
397
|
return depths;
|
|
387
398
|
}
|
|
399
|
+
var INIT_DIR_RULES = [
|
|
400
|
+
{
|
|
401
|
+
dir: ".github",
|
|
402
|
+
pattern: ".github/**",
|
|
403
|
+
comment: "GitHub Actions and workflows"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
dir: ".circleci",
|
|
407
|
+
pattern: ".circleci/**",
|
|
408
|
+
comment: "CircleCI configuration"
|
|
409
|
+
},
|
|
410
|
+
{ dir: ".husky", pattern: ".husky/**", comment: "Git hooks" },
|
|
411
|
+
{ dir: ".vscode", pattern: ".vscode/**", comment: "VS Code settings" },
|
|
412
|
+
{ dir: ".idea", pattern: ".idea/**", comment: "JetBrains settings" },
|
|
413
|
+
{
|
|
414
|
+
dir: "scripts",
|
|
415
|
+
pattern: "scripts/**",
|
|
416
|
+
comment: "Build and utility scripts"
|
|
417
|
+
},
|
|
418
|
+
{ dir: "docs", pattern: "docs/**", comment: "Documentation" },
|
|
419
|
+
{ dir: "docker", pattern: "docker/**", comment: "Docker configuration" },
|
|
420
|
+
{
|
|
421
|
+
dir: "fixtures",
|
|
422
|
+
pattern: "fixtures/**",
|
|
423
|
+
comment: "Test fixtures"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
dir: "vendor",
|
|
427
|
+
pattern: "vendor/**",
|
|
428
|
+
comment: "Vendored dependencies"
|
|
429
|
+
}
|
|
430
|
+
];
|
|
431
|
+
var INIT_FILE_RULES = [
|
|
432
|
+
{
|
|
433
|
+
test: /\.generated\./,
|
|
434
|
+
pattern: "*.generated.*",
|
|
435
|
+
comment: "Generated code"
|
|
436
|
+
},
|
|
437
|
+
{ test: /\.gen\.[^.]+$/, pattern: "*.gen.*", comment: "Generated code" },
|
|
438
|
+
{
|
|
439
|
+
test: /\.config\.\w/,
|
|
440
|
+
pattern: "*.config.*",
|
|
441
|
+
comment: "Configuration files"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
test: /(?:^|\/)\.gitlab-ci/,
|
|
445
|
+
pattern: ".gitlab-ci*",
|
|
446
|
+
comment: "GitLab CI configuration"
|
|
447
|
+
}
|
|
448
|
+
];
|
|
449
|
+
function detectIgnorePatterns() {
|
|
450
|
+
let raw;
|
|
451
|
+
try {
|
|
452
|
+
raw = execSync("git ls-files", {
|
|
453
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
454
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
455
|
+
});
|
|
456
|
+
} catch {
|
|
457
|
+
throw new Error("Not a git repository or git is not installed.");
|
|
458
|
+
}
|
|
459
|
+
const trackedFiles = raw.toString().split("\n").map((l) => normalizePath(l.trim())).filter(Boolean);
|
|
460
|
+
const patterns = [];
|
|
461
|
+
const topDirs = /* @__PURE__ */ new Set();
|
|
462
|
+
for (const f of trackedFiles) {
|
|
463
|
+
const slash = f.indexOf("/");
|
|
464
|
+
if (slash > 0) topDirs.add(f.slice(0, slash));
|
|
465
|
+
}
|
|
466
|
+
for (const rule of INIT_DIR_RULES) {
|
|
467
|
+
if (topDirs.has(rule.dir)) {
|
|
468
|
+
patterns.push({ pattern: rule.pattern, comment: rule.comment });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for (const rule of INIT_FILE_RULES) {
|
|
472
|
+
if (trackedFiles.some((f) => rule.test.test(f))) {
|
|
473
|
+
patterns.push({ pattern: rule.pattern, comment: rule.comment });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return patterns;
|
|
477
|
+
}
|
|
478
|
+
function formatIgnoreFile(detectedPatterns, universalGroups = UNIVERSAL_IGNORE_GROUPS) {
|
|
479
|
+
const lines = [
|
|
480
|
+
"# Generated by obscene init",
|
|
481
|
+
"# Edit this file to customize which files are excluded from analysis.",
|
|
482
|
+
"# Patterns use glob syntax (same as .gitignore).",
|
|
483
|
+
"# See: https://github.com/wbern/obscene#ignore-files",
|
|
484
|
+
""
|
|
485
|
+
];
|
|
486
|
+
for (const group of universalGroups) {
|
|
487
|
+
lines.push(`# ${group.title}`);
|
|
488
|
+
for (const p of group.patterns) {
|
|
489
|
+
lines.push(p.pattern);
|
|
490
|
+
}
|
|
491
|
+
lines.push("");
|
|
492
|
+
}
|
|
493
|
+
if (detectedPatterns.length > 0) {
|
|
494
|
+
lines.push("# Project-specific patterns");
|
|
495
|
+
for (const p of detectedPatterns) {
|
|
496
|
+
lines.push(`# ${p.comment}`);
|
|
497
|
+
lines.push(p.pattern);
|
|
498
|
+
}
|
|
499
|
+
lines.push("");
|
|
500
|
+
}
|
|
501
|
+
return lines.join("\n");
|
|
502
|
+
}
|
|
388
503
|
var RRF_K = 10;
|
|
389
504
|
function computeComposite(rankings, churn, top) {
|
|
390
505
|
const totalDimensions = Object.keys(rankings).length;
|
|
@@ -756,7 +871,7 @@ function formatCompositeTable(output) {
|
|
|
756
871
|
|
|
757
872
|
// src/cli.ts
|
|
758
873
|
var program = new Command();
|
|
759
|
-
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("
|
|
874
|
+
program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("2.0.0");
|
|
760
875
|
var REPORT_GUIDE = {
|
|
761
876
|
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
877
|
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,10 +927,25 @@ addSharedOptions(
|
|
|
812
927
|
exitWithError(err);
|
|
813
928
|
}
|
|
814
929
|
});
|
|
930
|
+
program.command("init").description("generate a starter .obsignore based on project structure").action(() => {
|
|
931
|
+
try {
|
|
932
|
+
runInit();
|
|
933
|
+
} catch (err) {
|
|
934
|
+
exitWithError(err);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
815
937
|
function resolveExcludes(cliExcludes) {
|
|
816
938
|
return [...readIgnoreFile(), ...cliExcludes ?? []];
|
|
817
939
|
}
|
|
940
|
+
function warnIfNoIgnoreFile() {
|
|
941
|
+
if (!existsSync(".obsignore") && !existsSync(".obsceneignore")) {
|
|
942
|
+
process.stderr.write(
|
|
943
|
+
"hint: no .obsignore found \u2014 run `obscene init` to generate one with recommended exclusions\n"
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
818
947
|
function runReport(opts) {
|
|
948
|
+
warnIfNoIgnoreFile();
|
|
819
949
|
const top = parseInt(opts.top, 10);
|
|
820
950
|
const allExcludes = resolveExcludes(opts.exclude);
|
|
821
951
|
const files = runScc(allExcludes);
|
|
@@ -848,6 +978,7 @@ function runReport(opts) {
|
|
|
848
978
|
}
|
|
849
979
|
}
|
|
850
980
|
function runHotspots(opts) {
|
|
981
|
+
warnIfNoIgnoreFile();
|
|
851
982
|
const top = parseInt(opts.top, 10);
|
|
852
983
|
const months = parseInt(opts.months, 10);
|
|
853
984
|
const allExcludes = resolveExcludes(opts.exclude);
|
|
@@ -887,6 +1018,7 @@ ${formatCompositeTable(composite)}
|
|
|
887
1018
|
}
|
|
888
1019
|
}
|
|
889
1020
|
function runCoupling(opts) {
|
|
1021
|
+
warnIfNoIgnoreFile();
|
|
890
1022
|
const top = parseInt(opts.top, 10);
|
|
891
1023
|
const months = parseInt(opts.months, 10);
|
|
892
1024
|
const minCochanges = parseInt(opts.minCochanges, 10);
|
|
@@ -929,6 +1061,38 @@ function runCoupling(opts) {
|
|
|
929
1061
|
`);
|
|
930
1062
|
}
|
|
931
1063
|
}
|
|
1064
|
+
function runInit() {
|
|
1065
|
+
if (existsSync(".obsignore")) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
".obsignore already exists. Remove it first to regenerate."
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
if (existsSync(".obsceneignore")) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
".obsceneignore already exists. Remove it first to regenerate."
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
const detected = detectIgnorePatterns();
|
|
1076
|
+
const content = formatIgnoreFile(detected);
|
|
1077
|
+
writeFileSync(".obsignore", content);
|
|
1078
|
+
const universalCount = UNIVERSAL_IGNORE_GROUPS.reduce(
|
|
1079
|
+
(sum, g) => sum + g.patterns.length,
|
|
1080
|
+
0
|
|
1081
|
+
);
|
|
1082
|
+
process.stderr.write(
|
|
1083
|
+
`Created .obsignore with ${universalCount} universal exclusions`
|
|
1084
|
+
);
|
|
1085
|
+
if (detected.length > 0) {
|
|
1086
|
+
process.stderr.write(` + ${detected.length} detected patterns:
|
|
1087
|
+
`);
|
|
1088
|
+
for (const p of detected) {
|
|
1089
|
+
process.stderr.write(` ${p.pattern.padEnd(20)} ${p.comment}
|
|
1090
|
+
`);
|
|
1091
|
+
}
|
|
1092
|
+
} else {
|
|
1093
|
+
process.stderr.write(" (no project-specific patterns detected)\n");
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
932
1096
|
function exitWithError(err) {
|
|
933
1097
|
const message = err instanceof Error ? err.message : String(err);
|
|
934
1098
|
process.stderr.write(`Error: ${message}
|