@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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractFunctions = extractFunctions;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
function isFunctionLike(node) {
|
|
9
|
+
return (typescript_1.default.isFunctionDeclaration(node) ||
|
|
10
|
+
typescript_1.default.isMethodDeclaration(node) ||
|
|
11
|
+
typescript_1.default.isArrowFunction(node) ||
|
|
12
|
+
typescript_1.default.isFunctionExpression(node) ||
|
|
13
|
+
typescript_1.default.isConstructorDeclaration(node) ||
|
|
14
|
+
typescript_1.default.isGetAccessorDeclaration(node) ||
|
|
15
|
+
typescript_1.default.isSetAccessorDeclaration(node));
|
|
16
|
+
}
|
|
17
|
+
const DECISION_KINDS = new Set([
|
|
18
|
+
typescript_1.default.SyntaxKind.IfStatement,
|
|
19
|
+
typescript_1.default.SyntaxKind.ConditionalExpression,
|
|
20
|
+
typescript_1.default.SyntaxKind.ForStatement,
|
|
21
|
+
typescript_1.default.SyntaxKind.ForInStatement,
|
|
22
|
+
typescript_1.default.SyntaxKind.ForOfStatement,
|
|
23
|
+
typescript_1.default.SyntaxKind.WhileStatement,
|
|
24
|
+
typescript_1.default.SyntaxKind.DoStatement,
|
|
25
|
+
typescript_1.default.SyntaxKind.CatchClause,
|
|
26
|
+
typescript_1.default.SyntaxKind.CaseClause,
|
|
27
|
+
]);
|
|
28
|
+
const LOGICAL_OPS = new Set([
|
|
29
|
+
typescript_1.default.SyntaxKind.AmpersandAmpersandToken,
|
|
30
|
+
typescript_1.default.SyntaxKind.BarBarToken,
|
|
31
|
+
typescript_1.default.SyntaxKind.QuestionQuestionToken,
|
|
32
|
+
]);
|
|
33
|
+
function nodeDecisionCount(node) {
|
|
34
|
+
if (DECISION_KINDS.has(node.kind))
|
|
35
|
+
return 1;
|
|
36
|
+
if (typescript_1.default.isBinaryExpression(node) && LOGICAL_OPS.has(node.operatorToken.kind))
|
|
37
|
+
return 1;
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
function countDecisions(node) {
|
|
41
|
+
if (isFunctionLike(node))
|
|
42
|
+
return 0;
|
|
43
|
+
let count = nodeDecisionCount(node);
|
|
44
|
+
node.forEachChild(child => {
|
|
45
|
+
count += countDecisions(child);
|
|
46
|
+
});
|
|
47
|
+
return count;
|
|
48
|
+
}
|
|
49
|
+
function computeComplexity(node) {
|
|
50
|
+
const body = node.body;
|
|
51
|
+
if (!body)
|
|
52
|
+
return 1; // abstract method or overload signature
|
|
53
|
+
return 1 + countDecisions(body);
|
|
54
|
+
}
|
|
55
|
+
function visitObjectLiteralMethods(varName, obj, add) {
|
|
56
|
+
for (const prop of obj.properties) {
|
|
57
|
+
if (typescript_1.default.isMethodDeclaration(prop) ||
|
|
58
|
+
typescript_1.default.isGetAccessorDeclaration(prop) ||
|
|
59
|
+
typescript_1.default.isSetAccessorDeclaration(prop)) {
|
|
60
|
+
if (typescript_1.default.isIdentifier(prop.name)) {
|
|
61
|
+
add(`${varName}.${prop.name.text}`, prop);
|
|
62
|
+
}
|
|
63
|
+
else if (typescript_1.default.isStringLiteral(prop.name)) {
|
|
64
|
+
add(`${varName}['${prop.name.text}']`, prop);
|
|
65
|
+
}
|
|
66
|
+
// Skip computed property names (e.g., [Symbol.iterator])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function visitVariableStatement(stmt, add) {
|
|
71
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
72
|
+
if (!typescript_1.default.isIdentifier(decl.name) || !decl.initializer)
|
|
73
|
+
continue;
|
|
74
|
+
if (typescript_1.default.isArrowFunction(decl.initializer) || typescript_1.default.isFunctionExpression(decl.initializer)) {
|
|
75
|
+
add(decl.name.text, decl.initializer);
|
|
76
|
+
}
|
|
77
|
+
else if (typescript_1.default.isObjectLiteralExpression(decl.initializer)) {
|
|
78
|
+
visitObjectLiteralMethods(decl.name.text, decl.initializer, add);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function visitClassDeclaration(stmt, add) {
|
|
83
|
+
if (!stmt.name)
|
|
84
|
+
return;
|
|
85
|
+
const className = stmt.name.text;
|
|
86
|
+
for (const member of stmt.members) {
|
|
87
|
+
if (typescript_1.default.isConstructorDeclaration(member)) {
|
|
88
|
+
add(`${className}.constructor`, member);
|
|
89
|
+
}
|
|
90
|
+
else if ((typescript_1.default.isMethodDeclaration(member) || typescript_1.default.isGetAccessorDeclaration(member) || typescript_1.default.isSetAccessorDeclaration(member)) &&
|
|
91
|
+
typescript_1.default.isIdentifier(member.name)) {
|
|
92
|
+
add(`${className}.${member.name.text}`, member);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function extractFunctions(source, filePath = 'file.ts') {
|
|
97
|
+
const sourceFile = typescript_1.default.createSourceFile(filePath, source, typescript_1.default.ScriptTarget.Latest, true);
|
|
98
|
+
const functions = [];
|
|
99
|
+
function lineOf(pos) {
|
|
100
|
+
return sourceFile.getLineAndCharacterOfPosition(pos).line + 1;
|
|
101
|
+
}
|
|
102
|
+
function add(name, node) {
|
|
103
|
+
functions.push({
|
|
104
|
+
name,
|
|
105
|
+
startLine: lineOf(node.getStart(sourceFile)),
|
|
106
|
+
endLine: lineOf(node.getEnd()),
|
|
107
|
+
complexity: computeComplexity(node),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const stmt of sourceFile.statements) {
|
|
111
|
+
if (typescript_1.default.isFunctionDeclaration(stmt) && stmt.name) {
|
|
112
|
+
add(stmt.name.text, stmt);
|
|
113
|
+
}
|
|
114
|
+
else if (typescript_1.default.isVariableStatement(stmt)) {
|
|
115
|
+
visitVariableStatement(stmt, add);
|
|
116
|
+
}
|
|
117
|
+
else if (typescript_1.default.isClassDeclaration(stmt)) {
|
|
118
|
+
visitClassDeclaration(stmt, add);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return functions;
|
|
122
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CliOptions } from './options';
|
|
2
|
+
export interface Crap4tsConfig {
|
|
3
|
+
src?: string;
|
|
4
|
+
exclude?: string[];
|
|
5
|
+
output?: 'text' | 'json' | 'markdown' | 'csv';
|
|
6
|
+
runner?: 'vitest' | 'jest';
|
|
7
|
+
coverageCommand?: string;
|
|
8
|
+
failOnCrap?: number;
|
|
9
|
+
failOnComplexity?: number;
|
|
10
|
+
failOnCoverageBelow?: number;
|
|
11
|
+
top?: number;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadConfig(explicitPath?: string): Crap4tsConfig;
|
|
15
|
+
export declare function mergeConfigIntoOptions(opts: CliOptions, config: Crap4tsConfig): CliOptions;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadConfig = loadConfig;
|
|
4
|
+
exports.mergeConfigIntoOptions = mergeConfigIntoOptions;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const CONFIG_FILES = ['crap4ts.config.json', '.crap4tsrc.json'];
|
|
8
|
+
function loadConfig(explicitPath) {
|
|
9
|
+
if (explicitPath) {
|
|
10
|
+
if (!(0, fs_1.existsSync)(explicitPath)) {
|
|
11
|
+
throw new Error(`Config file not found: ${explicitPath}`);
|
|
12
|
+
}
|
|
13
|
+
return parseConfigFile(explicitPath);
|
|
14
|
+
}
|
|
15
|
+
for (const name of CONFIG_FILES) {
|
|
16
|
+
const fullPath = (0, path_1.join)(process.cwd(), name);
|
|
17
|
+
if ((0, fs_1.existsSync)(fullPath)) {
|
|
18
|
+
return parseConfigFile(fullPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
function parseConfigFile(filePath) {
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new Error(`Failed to read config file: ${filePath}`);
|
|
30
|
+
}
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`Invalid JSON in config file ${filePath}. Check for syntax errors.`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
39
|
+
throw new Error(`Config file ${filePath} must contain a JSON object.`);
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
function mergeNullableOptions(opts, config) {
|
|
44
|
+
return {
|
|
45
|
+
runner: opts.runner ?? config.runner,
|
|
46
|
+
coverageCommand: opts.coverageCommand ?? config.coverageCommand,
|
|
47
|
+
failOnCrap: opts.failOnCrap ?? config.failOnCrap,
|
|
48
|
+
failOnComplexity: opts.failOnComplexity ?? config.failOnComplexity,
|
|
49
|
+
failOnCoverageBelow: opts.failOnCoverageBelow ?? config.failOnCoverageBelow,
|
|
50
|
+
top: opts.top ?? config.top,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function mergeDefaultOptions(opts, config) {
|
|
54
|
+
return {
|
|
55
|
+
srcDir: opts.srcDir !== 'src' ? opts.srcDir : (config.src ?? opts.srcDir),
|
|
56
|
+
excludes: opts.excludes.length > 0 ? opts.excludes : (config.exclude ?? []),
|
|
57
|
+
output: opts.output !== 'text' ? opts.output : (config.output ?? 'text'),
|
|
58
|
+
timeoutMs: opts.timeoutMs !== 600_000 ? opts.timeoutMs : ((config.timeout ?? 600) * 1000),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function mergeConfigIntoOptions(opts, config) {
|
|
62
|
+
return {
|
|
63
|
+
...opts,
|
|
64
|
+
...mergeDefaultOptions(opts, config),
|
|
65
|
+
...mergeNullableOptions(opts, config),
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { extractFunctions } from './complexity';
|
|
2
|
+
import { CoverageData } from './coverage';
|
|
3
|
+
import { CrapEntry } from './crap';
|
|
4
|
+
export interface SourceScanOptions {
|
|
5
|
+
srcDirs: string[];
|
|
6
|
+
excludes: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function findSourceFilesWithOptions(opts: SourceScanOptions): string[];
|
|
9
|
+
export declare function findSourceFiles(srcDir: string): string[];
|
|
10
|
+
export declare function filterSources(files: string[], filters: string[]): string[];
|
|
11
|
+
export declare function buildEntries(fns: ReturnType<typeof extractFunctions>, fileData: {
|
|
12
|
+
statementMap: Record<string, {
|
|
13
|
+
start: {
|
|
14
|
+
line: number;
|
|
15
|
+
column: number;
|
|
16
|
+
};
|
|
17
|
+
end: {
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
};
|
|
21
|
+
}>;
|
|
22
|
+
s: Record<string, number>;
|
|
23
|
+
}, module: string): CrapEntry[];
|
|
24
|
+
export declare function analyzeFile(filePath: string, filesData: CoverageData, srcDir: string): CrapEntry[];
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findSourceFilesWithOptions = findSourceFilesWithOptions;
|
|
4
|
+
exports.findSourceFiles = findSourceFiles;
|
|
5
|
+
exports.filterSources = filterSources;
|
|
6
|
+
exports.buildEntries = buildEntries;
|
|
7
|
+
exports.analyzeFile = analyzeFile;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const complexity_1 = require("./complexity");
|
|
11
|
+
const coverage_1 = require("./coverage");
|
|
12
|
+
const crap_1 = require("./crap");
|
|
13
|
+
function findSourceFilesWithOptions(opts) {
|
|
14
|
+
const fileSet = new Set();
|
|
15
|
+
function walk(dir) {
|
|
16
|
+
for (const entry of (0, fs_1.readdirSync)(dir)) {
|
|
17
|
+
if (entry === 'node_modules')
|
|
18
|
+
continue;
|
|
19
|
+
const full = (0, path_1.join)(dir, entry);
|
|
20
|
+
if (opts.excludes.some(ex => full.includes(ex)))
|
|
21
|
+
continue;
|
|
22
|
+
if ((0, fs_1.statSync)(full).isDirectory()) {
|
|
23
|
+
walk(full);
|
|
24
|
+
}
|
|
25
|
+
else if (/\.(ts|tsx)$/.test(entry) && !entry.endsWith('.d.ts')) {
|
|
26
|
+
fileSet.add(full);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const srcDir of opts.srcDirs) {
|
|
31
|
+
walk(srcDir);
|
|
32
|
+
}
|
|
33
|
+
return [...fileSet].sort();
|
|
34
|
+
}
|
|
35
|
+
function findSourceFiles(srcDir) {
|
|
36
|
+
return findSourceFilesWithOptions({ srcDirs: [srcDir], excludes: [] });
|
|
37
|
+
}
|
|
38
|
+
function filterSources(files, filters) {
|
|
39
|
+
if (filters.length === 0)
|
|
40
|
+
return files;
|
|
41
|
+
return files.filter(f => filters.some(filter => f.includes(filter)));
|
|
42
|
+
}
|
|
43
|
+
function buildEntries(fns, fileData, module) {
|
|
44
|
+
return fns.map(fn => {
|
|
45
|
+
const cov = (0, coverage_1.coverageForRange)(fileData, fn.startLine, fn.endLine);
|
|
46
|
+
return {
|
|
47
|
+
name: fn.name,
|
|
48
|
+
module,
|
|
49
|
+
complexity: fn.complexity,
|
|
50
|
+
coverage: cov,
|
|
51
|
+
crap: (0, crap_1.crapScore)(fn.complexity, cov),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function analyzeFile(filePath, filesData, srcDir) {
|
|
56
|
+
try {
|
|
57
|
+
const source = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
58
|
+
const fns = (0, complexity_1.extractFunctions)(source, filePath);
|
|
59
|
+
const absolutePath = (0, coverage_1.normalizePath)((0, path_1.resolve)(filePath));
|
|
60
|
+
const module = (0, coverage_1.sourceToModule)(absolutePath, srcDir);
|
|
61
|
+
const fileData = filesData[absolutePath] ?? { statementMap: {}, s: {} };
|
|
62
|
+
return buildEntries(fns, fileData, module);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.warn(`crap4ts: skipping ${filePath} (parse error: ${e.message})`);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface Location {
|
|
2
|
+
line: number;
|
|
3
|
+
column: number;
|
|
4
|
+
}
|
|
5
|
+
interface Range {
|
|
6
|
+
start: Location;
|
|
7
|
+
end: Location;
|
|
8
|
+
}
|
|
9
|
+
export interface FileCoverageData {
|
|
10
|
+
statementMap: Record<string, Range>;
|
|
11
|
+
s: Record<string, number>;
|
|
12
|
+
}
|
|
13
|
+
export type CoverageData = Record<string, FileCoverageData>;
|
|
14
|
+
/** Normalize a file path to always use forward slashes. */
|
|
15
|
+
export declare function normalizePath(p: string): string;
|
|
16
|
+
export declare function parseCoverage(coverageDir: string): CoverageData;
|
|
17
|
+
export declare function coverageForRange(fileData: FileCoverageData, startLine: number, endLine: number): number;
|
|
18
|
+
export declare function sourceToModule(filePath: string, srcDir: string): string;
|
|
19
|
+
export {};
|
package/dist/coverage.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizePath = normalizePath;
|
|
4
|
+
exports.parseCoverage = parseCoverage;
|
|
5
|
+
exports.coverageForRange = coverageForRange;
|
|
6
|
+
exports.sourceToModule = sourceToModule;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
/** Normalize a file path to always use forward slashes. */
|
|
10
|
+
function normalizePath(p) {
|
|
11
|
+
return p.replace(/\\/g, '/');
|
|
12
|
+
}
|
|
13
|
+
function assertValidCoverageEntry(key, entry) {
|
|
14
|
+
if (!entry || typeof entry !== 'object') {
|
|
15
|
+
throw new Error(`coverage-final.json entry ${key} is not an object`);
|
|
16
|
+
}
|
|
17
|
+
const e = entry;
|
|
18
|
+
if (!e.statementMap || typeof e.statementMap !== 'object') {
|
|
19
|
+
throw new Error(`coverage-final.json entry ${key} missing statementMap`);
|
|
20
|
+
}
|
|
21
|
+
if (!e.s || typeof e.s !== 'object') {
|
|
22
|
+
throw new Error(`coverage-final.json entry ${key} missing s`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function assertValidCoverage(data) {
|
|
26
|
+
if (!data || typeof data !== 'object') {
|
|
27
|
+
throw new Error('coverage-final.json is not an object');
|
|
28
|
+
}
|
|
29
|
+
for (const [key, entry] of Object.entries(data)) {
|
|
30
|
+
assertValidCoverageEntry(key, entry);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function parseCoverage(coverageDir) {
|
|
34
|
+
const jsonPath = (0, path_1.join)(coverageDir, 'coverage-final.json');
|
|
35
|
+
const content = (0, fs_1.readFileSync)(jsonPath, 'utf-8');
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(content);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
throw new Error(`coverage-final.json is not valid JSON: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
assertValidCoverage(parsed);
|
|
45
|
+
// Normalize coverage keys to forward slashes so lookups are platform-independent
|
|
46
|
+
const normalized = {};
|
|
47
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
48
|
+
normalized[normalizePath(key)] = value;
|
|
49
|
+
}
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
function coverageForRange(fileData, startLine, endLine) {
|
|
53
|
+
let total = 0;
|
|
54
|
+
let covered = 0;
|
|
55
|
+
for (const [id, loc] of Object.entries(fileData.statementMap)) {
|
|
56
|
+
// Overlap check: statement overlaps function if it starts before the function ends
|
|
57
|
+
// AND ends after the function starts (standard range-overlap test).
|
|
58
|
+
if (loc.start.line <= endLine && loc.end.line >= startLine) {
|
|
59
|
+
total++;
|
|
60
|
+
if ((fileData.s[id] ?? 0) > 0)
|
|
61
|
+
covered++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (total === 0)
|
|
65
|
+
return 100.0;
|
|
66
|
+
return (covered / total) * 100;
|
|
67
|
+
}
|
|
68
|
+
function sourceToModule(filePath, srcDir) {
|
|
69
|
+
const normalized = normalizePath(filePath);
|
|
70
|
+
const src = normalizePath(srcDir).replace(/\/$/, '') + '/';
|
|
71
|
+
let mod = normalized;
|
|
72
|
+
if (mod.startsWith(src)) {
|
|
73
|
+
mod = mod.slice(src.length);
|
|
74
|
+
}
|
|
75
|
+
mod = mod.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
76
|
+
return mod.replace(/\//g, '.');
|
|
77
|
+
}
|
package/dist/crap.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CrapEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
module: string;
|
|
4
|
+
complexity: number;
|
|
5
|
+
coverage: number;
|
|
6
|
+
crap: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function crapScore(complexity: number, coveragePct: number): number;
|
|
9
|
+
export declare function sortByCrap(entries: CrapEntry[]): CrapEntry[];
|
|
10
|
+
export declare function formatJsonReport(entries: CrapEntry[]): string;
|
|
11
|
+
export declare function formatCsvReport(entries: CrapEntry[]): string;
|
|
12
|
+
export declare function formatMarkdownReport(entries: CrapEntry[]): string;
|
|
13
|
+
export declare function formatReport(entries: CrapEntry[]): string;
|
package/dist/crap.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.crapScore = crapScore;
|
|
4
|
+
exports.sortByCrap = sortByCrap;
|
|
5
|
+
exports.formatJsonReport = formatJsonReport;
|
|
6
|
+
exports.formatCsvReport = formatCsvReport;
|
|
7
|
+
exports.formatMarkdownReport = formatMarkdownReport;
|
|
8
|
+
exports.formatReport = formatReport;
|
|
9
|
+
function crapScore(complexity, coveragePct) {
|
|
10
|
+
const cc = complexity;
|
|
11
|
+
const uncov = 1.0 - coveragePct / 100.0;
|
|
12
|
+
return cc * cc * uncov * uncov * uncov + cc;
|
|
13
|
+
}
|
|
14
|
+
function sortByCrap(entries) {
|
|
15
|
+
return [...entries].sort((a, b) => b.crap - a.crap);
|
|
16
|
+
}
|
|
17
|
+
function formatJsonReport(entries) {
|
|
18
|
+
return JSON.stringify({ tool: 'crap4ts', entries }, null, 2);
|
|
19
|
+
}
|
|
20
|
+
function formatCsvReport(entries) {
|
|
21
|
+
const header = 'Function,Module,CC,Coverage,CRAP';
|
|
22
|
+
const rows = entries.map(e => {
|
|
23
|
+
const name = e.name.includes(',') ? `"${e.name}"` : e.name;
|
|
24
|
+
const mod = e.module.includes(',') ? `"${e.module}"` : e.module;
|
|
25
|
+
return `${name},${mod},${e.complexity},${e.coverage.toFixed(1)},${e.crap.toFixed(1)}`;
|
|
26
|
+
});
|
|
27
|
+
return [header, ...rows, ''].join('\n');
|
|
28
|
+
}
|
|
29
|
+
function formatMarkdownReport(entries) {
|
|
30
|
+
const header = '| Function | Module | CC | Cov% | CRAP |';
|
|
31
|
+
const sep = '|---|---|---:|---:|---:|';
|
|
32
|
+
const rows = entries.map(e => `| ${e.name} | ${e.module} | ${e.complexity} | ${e.coverage.toFixed(1)}% | ${e.crap.toFixed(1)} |`);
|
|
33
|
+
return ['# CRAP Report', '', header, sep, ...rows, ''].join('\n');
|
|
34
|
+
}
|
|
35
|
+
function formatReport(entries) {
|
|
36
|
+
const header = `${'Function'.padEnd(30)} ${'Module'.padEnd(35)} ${'CC'.padStart(4)} ${'Cov%'.padStart(6)} ${'CRAP'.padStart(8)}`;
|
|
37
|
+
const sep = '-'.repeat(header.length);
|
|
38
|
+
const rows = entries.map(e => {
|
|
39
|
+
const name = e.name.slice(0, 30).padEnd(30);
|
|
40
|
+
const mod = e.module.slice(0, 35).padEnd(35);
|
|
41
|
+
const cc = String(e.complexity).padStart(4);
|
|
42
|
+
const cov = `${e.coverage.toFixed(1)}%`.padStart(6);
|
|
43
|
+
const crap = e.crap.toFixed(1).padStart(8);
|
|
44
|
+
return `${name} ${mod} ${cc} ${cov} ${crap}`;
|
|
45
|
+
});
|
|
46
|
+
return ['CRAP Report', '===========', header, sep, ...rows, ''].join('\n');
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface CliOptions {
|
|
2
|
+
mode: 'report' | 'help' | 'version';
|
|
3
|
+
filters: string[];
|
|
4
|
+
srcDir: string;
|
|
5
|
+
coverageDir: string;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
output: 'text' | 'json' | 'markdown' | 'csv';
|
|
8
|
+
excludes: string[];
|
|
9
|
+
runner?: 'vitest' | 'jest';
|
|
10
|
+
coverageCommand?: string;
|
|
11
|
+
failOnCrap?: number;
|
|
12
|
+
failOnComplexity?: number;
|
|
13
|
+
failOnCoverageBelow?: number;
|
|
14
|
+
top?: number;
|
|
15
|
+
configPath?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseOptions(argv: string[]): CliOptions;
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseOptions = parseOptions;
|
|
4
|
+
function parseOutputFormat(v) {
|
|
5
|
+
const valid = ['text', 'json', 'markdown', 'csv'];
|
|
6
|
+
if (!v || !valid.includes(v)) {
|
|
7
|
+
throw new Error(`--output must be one of: ${valid.join(', ')}. Got: ${v}`);
|
|
8
|
+
}
|
|
9
|
+
return v;
|
|
10
|
+
}
|
|
11
|
+
function parseRunnerValue(v) {
|
|
12
|
+
if (!v)
|
|
13
|
+
throw new Error('--runner requires an argument');
|
|
14
|
+
if (v !== 'vitest' && v !== 'jest') {
|
|
15
|
+
throw new Error(`--runner must be 'vitest' or 'jest', got: ${v}`);
|
|
16
|
+
}
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
function parsePositiveNum(v, flag) {
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
if (!v || !Number.isFinite(n) || n <= 0) {
|
|
22
|
+
throw new Error(`${flag} requires a positive number, got: ${v}`);
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
function parsePositiveInt(v, flag) {
|
|
27
|
+
const n = Number(v);
|
|
28
|
+
if (!v || !Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
|
|
29
|
+
throw new Error(`${flag} requires a positive integer, got: ${v}`);
|
|
30
|
+
}
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
function parseRange(v, flag, min, max) {
|
|
34
|
+
const n = Number(v);
|
|
35
|
+
if (!v || !Number.isFinite(n) || n < min || n > max) {
|
|
36
|
+
throw new Error(`${flag} requires a number between ${min} and ${max}, got: ${v}`);
|
|
37
|
+
}
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
const FLAG_HANDLERS = {
|
|
41
|
+
'--json'(_argv, i, state) {
|
|
42
|
+
state.output = 'json';
|
|
43
|
+
return i;
|
|
44
|
+
},
|
|
45
|
+
'--output'(argv, i, state) {
|
|
46
|
+
state.output = parseOutputFormat(argv[i + 1]);
|
|
47
|
+
return i + 1;
|
|
48
|
+
},
|
|
49
|
+
'--src'(argv, i, state) {
|
|
50
|
+
const v = argv[i + 1];
|
|
51
|
+
if (!v)
|
|
52
|
+
throw new Error('--src requires a directory argument');
|
|
53
|
+
state.srcDir = v;
|
|
54
|
+
return i + 1;
|
|
55
|
+
},
|
|
56
|
+
'--timeout'(argv, i, state) {
|
|
57
|
+
state.timeoutMs = parsePositiveNum(argv[i + 1], '--timeout') * 1000;
|
|
58
|
+
return i + 1;
|
|
59
|
+
},
|
|
60
|
+
'--runner'(argv, i, state) {
|
|
61
|
+
state.runner = parseRunnerValue(argv[i + 1]);
|
|
62
|
+
return i + 1;
|
|
63
|
+
},
|
|
64
|
+
'--coverage-command'(argv, i, state) {
|
|
65
|
+
const v = argv[i + 1];
|
|
66
|
+
if (!v)
|
|
67
|
+
throw new Error('--coverage-command requires an argument');
|
|
68
|
+
state.coverageCommand = v;
|
|
69
|
+
return i + 1;
|
|
70
|
+
},
|
|
71
|
+
'--exclude'(argv, i, state) {
|
|
72
|
+
const v = argv[i + 1];
|
|
73
|
+
if (!v)
|
|
74
|
+
throw new Error('--exclude requires a pattern argument');
|
|
75
|
+
state.excludes.push(v);
|
|
76
|
+
return i + 1;
|
|
77
|
+
},
|
|
78
|
+
'--fail-on-crap'(argv, i, state) {
|
|
79
|
+
state.failOnCrap = parsePositiveNum(argv[i + 1], '--fail-on-crap');
|
|
80
|
+
return i + 1;
|
|
81
|
+
},
|
|
82
|
+
'--fail-on-complexity'(argv, i, state) {
|
|
83
|
+
state.failOnComplexity = parsePositiveNum(argv[i + 1], '--fail-on-complexity');
|
|
84
|
+
return i + 1;
|
|
85
|
+
},
|
|
86
|
+
'--fail-on-coverage-below'(argv, i, state) {
|
|
87
|
+
state.failOnCoverageBelow = parseRange(argv[i + 1], '--fail-on-coverage-below', 0, 100);
|
|
88
|
+
return i + 1;
|
|
89
|
+
},
|
|
90
|
+
'--top'(argv, i, state) {
|
|
91
|
+
state.top = parsePositiveInt(argv[i + 1], '--top');
|
|
92
|
+
return i + 1;
|
|
93
|
+
},
|
|
94
|
+
'--config'(argv, i, state) {
|
|
95
|
+
const v = argv[i + 1];
|
|
96
|
+
if (!v)
|
|
97
|
+
throw new Error('--config requires a file path argument');
|
|
98
|
+
state.configPath = v;
|
|
99
|
+
return i + 1;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const EARLY_EXIT_DEFAULTS = {
|
|
103
|
+
filters: [], srcDir: 'src', coverageDir: 'coverage', timeoutMs: 600_000, output: 'text', excludes: [],
|
|
104
|
+
};
|
|
105
|
+
function parseOptions(argv) {
|
|
106
|
+
const state = {
|
|
107
|
+
filters: [], excludes: [], srcDir: 'src', timeoutMs: 600_000, output: 'text',
|
|
108
|
+
};
|
|
109
|
+
for (let i = 0; i < argv.length; i++) {
|
|
110
|
+
const a = argv[i];
|
|
111
|
+
if (a === '--help' || a === '-h') {
|
|
112
|
+
return { mode: 'help', ...EARLY_EXIT_DEFAULTS };
|
|
113
|
+
}
|
|
114
|
+
if (a === '--version' || a === '-v') {
|
|
115
|
+
return { mode: 'version', ...EARLY_EXIT_DEFAULTS };
|
|
116
|
+
}
|
|
117
|
+
const handler = FLAG_HANDLERS[a];
|
|
118
|
+
if (handler) {
|
|
119
|
+
i = handler(argv, i, state);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
state.filters.push(a);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { mode: 'report', ...state, coverageDir: 'coverage' };
|
|
126
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CrapEntry } from './crap';
|
|
2
|
+
export interface ReportOptions {
|
|
3
|
+
filters: string[];
|
|
4
|
+
srcDir: string;
|
|
5
|
+
coverageDir: string;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
output: 'text' | 'json' | 'markdown' | 'csv';
|
|
8
|
+
excludes: string[];
|
|
9
|
+
runner?: 'vitest' | 'jest';
|
|
10
|
+
coverageCommand?: string;
|
|
11
|
+
failOnCrap?: number;
|
|
12
|
+
failOnComplexity?: number;
|
|
13
|
+
failOnCoverageBelow?: number;
|
|
14
|
+
top?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function detectRunner(): 'vitest' | 'jest';
|
|
17
|
+
export declare function runCoverage(runner: 'vitest' | 'jest', timeoutMs: number): {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
timedOut: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function evaluateThresholds(entries: CrapEntry[], opts: ReportOptions): string | null;
|
|
22
|
+
export declare function runReport(opts: ReportOptions): Promise<number>;
|