tokenlean 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/README.md +248 -0
- package/bin/tl-api.mjs +515 -0
- package/bin/tl-blame.mjs +345 -0
- package/bin/tl-complexity.mjs +514 -0
- package/bin/tl-component.mjs +274 -0
- package/bin/tl-config.mjs +135 -0
- package/bin/tl-context.mjs +156 -0
- package/bin/tl-coverage.mjs +456 -0
- package/bin/tl-deps.mjs +474 -0
- package/bin/tl-diff.mjs +183 -0
- package/bin/tl-entry.mjs +256 -0
- package/bin/tl-env.mjs +376 -0
- package/bin/tl-exports.mjs +583 -0
- package/bin/tl-flow.mjs +324 -0
- package/bin/tl-history.mjs +289 -0
- package/bin/tl-hotspots.mjs +321 -0
- package/bin/tl-impact.mjs +345 -0
- package/bin/tl-prompt.mjs +175 -0
- package/bin/tl-related.mjs +227 -0
- package/bin/tl-routes.mjs +627 -0
- package/bin/tl-search.mjs +123 -0
- package/bin/tl-structure.mjs +161 -0
- package/bin/tl-symbols.mjs +430 -0
- package/bin/tl-todo.mjs +341 -0
- package/bin/tl-types.mjs +441 -0
- package/bin/tl-unused.mjs +494 -0
- package/package.json +55 -0
- package/src/config.mjs +271 -0
- package/src/output.mjs +251 -0
- package/src/project.mjs +277 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-complexity - Code complexity metrics
|
|
5
|
+
*
|
|
6
|
+
* Calculates cyclomatic and cognitive complexity for functions in your codebase.
|
|
7
|
+
* Helps identify functions that may need refactoring.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-complexity [file-or-dir] [--threshold N]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-complexity',
|
|
16
|
+
desc: 'Code complexity metrics for functions',
|
|
17
|
+
when: 'before-modify',
|
|
18
|
+
example: 'tl-complexity src/'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
24
|
+
import { join, relative, extname } from 'path';
|
|
25
|
+
import {
|
|
26
|
+
createOutput,
|
|
27
|
+
parseCommonArgs,
|
|
28
|
+
formatTable,
|
|
29
|
+
COMMON_OPTIONS_HELP
|
|
30
|
+
} from '../src/output.mjs';
|
|
31
|
+
import { findProjectRoot, shouldSkip } from '../src/project.mjs';
|
|
32
|
+
|
|
33
|
+
const HELP = `
|
|
34
|
+
tl-complexity - Code complexity metrics
|
|
35
|
+
|
|
36
|
+
Usage: tl-complexity [file-or-dir] [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--threshold N, -t N Only show functions with complexity >= N (default: 0)
|
|
40
|
+
--sort <field> Sort by: cyclomatic, cognitive, name, loc (default: cyclomatic)
|
|
41
|
+
--top N Show only top N most complex functions
|
|
42
|
+
--summary Show only file-level summary, not individual functions
|
|
43
|
+
${COMMON_OPTIONS_HELP}
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
tl-complexity src/ # All functions
|
|
47
|
+
tl-complexity src/utils.ts # Single file
|
|
48
|
+
tl-complexity src/ --threshold 10 # Only complex functions
|
|
49
|
+
tl-complexity src/ --top 20 # Top 20 most complex
|
|
50
|
+
tl-complexity src/ --summary # File-level only
|
|
51
|
+
|
|
52
|
+
Metrics:
|
|
53
|
+
Cyclomatic: Number of independent paths through code
|
|
54
|
+
(if, for, while, case, catch, &&, ||, ?:)
|
|
55
|
+
Cognitive: How hard code is to understand
|
|
56
|
+
(nesting increases weight of decisions)
|
|
57
|
+
|
|
58
|
+
Thresholds (suggestions):
|
|
59
|
+
1-10: Simple, low risk
|
|
60
|
+
11-20: Moderate, some risk
|
|
61
|
+
21-50: Complex, high risk
|
|
62
|
+
50+: Very complex, refactor recommended
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────
|
|
66
|
+
// File Discovery
|
|
67
|
+
// ─────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts']);
|
|
70
|
+
|
|
71
|
+
function findCodeFiles(dir, files = []) {
|
|
72
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
73
|
+
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const fullPath = join(dir, entry.name);
|
|
76
|
+
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (!shouldSkip(entry.name, true)) {
|
|
79
|
+
findCodeFiles(fullPath, files);
|
|
80
|
+
}
|
|
81
|
+
} else if (entry.isFile()) {
|
|
82
|
+
const ext = extname(entry.name).toLowerCase();
|
|
83
|
+
if (CODE_EXTENSIONS.has(ext) && !shouldSkip(entry.name, false)) {
|
|
84
|
+
files.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─────────────────────────────────────────────────────────────
|
|
93
|
+
// Function Extraction
|
|
94
|
+
// ─────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function extractFunctions(content, filePath) {
|
|
97
|
+
const functions = [];
|
|
98
|
+
const lines = content.split('\n');
|
|
99
|
+
|
|
100
|
+
let i = 0;
|
|
101
|
+
while (i < lines.length) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
|
|
105
|
+
// Match function declarations
|
|
106
|
+
const funcPatterns = [
|
|
107
|
+
// function name() or async function name()
|
|
108
|
+
/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
|
|
109
|
+
// const name = function() or const name = async function()
|
|
110
|
+
/^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(/,
|
|
111
|
+
// const name = () => or const name = async () =>
|
|
112
|
+
/^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
113
|
+
// const name = async () => (without params parens sometimes)
|
|
114
|
+
/^(?:export\s+)?const\s+(\w+)\s*=\s*async\s+\w+\s*=>/,
|
|
115
|
+
// Class method: name() { or async name() { or public name() {
|
|
116
|
+
/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
let funcName = null;
|
|
120
|
+
let funcStart = i;
|
|
121
|
+
|
|
122
|
+
for (const pattern of funcPatterns) {
|
|
123
|
+
const match = trimmed.match(pattern);
|
|
124
|
+
if (match) {
|
|
125
|
+
funcName = match[1];
|
|
126
|
+
// Skip constructor, get, set for class methods
|
|
127
|
+
if (['constructor', 'get', 'set', 'if', 'for', 'while', 'switch', 'catch'].includes(funcName)) {
|
|
128
|
+
funcName = null;
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (funcName) {
|
|
135
|
+
// Find the function body
|
|
136
|
+
const funcBody = extractFunctionBody(lines, i);
|
|
137
|
+
if (funcBody) {
|
|
138
|
+
functions.push({
|
|
139
|
+
name: funcName,
|
|
140
|
+
startLine: i + 1,
|
|
141
|
+
endLine: i + funcBody.lines.length,
|
|
142
|
+
body: funcBody.content,
|
|
143
|
+
loc: funcBody.lines.length
|
|
144
|
+
});
|
|
145
|
+
i += funcBody.lines.length;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return functions;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractFunctionBody(lines, startLine) {
|
|
157
|
+
let braceDepth = 0;
|
|
158
|
+
let arrowWithoutBrace = false;
|
|
159
|
+
let started = false;
|
|
160
|
+
const bodyLines = [];
|
|
161
|
+
|
|
162
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
163
|
+
const line = lines[i];
|
|
164
|
+
bodyLines.push(line);
|
|
165
|
+
|
|
166
|
+
// Check for arrow function without braces
|
|
167
|
+
if (i === startLine && line.includes('=>') && !line.includes('{')) {
|
|
168
|
+
// Single expression arrow function - ends at semicolon or next statement
|
|
169
|
+
if (line.trim().endsWith(';') || line.trim().endsWith(',')) {
|
|
170
|
+
return { lines: bodyLines, content: bodyLines.join('\n') };
|
|
171
|
+
}
|
|
172
|
+
arrowWithoutBrace = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Count braces
|
|
176
|
+
for (const char of line) {
|
|
177
|
+
if (char === '{') {
|
|
178
|
+
braceDepth++;
|
|
179
|
+
started = true;
|
|
180
|
+
} else if (char === '}') {
|
|
181
|
+
braceDepth--;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle arrow functions without braces (multi-line)
|
|
186
|
+
if (arrowWithoutBrace && !started) {
|
|
187
|
+
if (line.trim().endsWith(';') || line.trim().endsWith(',') ||
|
|
188
|
+
(i + 1 < lines.length && /^[\s]*(?:const|let|var|function|class|export|import|return|\}|\/\/)/.test(lines[i + 1]))) {
|
|
189
|
+
return { lines: bodyLines, content: bodyLines.join('\n') };
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// End of function body
|
|
195
|
+
if (started && braceDepth === 0) {
|
|
196
|
+
return { lines: bodyLines, content: bodyLines.join('\n') };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Safety limit
|
|
200
|
+
if (bodyLines.length > 1000) {
|
|
201
|
+
return { lines: bodyLines, content: bodyLines.join('\n') };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return bodyLines.length > 0 ? { lines: bodyLines, content: bodyLines.join('\n') } : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─────────────────────────────────────────────────────────────
|
|
209
|
+
// Complexity Calculation
|
|
210
|
+
// ─────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function calculateCyclomaticComplexity(code) {
|
|
213
|
+
// Start with 1 (base path)
|
|
214
|
+
let complexity = 1;
|
|
215
|
+
|
|
216
|
+
// Remove strings and comments to avoid false positives
|
|
217
|
+
const cleaned = removeStringsAndComments(code);
|
|
218
|
+
|
|
219
|
+
// Decision points that add to cyclomatic complexity
|
|
220
|
+
const patterns = [
|
|
221
|
+
/\bif\s*\(/g,
|
|
222
|
+
/\belse\s+if\s*\(/g,
|
|
223
|
+
/\bfor\s*\(/g,
|
|
224
|
+
/\bwhile\s*\(/g,
|
|
225
|
+
/\bdo\s*\{/g,
|
|
226
|
+
/\bcase\s+[^:]+:/g,
|
|
227
|
+
/\bcatch\s*\(/g,
|
|
228
|
+
/\?\s*[^:]/g, // Ternary operator (? but not ?. or ??)
|
|
229
|
+
/\&\&/g, // Logical AND
|
|
230
|
+
/\|\|/g, // Logical OR
|
|
231
|
+
/\?\?/g, // Nullish coalescing
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const pattern of patterns) {
|
|
235
|
+
const matches = cleaned.match(pattern);
|
|
236
|
+
if (matches) {
|
|
237
|
+
complexity += matches.length;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return complexity;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function calculateCognitiveComplexity(code) {
|
|
245
|
+
let complexity = 0;
|
|
246
|
+
let nestingLevel = 0;
|
|
247
|
+
|
|
248
|
+
const lines = code.split('\n');
|
|
249
|
+
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
const trimmed = line.trim();
|
|
252
|
+
|
|
253
|
+
// Skip empty lines and comments
|
|
254
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*')) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Track nesting level
|
|
259
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
260
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
261
|
+
|
|
262
|
+
// Check for control flow before adjusting nesting
|
|
263
|
+
// These add 1 + nesting level
|
|
264
|
+
if (/\bif\s*\(/.test(trimmed) && !/\belse\s+if/.test(trimmed)) {
|
|
265
|
+
complexity += 1 + nestingLevel;
|
|
266
|
+
}
|
|
267
|
+
if (/\belse\s+if\s*\(/.test(trimmed)) {
|
|
268
|
+
complexity += 1; // else if doesn't add nesting penalty
|
|
269
|
+
}
|
|
270
|
+
if (/\belse\s*\{/.test(trimmed)) {
|
|
271
|
+
complexity += 1; // else adds 1
|
|
272
|
+
}
|
|
273
|
+
if (/\bfor\s*\(/.test(trimmed)) {
|
|
274
|
+
complexity += 1 + nestingLevel;
|
|
275
|
+
}
|
|
276
|
+
if (/\bwhile\s*\(/.test(trimmed)) {
|
|
277
|
+
complexity += 1 + nestingLevel;
|
|
278
|
+
}
|
|
279
|
+
if (/\bdo\s*\{/.test(trimmed)) {
|
|
280
|
+
complexity += 1 + nestingLevel;
|
|
281
|
+
}
|
|
282
|
+
if (/\bswitch\s*\(/.test(trimmed)) {
|
|
283
|
+
complexity += 1 + nestingLevel;
|
|
284
|
+
}
|
|
285
|
+
if (/\bcatch\s*\(/.test(trimmed)) {
|
|
286
|
+
complexity += 1 + nestingLevel;
|
|
287
|
+
}
|
|
288
|
+
if (/\btry\s*\{/.test(trimmed)) {
|
|
289
|
+
// try doesn't add complexity, just nesting
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Logical operators add 1 each (no nesting penalty)
|
|
293
|
+
const andMatches = trimmed.match(/\&\&/g);
|
|
294
|
+
const orMatches = trimmed.match(/\|\|/g);
|
|
295
|
+
const nullishMatches = trimmed.match(/\?\?/g);
|
|
296
|
+
const ternaryMatches = trimmed.match(/\?[^:?]/g);
|
|
297
|
+
|
|
298
|
+
if (andMatches) complexity += andMatches.length;
|
|
299
|
+
if (orMatches) complexity += orMatches.length;
|
|
300
|
+
if (nullishMatches) complexity += nullishMatches.length;
|
|
301
|
+
if (ternaryMatches) complexity += ternaryMatches.length;
|
|
302
|
+
|
|
303
|
+
// Update nesting after processing line
|
|
304
|
+
// Only count structural nesting (if/for/while/etc), not object literals
|
|
305
|
+
if (/\b(if|for|while|do|switch|try|catch|else)\b/.test(trimmed) && openBraces > 0) {
|
|
306
|
+
nestingLevel += openBraces;
|
|
307
|
+
} else {
|
|
308
|
+
nestingLevel += openBraces;
|
|
309
|
+
}
|
|
310
|
+
nestingLevel -= closeBraces;
|
|
311
|
+
nestingLevel = Math.max(0, nestingLevel);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return complexity;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function removeStringsAndComments(code) {
|
|
318
|
+
// Remove single-line comments
|
|
319
|
+
let result = code.replace(/\/\/.*$/gm, '');
|
|
320
|
+
// Remove multi-line comments
|
|
321
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
322
|
+
// Remove template literals (simplified)
|
|
323
|
+
result = result.replace(/`[^`]*`/g, '""');
|
|
324
|
+
// Remove strings
|
|
325
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
326
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
327
|
+
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─────────────────────────────────────────────────────────────
|
|
332
|
+
// Complexity Rating
|
|
333
|
+
// ─────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
function getRating(cyclomatic) {
|
|
336
|
+
if (cyclomatic <= 10) return { label: 'simple', icon: '✓' };
|
|
337
|
+
if (cyclomatic <= 20) return { label: 'moderate', icon: '◐' };
|
|
338
|
+
if (cyclomatic <= 50) return { label: 'complex', icon: '⚠' };
|
|
339
|
+
return { label: 'very complex', icon: '✗' };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ─────────────────────────────────────────────────────────────
|
|
343
|
+
// Main
|
|
344
|
+
// ─────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
const args = process.argv.slice(2);
|
|
347
|
+
const options = parseCommonArgs(args);
|
|
348
|
+
|
|
349
|
+
// Parse custom options
|
|
350
|
+
let threshold = 0;
|
|
351
|
+
let sortBy = 'cyclomatic';
|
|
352
|
+
let topN = Infinity;
|
|
353
|
+
let summaryOnly = false;
|
|
354
|
+
|
|
355
|
+
const remaining = [];
|
|
356
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
357
|
+
const arg = options.remaining[i];
|
|
358
|
+
|
|
359
|
+
if (arg === '--threshold' || arg === '-t') {
|
|
360
|
+
threshold = parseInt(options.remaining[++i], 10) || 0;
|
|
361
|
+
} else if (arg === '--sort') {
|
|
362
|
+
sortBy = options.remaining[++i];
|
|
363
|
+
} else if (arg === '--top') {
|
|
364
|
+
topN = parseInt(options.remaining[++i], 10) || 20;
|
|
365
|
+
} else if (arg === '--summary') {
|
|
366
|
+
summaryOnly = true;
|
|
367
|
+
} else if (!arg.startsWith('-')) {
|
|
368
|
+
remaining.push(arg);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const targetPath = remaining[0] || '.';
|
|
373
|
+
|
|
374
|
+
if (options.help) {
|
|
375
|
+
console.log(HELP);
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!existsSync(targetPath)) {
|
|
380
|
+
console.error(`Path not found: ${targetPath}`);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const projectRoot = findProjectRoot();
|
|
385
|
+
const out = createOutput(options);
|
|
386
|
+
|
|
387
|
+
// Find files
|
|
388
|
+
let files = [];
|
|
389
|
+
const stat = statSync(targetPath);
|
|
390
|
+
if (stat.isFile()) {
|
|
391
|
+
files = [targetPath];
|
|
392
|
+
} else {
|
|
393
|
+
files = findCodeFiles(targetPath);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (files.length === 0) {
|
|
397
|
+
console.error('No code files found');
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Analyze all functions
|
|
402
|
+
const allFunctions = [];
|
|
403
|
+
const fileSummaries = [];
|
|
404
|
+
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
const content = readFileSync(file, 'utf-8');
|
|
407
|
+
const relPath = relative(projectRoot, file);
|
|
408
|
+
const functions = extractFunctions(content, file);
|
|
409
|
+
|
|
410
|
+
let fileComplexity = 0;
|
|
411
|
+
let fileCognitive = 0;
|
|
412
|
+
|
|
413
|
+
for (const func of functions) {
|
|
414
|
+
const cyclomatic = calculateCyclomaticComplexity(func.body);
|
|
415
|
+
const cognitive = calculateCognitiveComplexity(func.body);
|
|
416
|
+
|
|
417
|
+
fileComplexity += cyclomatic;
|
|
418
|
+
fileCognitive += cognitive;
|
|
419
|
+
|
|
420
|
+
if (cyclomatic >= threshold) {
|
|
421
|
+
allFunctions.push({
|
|
422
|
+
file: relPath,
|
|
423
|
+
name: func.name,
|
|
424
|
+
line: func.startLine,
|
|
425
|
+
loc: func.loc,
|
|
426
|
+
cyclomatic,
|
|
427
|
+
cognitive,
|
|
428
|
+
rating: getRating(cyclomatic)
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (functions.length > 0) {
|
|
434
|
+
fileSummaries.push({
|
|
435
|
+
file: relPath,
|
|
436
|
+
functions: functions.length,
|
|
437
|
+
totalCyclomatic: fileComplexity,
|
|
438
|
+
totalCognitive: fileCognitive,
|
|
439
|
+
avgCyclomatic: Math.round(fileComplexity / functions.length * 10) / 10
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Sort functions
|
|
445
|
+
allFunctions.sort((a, b) => {
|
|
446
|
+
if (sortBy === 'cognitive') return b.cognitive - a.cognitive;
|
|
447
|
+
if (sortBy === 'name') return a.name.localeCompare(b.name);
|
|
448
|
+
if (sortBy === 'loc') return b.loc - a.loc;
|
|
449
|
+
return b.cyclomatic - a.cyclomatic; // default: cyclomatic
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Apply top N limit
|
|
453
|
+
const displayFunctions = allFunctions.slice(0, topN);
|
|
454
|
+
|
|
455
|
+
// Set JSON data
|
|
456
|
+
out.setData('functions', allFunctions);
|
|
457
|
+
out.setData('fileSummaries', fileSummaries);
|
|
458
|
+
out.setData('totalFunctions', allFunctions.length);
|
|
459
|
+
|
|
460
|
+
// Output
|
|
461
|
+
if (summaryOnly) {
|
|
462
|
+
out.header(`📊 Complexity Summary (${fileSummaries.length} files)`);
|
|
463
|
+
out.blank();
|
|
464
|
+
|
|
465
|
+
// Sort files by total complexity
|
|
466
|
+
fileSummaries.sort((a, b) => b.totalCyclomatic - a.totalCyclomatic);
|
|
467
|
+
|
|
468
|
+
const rows = fileSummaries.map(f => [
|
|
469
|
+
f.file,
|
|
470
|
+
`${f.functions} fn`,
|
|
471
|
+
`avg ${f.avgCyclomatic}`,
|
|
472
|
+
`total ${f.totalCyclomatic}`
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
formatTable(rows).forEach(line => out.add(line));
|
|
476
|
+
} else {
|
|
477
|
+
out.header(`📊 Function Complexity (${displayFunctions.length}${allFunctions.length > topN ? ` of ${allFunctions.length}` : ''} functions)`);
|
|
478
|
+
out.blank();
|
|
479
|
+
|
|
480
|
+
if (displayFunctions.length === 0) {
|
|
481
|
+
out.add(`No functions found with complexity >= ${threshold}`);
|
|
482
|
+
} else {
|
|
483
|
+
// Group by file
|
|
484
|
+
const byFile = new Map();
|
|
485
|
+
for (const func of displayFunctions) {
|
|
486
|
+
if (!byFile.has(func.file)) byFile.set(func.file, []);
|
|
487
|
+
byFile.get(func.file).push(func);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const [file, funcs] of byFile) {
|
|
491
|
+
out.add(`${file}`);
|
|
492
|
+
for (const func of funcs) {
|
|
493
|
+
const rating = func.rating;
|
|
494
|
+
out.add(` ${rating.icon} ${func.name} (L${func.line}): cyclo=${func.cyclomatic} cog=${func.cognitive} loc=${func.loc}`);
|
|
495
|
+
}
|
|
496
|
+
out.blank();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Summary stats
|
|
502
|
+
if (!options.quiet && allFunctions.length > 0) {
|
|
503
|
+
const totalCyclo = allFunctions.reduce((sum, f) => sum + f.cyclomatic, 0);
|
|
504
|
+
const avgCyclo = Math.round(totalCyclo / allFunctions.length * 10) / 10;
|
|
505
|
+
const maxCyclo = Math.max(...allFunctions.map(f => f.cyclomatic));
|
|
506
|
+
|
|
507
|
+
const complex = allFunctions.filter(f => f.cyclomatic > 10).length;
|
|
508
|
+
const veryComplex = allFunctions.filter(f => f.cyclomatic > 20).length;
|
|
509
|
+
|
|
510
|
+
out.add('---');
|
|
511
|
+
out.add(`Avg complexity: ${avgCyclo} | Max: ${maxCyclo} | Complex (>10): ${complex} | Very complex (>20): ${veryComplex}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
out.print();
|