@pythonidaer/complexity-report 1.0.2
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/CHANGELOG.md +122 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/assets/prettify.css +1 -0
- package/assets/prettify.js +2 -0
- package/assets/sort-arrow-sprite.png +0 -0
- package/complexity-breakdown.js +53 -0
- package/decision-points/ast-utils.js +127 -0
- package/decision-points/decision-type.js +92 -0
- package/decision-points/function-matching.js +185 -0
- package/decision-points/in-params.js +262 -0
- package/decision-points/index.js +6 -0
- package/decision-points/node-helpers.js +89 -0
- package/decision-points/parent-map.js +62 -0
- package/decision-points/parse-main.js +101 -0
- package/decision-points/ternary-multiline.js +86 -0
- package/export-generators/helpers.js +309 -0
- package/export-generators/index.js +143 -0
- package/export-generators/md-exports.js +160 -0
- package/export-generators/txt-exports.js +262 -0
- package/function-boundaries/arrow-brace-body.js +302 -0
- package/function-boundaries/arrow-helpers.js +93 -0
- package/function-boundaries/arrow-jsx.js +73 -0
- package/function-boundaries/arrow-object-literal.js +65 -0
- package/function-boundaries/arrow-single-expr.js +72 -0
- package/function-boundaries/brace-scanning.js +151 -0
- package/function-boundaries/index.js +67 -0
- package/function-boundaries/named-helpers.js +227 -0
- package/function-boundaries/parse-utils.js +456 -0
- package/function-extraction/ast-utils.js +112 -0
- package/function-extraction/extract-callback.js +65 -0
- package/function-extraction/extract-from-eslint.js +91 -0
- package/function-extraction/extract-name-ast.js +133 -0
- package/function-extraction/extract-name-regex.js +267 -0
- package/function-extraction/index.js +6 -0
- package/function-extraction/utils.js +29 -0
- package/function-hierarchy.js +427 -0
- package/html-generators/about.js +75 -0
- package/html-generators/file-boundary-builders.js +36 -0
- package/html-generators/file-breakdown.js +412 -0
- package/html-generators/file-data.js +50 -0
- package/html-generators/file-helpers.js +100 -0
- package/html-generators/file-javascript.js +430 -0
- package/html-generators/file-line-render.js +160 -0
- package/html-generators/file.css +370 -0
- package/html-generators/file.js +207 -0
- package/html-generators/folder.js +424 -0
- package/html-generators/index.js +6 -0
- package/html-generators/main-index.js +346 -0
- package/html-generators/shared.css +471 -0
- package/html-generators/utils.js +15 -0
- package/index.js +36 -0
- package/integration/eslint/index.js +94 -0
- package/integration/threshold/index.js +45 -0
- package/package.json +64 -0
- package/report/cli.js +58 -0
- package/report/index.js +559 -0
package/report/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for complexity-report
|
|
5
|
+
* Parses command-line arguments and runs the report generator
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { generateComplexityReport } from './index.js';
|
|
9
|
+
|
|
10
|
+
function getCliFlags() {
|
|
11
|
+
const argv = process.argv;
|
|
12
|
+
return {
|
|
13
|
+
showAllInitially: argv.includes('--show-all') || argv.includes('--all'),
|
|
14
|
+
showAllColumnsInitially: argv.includes('--show-all-columns'),
|
|
15
|
+
hideTableInitially: argv.includes('--hide-table'),
|
|
16
|
+
hideLinesInitially: argv.includes('--no-lines'),
|
|
17
|
+
hideHighlightsInitially: argv.includes('--no-highlights'),
|
|
18
|
+
shouldExport: argv.includes('--export') || argv.includes('--exports'),
|
|
19
|
+
cwd: getCwdFlag(argv),
|
|
20
|
+
outputDir: getOutputDirFlag(argv),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getCwdFlag(argv) {
|
|
25
|
+
const cwdIndex = argv.findIndex(arg => arg === '--cwd');
|
|
26
|
+
if (cwdIndex !== -1 && argv[cwdIndex + 1]) {
|
|
27
|
+
return argv[cwdIndex + 1];
|
|
28
|
+
}
|
|
29
|
+
return process.cwd();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getOutputDirFlag(argv) {
|
|
33
|
+
const outputIndex = argv.findIndex(arg => arg === '--output-dir' || arg === '--output');
|
|
34
|
+
if (outputIndex !== -1 && argv[outputIndex + 1]) {
|
|
35
|
+
return argv[outputIndex + 1];
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const flags = getCliFlags();
|
|
42
|
+
|
|
43
|
+
await generateComplexityReport({
|
|
44
|
+
cwd: flags.cwd,
|
|
45
|
+
outputDir: flags.outputDir,
|
|
46
|
+
showAllInitially: flags.showAllInitially,
|
|
47
|
+
showAllColumnsInitially: flags.showAllColumnsInitially,
|
|
48
|
+
hideTableInitially: flags.hideTableInitially,
|
|
49
|
+
hideLinesInitially: flags.hideLinesInitially,
|
|
50
|
+
hideHighlightsInitially: flags.hideHighlightsInitially,
|
|
51
|
+
shouldExport: flags.shouldExport,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main().catch((error) => {
|
|
56
|
+
console.error('Error generating complexity report:', error);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
package/report/index.js
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, copyFileSync, readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
runESLintComplexityCheck,
|
|
7
|
+
findESLintConfig,
|
|
8
|
+
getComplexityVariant,
|
|
9
|
+
} from '../integration/eslint/index.js';
|
|
10
|
+
import {
|
|
11
|
+
extractFunctionsFromESLintResults,
|
|
12
|
+
getComplexityLevel,
|
|
13
|
+
getDirectory,
|
|
14
|
+
getBaseFunctionName,
|
|
15
|
+
} from '../function-extraction/index.js';
|
|
16
|
+
import { findFunctionBoundaries } from '../function-boundaries/index.js';
|
|
17
|
+
import { parseDecisionPointsAST } from '../decision-points/index.js';
|
|
18
|
+
import { calculateComplexityBreakdown } from '../complexity-breakdown.js';
|
|
19
|
+
import {
|
|
20
|
+
formatFunctionHierarchy,
|
|
21
|
+
setEscapeHtml,
|
|
22
|
+
} from '../function-hierarchy.js';
|
|
23
|
+
import {
|
|
24
|
+
escapeHtml,
|
|
25
|
+
generateAboutPageHTML,
|
|
26
|
+
generateMainIndexHTML,
|
|
27
|
+
generateFolderHTML,
|
|
28
|
+
generateFileHTML,
|
|
29
|
+
} from '../html-generators/index.js';
|
|
30
|
+
import { getComplexityThreshold } from '../integration/threshold/index.js';
|
|
31
|
+
import { generateAllExports } from '../export-generators/index.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Calculates decision point totals across all functions
|
|
35
|
+
*/
|
|
36
|
+
async function calculateDecisionPointTotals(
|
|
37
|
+
allFunctions,
|
|
38
|
+
projectRoot,
|
|
39
|
+
findFunctionBoundaries,
|
|
40
|
+
parseDecisionPoints
|
|
41
|
+
) {
|
|
42
|
+
const controlFlowTypes = [
|
|
43
|
+
'if',
|
|
44
|
+
'else if',
|
|
45
|
+
'for',
|
|
46
|
+
'for...of',
|
|
47
|
+
'for...in',
|
|
48
|
+
'while',
|
|
49
|
+
'do...while',
|
|
50
|
+
'switch',
|
|
51
|
+
'case',
|
|
52
|
+
'catch',
|
|
53
|
+
];
|
|
54
|
+
const expressionTypes = ['ternary', '&&', '||', '??', '?.'];
|
|
55
|
+
const functionParameterTypes = ['default parameter'];
|
|
56
|
+
|
|
57
|
+
let controlFlowTotal = 0;
|
|
58
|
+
let expressionsTotal = 0;
|
|
59
|
+
let functionParametersTotal = 0;
|
|
60
|
+
|
|
61
|
+
const fileToFunctions = new Map();
|
|
62
|
+
allFunctions.forEach((func) => {
|
|
63
|
+
if (!fileToFunctions.has(func.file)) fileToFunctions.set(func.file, []);
|
|
64
|
+
fileToFunctions.get(func.file).push(func);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
for (const [filePath, functions] of fileToFunctions.entries()) {
|
|
68
|
+
const fullPath = resolve(projectRoot, filePath);
|
|
69
|
+
if (!existsSync(fullPath)) continue;
|
|
70
|
+
try {
|
|
71
|
+
const sourceCode = readFileSync(fullPath, 'utf-8');
|
|
72
|
+
const functionBoundaries = findFunctionBoundaries(sourceCode, functions);
|
|
73
|
+
const decisionPoints = await parseDecisionPoints(
|
|
74
|
+
sourceCode,
|
|
75
|
+
functionBoundaries,
|
|
76
|
+
functions,
|
|
77
|
+
filePath,
|
|
78
|
+
projectRoot
|
|
79
|
+
);
|
|
80
|
+
const seenLines = new Set();
|
|
81
|
+
functions.forEach((func) => {
|
|
82
|
+
const lineKey = `${filePath}:${func.line}`;
|
|
83
|
+
if (seenLines.has(lineKey)) return;
|
|
84
|
+
seenLines.add(lineKey);
|
|
85
|
+
const breakdown = calculateComplexityBreakdown(func.line, decisionPoints, 1);
|
|
86
|
+
controlFlowTypes.forEach((type) => {
|
|
87
|
+
controlFlowTotal += breakdown.breakdown[type] || 0;
|
|
88
|
+
});
|
|
89
|
+
expressionTypes.forEach((type) => {
|
|
90
|
+
expressionsTotal += breakdown.breakdown[type] || 0;
|
|
91
|
+
});
|
|
92
|
+
functionParameterTypes.forEach((type) => {
|
|
93
|
+
functionParametersTotal += breakdown.breakdown[type] || 0;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn(
|
|
98
|
+
`Warning: Could not process ${filePath} for decision point totals:`,
|
|
99
|
+
error.message
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
controlFlow: controlFlowTotal,
|
|
106
|
+
expressions: expressionsTotal,
|
|
107
|
+
functionParameters: functionParametersTotal,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function calculateFunctionStatistics(allFunctions, complexityThreshold) {
|
|
112
|
+
const overThreshold = allFunctions.filter(
|
|
113
|
+
(f) => parseInt(f.complexity, 10) > complexityThreshold
|
|
114
|
+
);
|
|
115
|
+
const allFunctionsCount = allFunctions.length;
|
|
116
|
+
const maxComplexity =
|
|
117
|
+
allFunctions.length > 0
|
|
118
|
+
? Math.max(...allFunctions.map((i) => parseInt(i.complexity, 10)))
|
|
119
|
+
: 0;
|
|
120
|
+
const avgComplexity =
|
|
121
|
+
allFunctions.length > 0
|
|
122
|
+
? Math.round(
|
|
123
|
+
allFunctions.reduce(
|
|
124
|
+
(sum, i) => sum + parseInt(i.complexity, 10),
|
|
125
|
+
0
|
|
126
|
+
) / allFunctions.length
|
|
127
|
+
)
|
|
128
|
+
: 0;
|
|
129
|
+
const withinThreshold = allFunctions.filter(
|
|
130
|
+
(f) => parseInt(f.complexity, 10) <= complexityThreshold
|
|
131
|
+
).length;
|
|
132
|
+
const withinThresholdPercentage =
|
|
133
|
+
allFunctionsCount > 0
|
|
134
|
+
? Math.round((withinThreshold / allFunctionsCount) * 100)
|
|
135
|
+
: 100;
|
|
136
|
+
return {
|
|
137
|
+
overThreshold,
|
|
138
|
+
allFunctionsCount,
|
|
139
|
+
maxComplexity,
|
|
140
|
+
avgComplexity,
|
|
141
|
+
withinThreshold,
|
|
142
|
+
withinThresholdPercentage,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function groupFunctionsByFolder(allFunctions, complexityThreshold) {
|
|
147
|
+
const folderMap = new Map();
|
|
148
|
+
allFunctions.forEach((func) => {
|
|
149
|
+
const dir = getDirectory(func.file);
|
|
150
|
+
if (!folderMap.has(dir)) folderMap.set(dir, []);
|
|
151
|
+
folderMap.get(dir).push(func);
|
|
152
|
+
});
|
|
153
|
+
return Array.from(folderMap.entries()).map(([dir, functions]) => {
|
|
154
|
+
const totalFunctions = functions.length;
|
|
155
|
+
const withinThreshold = functions.filter(
|
|
156
|
+
(f) => parseInt(f.complexity, 10) <= complexityThreshold
|
|
157
|
+
).length;
|
|
158
|
+
const percentage =
|
|
159
|
+
totalFunctions > 0
|
|
160
|
+
? Math.round((withinThreshold / totalFunctions) * 100)
|
|
161
|
+
: 100;
|
|
162
|
+
return {
|
|
163
|
+
directory: dir,
|
|
164
|
+
totalFunctions,
|
|
165
|
+
withinThreshold,
|
|
166
|
+
percentage,
|
|
167
|
+
functions: functions.sort(
|
|
168
|
+
(a, b) => parseInt(b.complexity, 10) - parseInt(a.complexity, 10)
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
}).sort((a, b) => a.directory.localeCompare(b.directory));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function groupFunctionsByFile(allFunctions) {
|
|
175
|
+
const fileMap = new Map();
|
|
176
|
+
allFunctions.forEach((func) => {
|
|
177
|
+
if (!fileMap.has(func.file)) fileMap.set(func.file, []);
|
|
178
|
+
fileMap.get(func.file).push(func);
|
|
179
|
+
});
|
|
180
|
+
return fileMap;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function copyRequiredFiles(projectRoot, complexityDir, packageRoot) {
|
|
184
|
+
const assetsDir = resolve(packageRoot, 'assets');
|
|
185
|
+
const htmlGeneratorsDir = resolve(packageRoot, 'html-generators');
|
|
186
|
+
const prettifyFiles = [
|
|
187
|
+
{ source: resolve(assetsDir, 'prettify.css'), dest: resolve(complexityDir, 'prettify.css') },
|
|
188
|
+
{ source: resolve(assetsDir, 'prettify.js'), dest: resolve(complexityDir, 'prettify.js') },
|
|
189
|
+
];
|
|
190
|
+
prettifyFiles.forEach(({ source, dest }) => {
|
|
191
|
+
try {
|
|
192
|
+
copyFileSync(source, dest);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.warn(`Warning: Could not copy ${source}:`, error.message);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
copyFileSync(
|
|
199
|
+
resolve(assetsDir, 'sort-arrow-sprite.png'),
|
|
200
|
+
resolve(complexityDir, 'sort-arrow-sprite.png')
|
|
201
|
+
);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.warn(
|
|
204
|
+
'Warning: Could not copy sort-arrow-sprite.png:',
|
|
205
|
+
error.message
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
copyFileSync(
|
|
210
|
+
resolve(htmlGeneratorsDir, 'shared.css'),
|
|
211
|
+
resolve(complexityDir, 'shared.css')
|
|
212
|
+
);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.warn('Warning: Could not copy shared.css:', error.message);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
copyFileSync(
|
|
218
|
+
resolve(htmlGeneratorsDir, 'file.css'),
|
|
219
|
+
resolve(complexityDir, 'file.css')
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.warn('Warning: Could not copy file.css:', error.message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getCliFlags() {
|
|
227
|
+
const argv = process.argv;
|
|
228
|
+
return {
|
|
229
|
+
showAllInitially: argv.includes('--show-all') || argv.includes('--all'),
|
|
230
|
+
showAllColumnsInitially: argv.includes('--show-all-columns'),
|
|
231
|
+
hideTableInitially: argv.includes('--hide-table'),
|
|
232
|
+
hideLinesInitially: argv.includes('--no-lines'),
|
|
233
|
+
hideHighlightsInitially: argv.includes('--no-highlights'),
|
|
234
|
+
shouldExport: argv.includes('--export') || argv.includes('--exports'),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function writeMainReport(projectRoot, complexityDir, packageRoot, html) {
|
|
239
|
+
mkdirSync(complexityDir, { recursive: true });
|
|
240
|
+
copyRequiredFiles(projectRoot, complexityDir, packageRoot);
|
|
241
|
+
writeFileSync(resolve(complexityDir, 'index.html'), html, 'utf-8');
|
|
242
|
+
writeFileSync(
|
|
243
|
+
resolve(complexityDir, 'about.html'),
|
|
244
|
+
generateAboutPageHTML(),
|
|
245
|
+
'utf-8'
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function generateOneFolderHTML(
|
|
250
|
+
folder,
|
|
251
|
+
folders,
|
|
252
|
+
projectRoot,
|
|
253
|
+
complexityDir,
|
|
254
|
+
parseDecisionPointsFn,
|
|
255
|
+
showAllInitially,
|
|
256
|
+
complexityThreshold
|
|
257
|
+
) {
|
|
258
|
+
if (!folder.directory) return 0;
|
|
259
|
+
const folderPath = folder.directory;
|
|
260
|
+
const folderDir = resolve(complexityDir, ...folderPath.split('/'));
|
|
261
|
+
try {
|
|
262
|
+
mkdirSync(folderDir, { recursive: true });
|
|
263
|
+
const folderDecisionPointTotals = await calculateDecisionPointTotals(
|
|
264
|
+
folder.functions,
|
|
265
|
+
projectRoot,
|
|
266
|
+
findFunctionBoundaries,
|
|
267
|
+
parseDecisionPointsFn
|
|
268
|
+
);
|
|
269
|
+
const folderHTML = generateFolderHTML(
|
|
270
|
+
folder,
|
|
271
|
+
folders,
|
|
272
|
+
showAllInitially,
|
|
273
|
+
getComplexityLevel,
|
|
274
|
+
getBaseFunctionName,
|
|
275
|
+
complexityThreshold,
|
|
276
|
+
folderDecisionPointTotals
|
|
277
|
+
);
|
|
278
|
+
writeFileSync(resolve(folderDir, 'index.html'), folderHTML, 'utf-8');
|
|
279
|
+
return 1;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(
|
|
282
|
+
`Error generating folder HTML for ${folderPath}:`,
|
|
283
|
+
error.message
|
|
284
|
+
);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function generateOneFileHTML(
|
|
290
|
+
filePath,
|
|
291
|
+
functions,
|
|
292
|
+
projectRoot,
|
|
293
|
+
complexityDir,
|
|
294
|
+
parseDecisionPointsFn,
|
|
295
|
+
showAllColumnsInitially,
|
|
296
|
+
hideTableInitially,
|
|
297
|
+
complexityThreshold,
|
|
298
|
+
hideLinesInitially = false,
|
|
299
|
+
hideHighlightsInitially = false,
|
|
300
|
+
variant = 'classic'
|
|
301
|
+
) {
|
|
302
|
+
try {
|
|
303
|
+
const fileDir = getDirectory(filePath);
|
|
304
|
+
const fileName = filePath.split('/').pop();
|
|
305
|
+
if (fileDir) {
|
|
306
|
+
mkdirSync(
|
|
307
|
+
resolve(complexityDir, ...fileDir.split('/')),
|
|
308
|
+
{ recursive: true }
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const fileHTML = await generateFileHTML(
|
|
312
|
+
filePath,
|
|
313
|
+
functions,
|
|
314
|
+
projectRoot,
|
|
315
|
+
findFunctionBoundaries,
|
|
316
|
+
parseDecisionPointsFn,
|
|
317
|
+
calculateComplexityBreakdown,
|
|
318
|
+
formatFunctionHierarchy,
|
|
319
|
+
getComplexityLevel,
|
|
320
|
+
getDirectory,
|
|
321
|
+
escapeHtml,
|
|
322
|
+
showAllColumnsInitially,
|
|
323
|
+
hideTableInitially,
|
|
324
|
+
complexityThreshold,
|
|
325
|
+
hideLinesInitially,
|
|
326
|
+
hideHighlightsInitially,
|
|
327
|
+
variant
|
|
328
|
+
);
|
|
329
|
+
const fileHTMLPath = fileDir
|
|
330
|
+
? resolve(complexityDir, ...fileDir.split('/'), `${fileName}.html`)
|
|
331
|
+
: resolve(complexityDir, `${fileName}.html`);
|
|
332
|
+
writeFileSync(fileHTMLPath, fileHTML, 'utf-8');
|
|
333
|
+
return 1;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error(
|
|
336
|
+
`Error generating file HTML for ${filePath}:`,
|
|
337
|
+
error.message
|
|
338
|
+
);
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function runExportsIfRequested(shouldExport, projectRoot, allFunctions) {
|
|
344
|
+
if (!shouldExport) return;
|
|
345
|
+
try {
|
|
346
|
+
const packageJsonPath = resolve(projectRoot, 'package.json');
|
|
347
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
348
|
+
const exportDirConfig =
|
|
349
|
+
packageJson.complexityReport?.exportDir || 'complexity/reports';
|
|
350
|
+
const exportDir = resolve(projectRoot, exportDirConfig);
|
|
351
|
+
const exportResult = generateAllExports(
|
|
352
|
+
allFunctions,
|
|
353
|
+
projectRoot,
|
|
354
|
+
exportDir
|
|
355
|
+
);
|
|
356
|
+
console.log(`\n✅ Exports generated in: ${exportDirConfig}/`);
|
|
357
|
+
console.log(
|
|
358
|
+
` Generated ${exportResult.generatedFiles.length} export file(s):`
|
|
359
|
+
);
|
|
360
|
+
exportResult.generatedFiles.forEach((file) =>
|
|
361
|
+
console.log(` - ${file.replace(projectRoot + '/', '')}`)
|
|
362
|
+
);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Error generating exports:', error.message);
|
|
365
|
+
console.error(' Exports will be skipped, but HTML report generation will continue.');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function printReportSummary(
|
|
370
|
+
stats,
|
|
371
|
+
complexityThreshold,
|
|
372
|
+
foldersGenerated,
|
|
373
|
+
filesGenerated
|
|
374
|
+
) {
|
|
375
|
+
console.log(`\n✅ Complexity report generated: complexity/index.html`);
|
|
376
|
+
console.log(` About: complexity/about.html`);
|
|
377
|
+
console.log(` Generated ${foldersGenerated} folder HTML file(s)`);
|
|
378
|
+
console.log(` Generated ${filesGenerated} file HTML page(s)`);
|
|
379
|
+
console.log(` Found ${stats.allFunctionsCount} total function(s)`);
|
|
380
|
+
if (stats.overThreshold.length > 0) {
|
|
381
|
+
console.log(
|
|
382
|
+
` ${stats.overThreshold.length} function(s) with complexity > ${complexityThreshold}`
|
|
383
|
+
);
|
|
384
|
+
console.log('');
|
|
385
|
+
stats.overThreshold.forEach((f) =>
|
|
386
|
+
console.log(
|
|
387
|
+
` ${f.file}:${f.line} ${f.functionName} (complexity ${f.complexity})`
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (stats.allFunctionsCount > 0) {
|
|
392
|
+
console.log(
|
|
393
|
+
` Highest complexity: ${stats.maxComplexity} / Average: ${stats.avgComplexity}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
console.log(` Using AST-based parser for 100% accuracy`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Generate complexity report for a project
|
|
401
|
+
* @param {Object} options - Configuration options
|
|
402
|
+
* @param {string} [options.cwd] - Project root directory (defaults to process.cwd())
|
|
403
|
+
* @param {string} [options.outputDir] - Output directory for reports (defaults to 'complexity' under cwd)
|
|
404
|
+
* @param {boolean} [options.showAllInitially] - Show all functions initially
|
|
405
|
+
* @param {boolean} [options.showAllColumnsInitially] - Show all breakdown columns initially
|
|
406
|
+
* @param {boolean} [options.hideTableInitially] - Hide breakdown table initially
|
|
407
|
+
* @param {boolean} [options.hideLinesInitially] - Hide line numbers initially
|
|
408
|
+
* @param {boolean} [options.hideHighlightsInitially] - Hide highlights initially
|
|
409
|
+
* @param {boolean} [options.shouldExport] - Generate export files
|
|
410
|
+
*/
|
|
411
|
+
export async function generateComplexityReport(options = {}) {
|
|
412
|
+
const {
|
|
413
|
+
cwd = process.cwd(),
|
|
414
|
+
outputDir,
|
|
415
|
+
showAllInitially = false,
|
|
416
|
+
showAllColumnsInitially = false,
|
|
417
|
+
hideTableInitially = false,
|
|
418
|
+
hideLinesInitially = false,
|
|
419
|
+
hideHighlightsInitially = false,
|
|
420
|
+
shouldExport = false,
|
|
421
|
+
} = options;
|
|
422
|
+
|
|
423
|
+
// Get package root from this file's location
|
|
424
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
425
|
+
const __dirname = dirname(__filename);
|
|
426
|
+
const packageRoot = resolve(__dirname, '..');
|
|
427
|
+
|
|
428
|
+
// Use provided cwd or default to process.cwd()
|
|
429
|
+
const projectRoot = resolve(cwd);
|
|
430
|
+
|
|
431
|
+
setEscapeHtml(escapeHtml);
|
|
432
|
+
|
|
433
|
+
const complexityThreshold = getComplexityThreshold(projectRoot);
|
|
434
|
+
const configPath = findESLintConfig(projectRoot);
|
|
435
|
+
const variant = configPath ? getComplexityVariant(configPath) : 'classic';
|
|
436
|
+
|
|
437
|
+
const eslintResults = await runESLintComplexityCheck(projectRoot);
|
|
438
|
+
const allFunctions = extractFunctionsFromESLintResults(eslintResults, projectRoot);
|
|
439
|
+
allFunctions.sort((a, b) => parseInt(b.complexity, 10) - parseInt(a.complexity, 10));
|
|
440
|
+
|
|
441
|
+
const stats = calculateFunctionStatistics(allFunctions, complexityThreshold);
|
|
442
|
+
const folders = groupFunctionsByFolder(allFunctions, complexityThreshold);
|
|
443
|
+
const fileMap = groupFunctionsByFile(allFunctions);
|
|
444
|
+
|
|
445
|
+
// Use options passed to function (CLI will override via getCliFlags)
|
|
446
|
+
|
|
447
|
+
const parseDecisionPointsFn = (
|
|
448
|
+
sourceCode,
|
|
449
|
+
functionBoundaries,
|
|
450
|
+
functions,
|
|
451
|
+
filePath,
|
|
452
|
+
projectRoot
|
|
453
|
+
) =>
|
|
454
|
+
parseDecisionPointsAST(
|
|
455
|
+
sourceCode,
|
|
456
|
+
functionBoundaries,
|
|
457
|
+
functions,
|
|
458
|
+
filePath,
|
|
459
|
+
projectRoot,
|
|
460
|
+
{ variant }
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const decisionPointTotals = await calculateDecisionPointTotals(
|
|
464
|
+
allFunctions,
|
|
465
|
+
projectRoot,
|
|
466
|
+
findFunctionBoundaries,
|
|
467
|
+
parseDecisionPointsFn
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const html = generateMainIndexHTML(
|
|
471
|
+
folders,
|
|
472
|
+
stats.allFunctionsCount,
|
|
473
|
+
stats.overThreshold,
|
|
474
|
+
stats.maxComplexity,
|
|
475
|
+
stats.avgComplexity,
|
|
476
|
+
showAllInitially,
|
|
477
|
+
complexityThreshold,
|
|
478
|
+
decisionPointTotals,
|
|
479
|
+
stats.withinThreshold,
|
|
480
|
+
stats.withinThresholdPercentage
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const complexityDir = outputDir
|
|
484
|
+
? resolve(projectRoot, outputDir)
|
|
485
|
+
: resolve(projectRoot, 'complexity');
|
|
486
|
+
writeMainReport(projectRoot, complexityDir, packageRoot, html);
|
|
487
|
+
|
|
488
|
+
const folderPromises = folders.map((folder) =>
|
|
489
|
+
generateOneFolderHTML(
|
|
490
|
+
folder,
|
|
491
|
+
folders,
|
|
492
|
+
projectRoot,
|
|
493
|
+
complexityDir,
|
|
494
|
+
parseDecisionPointsFn,
|
|
495
|
+
showAllInitially,
|
|
496
|
+
complexityThreshold
|
|
497
|
+
)
|
|
498
|
+
);
|
|
499
|
+
const foldersGenerated = (await Promise.all(folderPromises)).reduce(
|
|
500
|
+
(a, b) => a + b,
|
|
501
|
+
0
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const filePromises = Array.from(fileMap.entries()).map(
|
|
505
|
+
([filePath, functions]) =>
|
|
506
|
+
generateOneFileHTML(
|
|
507
|
+
filePath,
|
|
508
|
+
functions,
|
|
509
|
+
projectRoot,
|
|
510
|
+
complexityDir,
|
|
511
|
+
parseDecisionPointsFn,
|
|
512
|
+
showAllColumnsInitially,
|
|
513
|
+
hideTableInitially,
|
|
514
|
+
complexityThreshold,
|
|
515
|
+
hideLinesInitially,
|
|
516
|
+
hideHighlightsInitially,
|
|
517
|
+
variant
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
const filesGenerated = (await Promise.all(filePromises)).reduce(
|
|
521
|
+
(a, b) => a + b,
|
|
522
|
+
0
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
runExportsIfRequested(shouldExport, projectRoot, allFunctions);
|
|
526
|
+
printReportSummary(stats, complexityThreshold, foldersGenerated, filesGenerated);
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
stats,
|
|
530
|
+
folders,
|
|
531
|
+
complexityDir,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Main entry point when run directly (not as a module)
|
|
537
|
+
* Parses CLI flags and calls generateComplexityReport
|
|
538
|
+
*/
|
|
539
|
+
async function main() {
|
|
540
|
+
const flags = getCliFlags();
|
|
541
|
+
|
|
542
|
+
await generateComplexityReport({
|
|
543
|
+
cwd: process.cwd(),
|
|
544
|
+
showAllInitially: flags.showAllInitially,
|
|
545
|
+
showAllColumnsInitially: flags.showAllColumnsInitially,
|
|
546
|
+
hideTableInitially: flags.hideTableInitially,
|
|
547
|
+
hideLinesInitially: flags.hideLinesInitially,
|
|
548
|
+
hideHighlightsInitially: flags.hideHighlightsInitially,
|
|
549
|
+
shouldExport: flags.shouldExport,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Only run main if this file is executed directly
|
|
554
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
555
|
+
main().catch((error) => {
|
|
556
|
+
console.error('Error generating complexity report:', error);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
});
|
|
559
|
+
}
|