@sebassdc/crap4ts 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 +27 -0
- package/README.md +454 -0
- package/dist/api.d.ts +18 -0
- package/dist/api.js +49 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +91 -0
- package/dist/complexity.d.ts +7 -0
- package/dist/complexity.js +122 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +67 -0
- package/dist/core.d.ts +24 -0
- package/dist/core.js +68 -0
- package/dist/coverage.d.ts +19 -0
- package/dist/coverage.js +77 -0
- package/dist/crap.d.ts +13 -0
- package/dist/crap.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/options.d.ts +17 -0
- package/dist/options.js +126 -0
- package/dist/report.d.ts +22 -0
- package/dist/report.js +163 -0
- package/dist/skill-cmd.d.ts +13 -0
- package/dist/skill-cmd.js +93 -0
- package/package.json +62 -0
- package/src/skill/SKILL.md +179 -0
package/dist/report.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectRunner = detectRunner;
|
|
4
|
+
exports.runCoverage = runCoverage;
|
|
5
|
+
exports.evaluateThresholds = evaluateThresholds;
|
|
6
|
+
exports.runReport = runReport;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const core_1 = require("./core");
|
|
11
|
+
const coverage_1 = require("./coverage");
|
|
12
|
+
const crap_1 = require("./crap");
|
|
13
|
+
const VITEST_CONFIGS = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts', 'vitest.config.mjs'];
|
|
14
|
+
const JEST_CONFIGS = ['jest.config.ts', 'jest.config.js', 'jest.config.mjs'];
|
|
15
|
+
function hasJestInPackageJson() {
|
|
16
|
+
if (!(0, fs_1.existsSync)('package.json'))
|
|
17
|
+
return false;
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)('package.json', 'utf-8'));
|
|
20
|
+
return !!(pkg.devDependencies?.jest || pkg.dependencies?.jest);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
console.warn("Unable to parse package.json while detecting the test runner. Fix package.json or use --runner to specify explicitly.");
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function detectRunner() {
|
|
28
|
+
if (VITEST_CONFIGS.some(f => (0, fs_1.existsSync)(f)))
|
|
29
|
+
return 'vitest';
|
|
30
|
+
if (JEST_CONFIGS.some(f => (0, fs_1.existsSync)(f)))
|
|
31
|
+
return 'jest';
|
|
32
|
+
if (hasJestInPackageJson())
|
|
33
|
+
return 'jest';
|
|
34
|
+
return 'vitest';
|
|
35
|
+
}
|
|
36
|
+
function runCoverage(runner, timeoutMs) {
|
|
37
|
+
const cmd = runner === 'vitest'
|
|
38
|
+
? ['npx', 'vitest', 'run', '--coverage']
|
|
39
|
+
: ['npx', 'jest', '--coverage', '--coverageReporters=json'];
|
|
40
|
+
const result = (0, child_process_1.spawnSync)(cmd[0], cmd.slice(1), { stdio: 'inherit', timeout: timeoutMs });
|
|
41
|
+
const timedOut = result.signal === 'SIGTERM' && result.status === null;
|
|
42
|
+
return { ok: result.status === 0, timedOut };
|
|
43
|
+
}
|
|
44
|
+
function evaluateThresholds(entries, opts) {
|
|
45
|
+
if (opts.failOnCrap != null) {
|
|
46
|
+
const violations = entries.filter(e => e.crap >= opts.failOnCrap);
|
|
47
|
+
if (violations.length > 0) {
|
|
48
|
+
return `CI failed: ${violations.length} function(s) exceed CRAP threshold of ${opts.failOnCrap}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (opts.failOnComplexity != null) {
|
|
52
|
+
const violations = entries.filter(e => e.complexity >= opts.failOnComplexity);
|
|
53
|
+
if (violations.length > 0) {
|
|
54
|
+
return `CI failed: ${violations.length} function(s) exceed complexity threshold of ${opts.failOnComplexity}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (opts.failOnCoverageBelow != null) {
|
|
58
|
+
const violations = entries.filter(e => e.coverage < opts.failOnCoverageBelow);
|
|
59
|
+
if (violations.length > 0) {
|
|
60
|
+
return `CI failed: ${violations.length} function(s) below coverage threshold of ${opts.failOnCoverageBelow}%`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function findFiles(opts) {
|
|
66
|
+
if (!(0, fs_1.existsSync)(opts.srcDir)) {
|
|
67
|
+
console.error(`Source directory '${opts.srcDir}' not found. Use --src to specify a different directory.`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const rawFiles = (0, core_1.findSourceFiles)(opts.srcDir);
|
|
71
|
+
const excludes = opts.excludes ?? [];
|
|
72
|
+
const allFiles = excludes.length > 0
|
|
73
|
+
? rawFiles.filter(f => !excludes.some(ex => f.includes(ex)))
|
|
74
|
+
: rawFiles;
|
|
75
|
+
if (allFiles.length === 0) {
|
|
76
|
+
console.error(`No TypeScript files found in '${opts.srcDir}'. Verify your source directory contains .ts files.`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const filtered = (0, core_1.filterSources)(allFiles, opts.filters);
|
|
80
|
+
if (filtered.length === 0) {
|
|
81
|
+
console.error(`No files match the filters: [${opts.filters.join(', ')}]. Check your filter arguments.`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return filtered;
|
|
85
|
+
}
|
|
86
|
+
function cleanStaleCoverage(coverageDir) {
|
|
87
|
+
if ((0, fs_1.existsSync)(coverageDir)) {
|
|
88
|
+
try {
|
|
89
|
+
(0, fs_1.rmSync)(coverageDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.warn(`Warning: failed to remove stale coverage dir ${coverageDir}: ${e.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function executeCoverage(opts) {
|
|
97
|
+
const { coverageDir, timeoutMs } = opts;
|
|
98
|
+
cleanStaleCoverage(coverageDir);
|
|
99
|
+
let ok;
|
|
100
|
+
let timedOut;
|
|
101
|
+
if (opts.coverageCommand) {
|
|
102
|
+
const result = (0, child_process_1.spawnSync)(opts.coverageCommand, [], { shell: true, stdio: 'inherit', timeout: timeoutMs });
|
|
103
|
+
timedOut = result.signal === 'SIGTERM' && result.status === null;
|
|
104
|
+
ok = result.status === 0;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const runner = opts.runner ?? detectRunner();
|
|
108
|
+
({ ok, timedOut } = runCoverage(runner, timeoutMs));
|
|
109
|
+
}
|
|
110
|
+
if (timedOut) {
|
|
111
|
+
console.error(`Coverage run timed out after ${timeoutMs / 1000}s.`);
|
|
112
|
+
return { ok: false };
|
|
113
|
+
}
|
|
114
|
+
if (!ok) {
|
|
115
|
+
console.error('Coverage run failed.');
|
|
116
|
+
return { ok: false };
|
|
117
|
+
}
|
|
118
|
+
if (!(0, fs_1.existsSync)(`${coverageDir}/coverage-final.json`)) {
|
|
119
|
+
console.error(`No coverage-final.json found in ${coverageDir}/.\n` +
|
|
120
|
+
`Configure your test runner to output Istanbul JSON coverage.\n` +
|
|
121
|
+
` Vitest: add coverage.reporter: ['json'] in vitest.config.ts\n` +
|
|
122
|
+
` Jest: add "json" to coverageReporters in jest.config`);
|
|
123
|
+
return { ok: false };
|
|
124
|
+
}
|
|
125
|
+
return { ok: true };
|
|
126
|
+
}
|
|
127
|
+
function formatOutput(entries, opts) {
|
|
128
|
+
if (opts.output === 'json') {
|
|
129
|
+
console.log((0, crap_1.formatJsonReport)(entries));
|
|
130
|
+
}
|
|
131
|
+
else if (opts.output === 'markdown') {
|
|
132
|
+
console.log((0, crap_1.formatMarkdownReport)(entries));
|
|
133
|
+
}
|
|
134
|
+
else if (opts.output === 'csv') {
|
|
135
|
+
console.log((0, crap_1.formatCsvReport)(entries));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log((0, crap_1.formatReport)(entries));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function runReport(opts) {
|
|
142
|
+
const files = findFiles(opts);
|
|
143
|
+
if (!files)
|
|
144
|
+
return 1;
|
|
145
|
+
const covResult = executeCoverage(opts);
|
|
146
|
+
if (!covResult.ok)
|
|
147
|
+
return 1;
|
|
148
|
+
const filesData = (0, coverage_1.parseCoverage)(opts.coverageDir);
|
|
149
|
+
const resolvedSrc = (0, path_1.resolve)(opts.srcDir);
|
|
150
|
+
const allEntries = files.flatMap(f => (0, core_1.analyzeFile)(f, filesData, resolvedSrc));
|
|
151
|
+
if (allEntries.length === 0) {
|
|
152
|
+
console.warn('No functions found. crap4ts analyzes top-level functions, arrow functions, and class methods.');
|
|
153
|
+
}
|
|
154
|
+
const sorted = (0, crap_1.sortByCrap)(allEntries);
|
|
155
|
+
const displayed = opts.top != null ? sorted.slice(0, opts.top) : sorted;
|
|
156
|
+
formatOutput(displayed, opts);
|
|
157
|
+
const failureMessage = evaluateThresholds(sorted, opts);
|
|
158
|
+
if (failureMessage) {
|
|
159
|
+
console.error(failureMessage);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Scope = 'global' | 'project';
|
|
2
|
+
export declare function bundledSkillPath(): string;
|
|
3
|
+
export declare function globalSkillDir(): string;
|
|
4
|
+
export declare function projectSkillDir(cwd?: string): string;
|
|
5
|
+
export declare function installSkill(opts: {
|
|
6
|
+
scope: Scope;
|
|
7
|
+
}): Promise<string>;
|
|
8
|
+
export declare function uninstallSkill(opts: {
|
|
9
|
+
scope: Scope;
|
|
10
|
+
}): Promise<boolean>;
|
|
11
|
+
export declare function showSkill(): string;
|
|
12
|
+
export declare function skillPath(scope: Scope): string;
|
|
13
|
+
export declare function runSkillCommand(argv: string[]): Promise<number>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bundledSkillPath = bundledSkillPath;
|
|
4
|
+
exports.globalSkillDir = globalSkillDir;
|
|
5
|
+
exports.projectSkillDir = projectSkillDir;
|
|
6
|
+
exports.installSkill = installSkill;
|
|
7
|
+
exports.uninstallSkill = uninstallSkill;
|
|
8
|
+
exports.showSkill = showSkill;
|
|
9
|
+
exports.skillPath = skillPath;
|
|
10
|
+
exports.runSkillCommand = runSkillCommand;
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const os_1 = require("os");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
function bundledSkillPath() {
|
|
15
|
+
const candidates = [
|
|
16
|
+
(0, path_1.join)(__dirname, 'skill', 'SKILL.md'),
|
|
17
|
+
(0, path_1.join)(__dirname, '..', 'src', 'skill', 'SKILL.md'),
|
|
18
|
+
];
|
|
19
|
+
for (const c of candidates) {
|
|
20
|
+
if ((0, fs_1.existsSync)(c))
|
|
21
|
+
return (0, path_1.resolve)(c);
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`crap4ts: bundled SKILL.md not found. Looked in:\n ${candidates.join('\n ')}`);
|
|
24
|
+
}
|
|
25
|
+
function globalSkillDir() {
|
|
26
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.agents', 'skills', 'crap4ts');
|
|
27
|
+
}
|
|
28
|
+
function projectSkillDir(cwd = process.cwd()) {
|
|
29
|
+
return (0, path_1.join)(cwd, '.agents', 'skills', 'crap4ts');
|
|
30
|
+
}
|
|
31
|
+
function destDir(scope) {
|
|
32
|
+
return scope === 'global' ? globalSkillDir() : projectSkillDir();
|
|
33
|
+
}
|
|
34
|
+
async function installSkill(opts) {
|
|
35
|
+
const src = bundledSkillPath();
|
|
36
|
+
const dir = destDir(opts.scope);
|
|
37
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
38
|
+
const dest = (0, path_1.join)(dir, 'SKILL.md');
|
|
39
|
+
(0, fs_1.copyFileSync)(src, dest);
|
|
40
|
+
return dest;
|
|
41
|
+
}
|
|
42
|
+
async function uninstallSkill(opts) {
|
|
43
|
+
const dir = destDir(opts.scope);
|
|
44
|
+
const file = (0, path_1.join)(dir, 'SKILL.md');
|
|
45
|
+
if (!(0, fs_1.existsSync)(file))
|
|
46
|
+
return false;
|
|
47
|
+
(0, fs_1.rmSync)(file, { force: true });
|
|
48
|
+
try {
|
|
49
|
+
(0, fs_1.rmSync)(dir, { recursive: false });
|
|
50
|
+
}
|
|
51
|
+
catch { /* non-empty or already gone */ }
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function showSkill() {
|
|
55
|
+
return (0, fs_1.readFileSync)(bundledSkillPath(), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
function skillPath(scope) {
|
|
58
|
+
return (0, path_1.join)(destDir(scope), 'SKILL.md');
|
|
59
|
+
}
|
|
60
|
+
function scopeFromArgs(argv) {
|
|
61
|
+
return argv.includes('--project') ? 'project' : 'global';
|
|
62
|
+
}
|
|
63
|
+
async function runSkillCommand(argv) {
|
|
64
|
+
const sub = argv[0];
|
|
65
|
+
const rest = argv.slice(1);
|
|
66
|
+
const scope = scopeFromArgs(rest);
|
|
67
|
+
switch (sub) {
|
|
68
|
+
case 'install': {
|
|
69
|
+
const dest = await installSkill({ scope });
|
|
70
|
+
console.log(`Installed crap4ts skill to ${dest}`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
case 'uninstall': {
|
|
74
|
+
const removed = await uninstallSkill({ scope });
|
|
75
|
+
console.log(removed
|
|
76
|
+
? `Removed crap4ts skill from ${skillPath(scope)}`
|
|
77
|
+
: `No crap4ts skill installed at ${skillPath(scope)}`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
case 'show': {
|
|
81
|
+
console.log(showSkill());
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
case 'path': {
|
|
85
|
+
console.log(skillPath(scope));
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
console.error(`Unknown skill subcommand: ${sub ?? '(none)'}\n` +
|
|
90
|
+
`Usage: crap4ts skill <install|uninstall|show|path> [--project]`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sebassdc/crap4ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "CRAP metric CLI for TypeScript: combines cyclomatic complexity and test coverage to find risky code, with a bundled cross-agent AI skill.",
|
|
8
|
+
"main": "dist/api.js",
|
|
9
|
+
"types": "dist/api.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/api.js",
|
|
12
|
+
"./cli": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"crap4ts": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src/skill/SKILL.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"typescript",
|
|
29
|
+
"cli",
|
|
30
|
+
"testing",
|
|
31
|
+
"coverage",
|
|
32
|
+
"cyclomatic-complexity",
|
|
33
|
+
"code-quality",
|
|
34
|
+
"static-analysis",
|
|
35
|
+
"metrics",
|
|
36
|
+
"crap",
|
|
37
|
+
"ai-agent",
|
|
38
|
+
"skill"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/sebassdc/crap4ts.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/sebassdc/crap4ts/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/sebassdc/crap4ts#readme",
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"coverage": "vitest run --coverage",
|
|
52
|
+
"prepublishOnly": "npm run build"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"typescript": "^5.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^20.0.0",
|
|
59
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
60
|
+
"vitest": "^2.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crap4ts
|
|
3
|
+
description: Use when the user asks for a CRAP report, cyclomatic complexity analysis, or code quality metrics on a TypeScript project
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# crap4ts — CRAP Metric for TypeScript
|
|
7
|
+
|
|
8
|
+
Computes the **CRAP** (Change Risk Anti-Pattern) score for every function and method in a TypeScript project. CRAP combines cyclomatic complexity with test coverage to identify functions that are both complex and under-tested.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
The target project must use Vitest or Jest with Istanbul JSON coverage output.
|
|
13
|
+
|
|
14
|
+
**Vitest** — add to `vitest.config.ts`:
|
|
15
|
+
```ts
|
|
16
|
+
coverage: {
|
|
17
|
+
provider: 'v8',
|
|
18
|
+
reporter: ['text', 'json'],
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Jest** — add to `jest.config.ts`:
|
|
23
|
+
```ts
|
|
24
|
+
coverageReporters: ['text', 'json']
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Install crap4ts from source:
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/sebassdc/crap4ts.git
|
|
30
|
+
cd crap4ts
|
|
31
|
+
npm install
|
|
32
|
+
npm run build
|
|
33
|
+
npm install -g .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Run from the project root (where `src/` lives):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
crap4ts
|
|
42
|
+
|
|
43
|
+
# Filter to specific modules
|
|
44
|
+
crap4ts parser validator
|
|
45
|
+
|
|
46
|
+
# Exclude paths
|
|
47
|
+
crap4ts --exclude dist --exclude fixtures
|
|
48
|
+
|
|
49
|
+
# Custom source directory
|
|
50
|
+
crap4ts --src packages/core/src
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
crap4ts automatically deletes stale coverage data, runs the test suite with coverage, and prints the report.
|
|
54
|
+
|
|
55
|
+
### CLI Options
|
|
56
|
+
|
|
57
|
+
| Flag | Argument | Description | Default |
|
|
58
|
+
|------|----------|-------------|---------|
|
|
59
|
+
| `--src` | `<dir>` | Source directory to analyze | `src` |
|
|
60
|
+
| `--exclude` | `<pattern>` | Exclude paths containing pattern (repeatable) | none |
|
|
61
|
+
| `--output` | `text\|json\|markdown\|csv` | Output format | `text` |
|
|
62
|
+
| `--json` | — | Shorthand for `--output json` | — |
|
|
63
|
+
| `--runner` | `vitest\|jest` | Skip auto-detection, use specified runner | auto-detect |
|
|
64
|
+
| `--coverage-command` | `<cmd>` | Custom shell command for coverage generation | none |
|
|
65
|
+
| `--fail-on-crap` | `<n>` | Exit 1 if any function CRAP score >= n | none |
|
|
66
|
+
| `--fail-on-complexity` | `<n>` | Exit 1 if any function complexity >= n | none |
|
|
67
|
+
| `--fail-on-coverage-below` | `<n>` | Exit 1 if coverage < n% (0-100) | none |
|
|
68
|
+
| `--top` | `<n>` | Show only top N entries (thresholds still check all) | all |
|
|
69
|
+
| `--timeout` | `<seconds>` | Analysis timeout | `600` |
|
|
70
|
+
| `--config` | `<path>` | Load config from specific file | auto-discover |
|
|
71
|
+
| `--help, -h` | — | Show help | — |
|
|
72
|
+
| `--version, -v` | — | Show version | — |
|
|
73
|
+
|
|
74
|
+
### Configuration File
|
|
75
|
+
|
|
76
|
+
crap4ts auto-discovers `crap4ts.config.json` or `.crap4tsrc.json` in the working directory. CLI flags override config values.
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"src": "lib",
|
|
81
|
+
"exclude": ["dist", "fixtures"],
|
|
82
|
+
"output": "json",
|
|
83
|
+
"runner": "vitest",
|
|
84
|
+
"coverageCommand": "npm run test:api -- --coverage",
|
|
85
|
+
"failOnCrap": 30,
|
|
86
|
+
"failOnComplexity": 15,
|
|
87
|
+
"failOnCoverageBelow": 70,
|
|
88
|
+
"top": 10,
|
|
89
|
+
"timeout": 120
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Output Formats
|
|
94
|
+
|
|
95
|
+
**Text** (default) — sorted table:
|
|
96
|
+
```
|
|
97
|
+
CRAP Report
|
|
98
|
+
===========
|
|
99
|
+
Function Module CC Cov% CRAP
|
|
100
|
+
-------------------------------------------------------------------------------------
|
|
101
|
+
complexFn my.module 12 45.0% 130.2
|
|
102
|
+
simpleFn my.module 1 100.0% 1.0
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**JSON** (`--json` or `--output json`):
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"tool": "crap4ts",
|
|
109
|
+
"entries": [
|
|
110
|
+
{ "name": "complexFn", "module": "my.module", "complexity": 12, "coverage": 45, "crap": 130.2 }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Markdown** (`--output markdown`) — pipe-table format.
|
|
116
|
+
|
|
117
|
+
**CSV** (`--output csv`) — header row + data rows.
|
|
118
|
+
|
|
119
|
+
### CI Integration
|
|
120
|
+
|
|
121
|
+
Use `--fail-on-*` flags to enforce quality gates:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
crap4ts --fail-on-crap 30 --fail-on-complexity 15 --fail-on-coverage-below 70
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Exit codes: `0` success, `1` threshold violation or error, `2` usage error.
|
|
128
|
+
|
|
129
|
+
### Custom Coverage Command
|
|
130
|
+
|
|
131
|
+
For non-standard setups, bypass runner auto-detection entirely:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
crap4ts --coverage-command "CI=1 yarn test --coverage --coverageReporters=json"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Interpreting Scores
|
|
138
|
+
|
|
139
|
+
| CRAP Score | Meaning |
|
|
140
|
+
|-----------|---------|
|
|
141
|
+
| 1-5 | Clean — low complexity, well tested |
|
|
142
|
+
| 5-30 | Moderate — consider refactoring or adding tests |
|
|
143
|
+
| 30+ | Crappy — high complexity with poor coverage |
|
|
144
|
+
|
|
145
|
+
## Programmatic API
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { generateReport } from '@sebassdc/crap4ts';
|
|
149
|
+
|
|
150
|
+
const { entries } = generateReport({
|
|
151
|
+
srcDir: 'src',
|
|
152
|
+
coverageDir: 'coverage',
|
|
153
|
+
filters: ['parser', 'validator'],
|
|
154
|
+
excludes: ['__mocks__', '.stories']
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
entries.forEach(e => console.log(`${e.name}: CRAP ${e.crap}`));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Also exports: `extractFunctions`, `parseCoverage`, `coverageForRange`, `crapScore`, `sortByCrap`, `formatReport`, `formatJsonReport`, `formatMarkdownReport`, `formatCsvReport`, `findSourceFiles`, `filterSources`, `analyzeFile`, `sourceToModule`, `normalizePath`.
|
|
161
|
+
|
|
162
|
+
## How It Works
|
|
163
|
+
|
|
164
|
+
1. Deletes stale `coverage/` directory
|
|
165
|
+
2. Detects test runner (Vitest or Jest) from config files, or uses `--runner` / `--coverage-command`
|
|
166
|
+
3. Runs tests with coverage to generate `coverage/coverage-final.json`
|
|
167
|
+
4. Finds all `.ts` / `.tsx` files under the source directory
|
|
168
|
+
5. Extracts top-level functions, class methods/getters/setters, and object literal methods using the TypeScript compiler API
|
|
169
|
+
6. Computes cyclomatic complexity:
|
|
170
|
+
- `if`/`else if`, ternary: +1 each
|
|
171
|
+
- `for`, `for...of`, `for...in`, `while`, `do...while`: +1 each
|
|
172
|
+
- `catch` handler: +1 each
|
|
173
|
+
- `&&`, `||`, `??`: +1 per operator
|
|
174
|
+
- `case` in `switch`: +1 each (`default` excluded)
|
|
175
|
+
- Nested functions and classes are skipped
|
|
176
|
+
7. Reads `coverage/coverage-final.json` for per-statement coverage data
|
|
177
|
+
8. Applies CRAP formula: `CC` x `(1 - cov)^3 + CC`
|
|
178
|
+
9. Applies filters, exclusions, and `--top` limit
|
|
179
|
+
10. Formats output and checks thresholds
|