Skip to content

Commit 05560fa

Browse files
committed
fix: remove escomplex in favor of in-house halstead+cyclomatic implem
1 parent 6a8d99e commit 05560fa

File tree

10 files changed

+499
-2349
lines changed

10 files changed

+499
-2349
lines changed

package-lock.json

Lines changed: 38 additions & 2243 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,17 @@
4242
"license": "MIT",
4343
"dependencies": {
4444
"@babel/core": "7.28.3",
45+
"@babel/parser": "7.28.3",
4546
"@babel/plugin-transform-typescript": "7.28.0",
47+
"@babel/preset-env": "7.28.3",
4648
"cli-table3": "0.6.5",
4749
"commander": "10.0.1",
4850
"debug": "4.4.1",
49-
"escomplex": "2.0.0-alpha",
50-
"esprima": "4.0.1",
5151
"micromatch": "4.0.8",
5252
"node-sloc": "0.2.1"
5353
},
5454
"devDependencies": {
55-
"@babel/preset-env": "7.28.3",
56-
"@types/babel__core": "^7.20.5",
55+
"@types/babel__core": "7.20.5",
5756
"@types/debug": "4.1.12",
5857
"@types/micromatch": "4.0.9",
5958
"@typescript-eslint/eslint-plugin": "5.42.1",
@@ -67,8 +66,5 @@
6766
"prettier": "2.8.8",
6867
"ts-node": "10.9.2",
6968
"typescript": "4.9.5"
70-
},
71-
"bundleDependencies": [
72-
"esprima"
73-
]
69+
}
7470
}

src/lib/complexity/strategies/cyclomatic.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import * as assert from "node:assert";
2+
import { readFileSync } from "node:fs";
3+
import { ParseResult, traverse } from "@babel/core";
4+
import { parse, ParserOptions } from "@babel/parser";
5+
6+
type FunctionComplexity = { name: string; complexity: number };
7+
8+
export function compute(path: string, options: ParserOptions) {
9+
const code = readFileSync(path, { encoding: "utf8" });
10+
const ast = parse(code, options);
11+
assert.ok(ast);
12+
return cyclomaticForAst(ast, code, options);
13+
}
14+
15+
export function cyclomaticForAst(
16+
ast: ParseResult,
17+
code: string,
18+
options: ParserOptions
19+
): number {
20+
const results: FunctionComplexity[] = [];
21+
let hasTopLevelStatements = false;
22+
23+
traverse(ast, {
24+
FunctionDeclaration(path) {
25+
const fnCode = code.slice(
26+
path.node.start ?? undefined,
27+
path.node.end ?? undefined
28+
);
29+
results.push({
30+
name: path.node.id?.name || "<anonymous>",
31+
complexity: cyclomaticForCode(fnCode, options),
32+
});
33+
},
34+
FunctionExpression(path) {
35+
const fnCode = code.slice(
36+
path.node.start ?? undefined,
37+
path.node.end ?? undefined
38+
);
39+
results.push({
40+
name: path.node.id?.name || "<anonymous>",
41+
complexity: cyclomaticForCode(fnCode, options),
42+
});
43+
},
44+
ArrowFunctionExpression(path) {
45+
const fnCode = code.slice(
46+
path.node.start ?? undefined,
47+
path.node.end ?? undefined
48+
);
49+
let name = "<arrow>";
50+
if (path.parent.type === "VariableDeclarator" && path.parent.id) {
51+
assert.ok("name" in path.parent.id);
52+
name = path.parent.id.name;
53+
}
54+
results.push({
55+
name,
56+
complexity: cyclomaticForCode(fnCode, options),
57+
});
58+
},
59+
60+
enter(path) {
61+
const topLevelTypes = [
62+
"ExpressionStatement",
63+
"IfStatement",
64+
"ForStatement",
65+
"ForInStatement",
66+
"ForOfStatement",
67+
"WhileStatement",
68+
"DoWhileStatement",
69+
"SwitchStatement",
70+
"TryStatement",
71+
];
72+
if (path.parent.type === "Program" && topLevelTypes.includes(path.type)) {
73+
hasTopLevelStatements = true;
74+
}
75+
},
76+
});
77+
78+
return hasTopLevelStatements
79+
? cyclomaticForCode(code, options)
80+
: results.reduce((prev, cur) => prev + cur.complexity, 0);
81+
}
82+
83+
function cyclomaticForCode(code: string, options: ParserOptions) {
84+
const ast = parse(code, options);
85+
assert.ok(ast);
86+
87+
let complexity = 1;
88+
89+
traverse(ast, {
90+
IfStatement() {
91+
complexity++;
92+
},
93+
ForStatement() {
94+
complexity++;
95+
},
96+
ForInStatement() {
97+
complexity++;
98+
},
99+
ForOfStatement() {
100+
complexity++;
101+
},
102+
WhileStatement() {
103+
complexity++;
104+
},
105+
DoWhileStatement() {
106+
complexity++;
107+
},
108+
SwitchCase(path) {
109+
if (path.node.test) complexity++;
110+
},
111+
ConditionalExpression() {
112+
complexity++;
113+
},
114+
LogicalExpression(path) {
115+
if (["&&", "||"].includes(path.node.operator)) complexity++;
116+
},
117+
CatchClause() {
118+
complexity++;
119+
},
120+
});
121+
122+
return complexity;
123+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { extname } from "node:path";
2+
3+
import { buildDebugger, UnsupportedExtension } from "../../../../utils";
4+
import { compute as computeFromBabel } from "./cyclomatic.babel";
5+
6+
const internal = { debug: buildDebugger("cyclomatic") };
7+
8+
export function calculate(path: string): number | UnsupportedExtension {
9+
internal.debug(`Processing ${path}...`);
10+
11+
switch (extname(path)) {
12+
case ".ts":
13+
return computeFromBabel(path, {
14+
sourceType: "unambiguous",
15+
plugins: ["typescript"],
16+
});
17+
case ".mjs":
18+
case ".cjs":
19+
case ".js":
20+
return computeFromBabel(path, { sourceType: "unambiguous" });
21+
default:
22+
internal.debug(
23+
"Unsupported file extension. Falling back on default complexity (1)"
24+
);
25+
return new UnsupportedExtension();
26+
}
27+
}

src/lib/complexity/strategies/halstead.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)