pi-lens 2.0.7 → 2.0.8

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.
Files changed (34) hide show
  1. package/clients/ast-grep-client.test.ts +146 -116
  2. package/clients/ast-grep-client.ts +645 -551
  3. package/clients/biome-client.test.ts +154 -137
  4. package/clients/biome-client.ts +397 -337
  5. package/clients/complexity-client.test.ts +188 -200
  6. package/clients/complexity-client.ts +815 -667
  7. package/clients/dependency-checker.test.ts +55 -55
  8. package/clients/dependency-checker.ts +358 -333
  9. package/clients/go-client.test.ts +121 -111
  10. package/clients/go-client.ts +218 -216
  11. package/clients/jscpd-client.test.ts +132 -132
  12. package/clients/jscpd-client.ts +155 -118
  13. package/clients/knip-client.test.ts +123 -133
  14. package/clients/knip-client.ts +231 -218
  15. package/clients/metrics-client.test.ts +171 -167
  16. package/clients/metrics-client.ts +283 -252
  17. package/clients/ruff-client.test.ts +128 -117
  18. package/clients/ruff-client.ts +300 -269
  19. package/clients/rust-client.test.ts +104 -85
  20. package/clients/rust-client.ts +241 -234
  21. package/clients/subprocess-client.ts +1 -1
  22. package/clients/test-runner-client.test.ts +248 -215
  23. package/clients/test-runner-client.ts +728 -608
  24. package/clients/test-utils.ts +10 -3
  25. package/clients/todo-scanner.test.ts +288 -202
  26. package/clients/todo-scanner.ts +225 -187
  27. package/clients/type-coverage-client.test.ts +119 -119
  28. package/clients/type-coverage-client.ts +142 -115
  29. package/clients/types.ts +28 -28
  30. package/clients/typescript-client.test.ts +99 -93
  31. package/clients/typescript-client.ts +527 -502
  32. package/index.ts +662 -212
  33. package/package.json +1 -1
  34. package/tsconfig.json +12 -12
@@ -22,701 +22,849 @@ import * as ts from "typescript";
22
22
  // --- Types ---
23
23
 
24
24
  export interface FileComplexity {
25
- filePath: string;
26
- maxNestingDepth: number;
27
- avgFunctionLength: number;
28
- maxFunctionLength: number;
29
- functionCount: number;
30
- cyclomaticComplexity: number; // Average across functions
31
- maxCyclomaticComplexity: number; // Most complex function
32
- cognitiveComplexity: number;
33
- halsteadVolume: number;
34
- maintainabilityIndex: number; // 0-100
35
- linesOfCode: number;
36
- commentLines: number;
37
- codeEntropy: number; // Shannon entropy (0-1, lower = more predictable)
38
- // AI slop indicators
39
- maxParamsInFunction: number; // Max parameters in any function
40
- aiCommentPatterns: number; // Emoji comments, boilerplate phrases
41
- singleUseFunctions: number; // Functions only called once (estimated)
42
- tryCatchCount: number; // Number of try/catch blocks
25
+ filePath: string;
26
+ maxNestingDepth: number;
27
+ avgFunctionLength: number;
28
+ maxFunctionLength: number;
29
+ functionCount: number;
30
+ cyclomaticComplexity: number; // Average across functions
31
+ maxCyclomaticComplexity: number; // Most complex function
32
+ cognitiveComplexity: number;
33
+ halsteadVolume: number;
34
+ maintainabilityIndex: number; // 0-100
35
+ linesOfCode: number;
36
+ commentLines: number;
37
+ codeEntropy: number; // Shannon entropy (0-1, lower = more predictable)
38
+ // AI slop indicators
39
+ maxParamsInFunction: number; // Max parameters in any function
40
+ aiCommentPatterns: number; // Emoji comments, boilerplate phrases
41
+ singleUseFunctions: number; // Functions only called once (estimated)
42
+ tryCatchCount: number; // Number of try/catch blocks
43
43
  }
44
44
 
45
45
  export interface FunctionMetrics {
46
- name: string;
47
- line: number;
48
- length: number;
49
- cyclomatic: number;
50
- cognitive: number;
51
- nestingDepth: number;
46
+ name: string;
47
+ line: number;
48
+ length: number;
49
+ cyclomatic: number;
50
+ cognitive: number;
51
+ nestingDepth: number;
52
52
  }
53
53
 
54
54
  // --- Constants ---
55
55
 
56
56
  // Nodes that increase cyclomatic complexity
57
57
  const CYCLOMAL_NODES = new Set([
58
- ts.SyntaxKind.IfStatement,
59
- ts.SyntaxKind.WhileStatement,
60
- ts.SyntaxKind.ForStatement,
61
- ts.SyntaxKind.ForInStatement,
62
- ts.SyntaxKind.ForOfStatement,
63
- ts.SyntaxKind.CaseClause,
64
- ts.SyntaxKind.ConditionalExpression,
65
- ts.SyntaxKind.BinaryExpression, // && and ||
58
+ ts.SyntaxKind.IfStatement,
59
+ ts.SyntaxKind.WhileStatement,
60
+ ts.SyntaxKind.ForStatement,
61
+ ts.SyntaxKind.ForInStatement,
62
+ ts.SyntaxKind.ForOfStatement,
63
+ ts.SyntaxKind.CaseClause,
64
+ ts.SyntaxKind.ConditionalExpression,
65
+ ts.SyntaxKind.BinaryExpression, // && and ||
66
66
  ]);
67
67
 
68
68
  // Nodes that increase cognitive complexity (with nesting penalty)
69
69
  const COGNITIVE_NODES = new Set([
70
- ts.SyntaxKind.IfStatement,
71
- ts.SyntaxKind.WhileStatement,
72
- ts.SyntaxKind.ForStatement,
73
- ts.SyntaxKind.ForInStatement,
74
- ts.SyntaxKind.ForOfStatement,
75
- ts.SyntaxKind.SwitchStatement,
76
- ts.SyntaxKind.CaseClause,
77
- ts.SyntaxKind.ConditionalExpression,
78
- ts.SyntaxKind.CatchClause,
70
+ ts.SyntaxKind.IfStatement,
71
+ ts.SyntaxKind.WhileStatement,
72
+ ts.SyntaxKind.ForStatement,
73
+ ts.SyntaxKind.ForInStatement,
74
+ ts.SyntaxKind.ForOfStatement,
75
+ ts.SyntaxKind.SwitchStatement,
76
+ ts.SyntaxKind.CaseClause,
77
+ ts.SyntaxKind.ConditionalExpression,
78
+ ts.SyntaxKind.CatchClause,
79
79
  ]);
80
80
 
81
81
  // Nesting-increasing nodes
82
82
  const NESTING_NODES = new Set([
83
- ts.SyntaxKind.IfStatement,
84
- ts.SyntaxKind.WhileStatement,
85
- ts.SyntaxKind.ForStatement,
86
- ts.SyntaxKind.ForInStatement,
87
- ts.SyntaxKind.ForOfStatement,
88
- ts.SyntaxKind.SwitchStatement,
89
- ts.SyntaxKind.FunctionDeclaration,
90
- ts.SyntaxKind.FunctionExpression,
91
- ts.SyntaxKind.ArrowFunction,
92
- ts.SyntaxKind.ClassDeclaration,
93
- ts.SyntaxKind.MethodDeclaration,
94
- ts.SyntaxKind.TryStatement,
95
- ts.SyntaxKind.CatchClause,
83
+ ts.SyntaxKind.IfStatement,
84
+ ts.SyntaxKind.WhileStatement,
85
+ ts.SyntaxKind.ForStatement,
86
+ ts.SyntaxKind.ForInStatement,
87
+ ts.SyntaxKind.ForOfStatement,
88
+ ts.SyntaxKind.SwitchStatement,
89
+ ts.SyntaxKind.FunctionDeclaration,
90
+ ts.SyntaxKind.FunctionExpression,
91
+ ts.SyntaxKind.ArrowFunction,
92
+ ts.SyntaxKind.ClassDeclaration,
93
+ ts.SyntaxKind.MethodDeclaration,
94
+ ts.SyntaxKind.TryStatement,
95
+ ts.SyntaxKind.CatchClause,
96
96
  ]);
97
97
 
98
98
  // Function-like nodes
99
99
  const FUNCTION_LIKE_NODES = new Set([
100
- ts.SyntaxKind.FunctionDeclaration,
101
- ts.SyntaxKind.FunctionExpression,
102
- ts.SyntaxKind.ArrowFunction,
103
- ts.SyntaxKind.MethodDeclaration,
104
- ts.SyntaxKind.Constructor,
105
- ts.SyntaxKind.GetAccessor,
106
- ts.SyntaxKind.SetAccessor,
100
+ ts.SyntaxKind.FunctionDeclaration,
101
+ ts.SyntaxKind.FunctionExpression,
102
+ ts.SyntaxKind.ArrowFunction,
103
+ ts.SyntaxKind.MethodDeclaration,
104
+ ts.SyntaxKind.Constructor,
105
+ ts.SyntaxKind.GetAccessor,
106
+ ts.SyntaxKind.SetAccessor,
107
107
  ]);
108
108
 
109
109
  // Halstead operators (common operators)
110
110
  const HALSTEAD_OPERATORS = new Set([
111
- ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken,
112
- ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.SlashToken,
113
- ts.SyntaxKind.PercentToken, ts.SyntaxKind.AmpersandToken,
114
- ts.SyntaxKind.BarToken, ts.SyntaxKind.CaretToken,
115
- ts.SyntaxKind.LessThanToken, ts.SyntaxKind.GreaterThanToken,
116
- ts.SyntaxKind.LessThanEqualsToken, ts.SyntaxKind.GreaterThanEqualsToken,
117
- ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken,
118
- ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken,
119
- ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken,
120
- ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
121
- ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
122
- ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.BarEqualsToken,
123
- ts.SyntaxKind.LessThanLessThanToken, ts.SyntaxKind.GreaterThanGreaterThanToken,
124
- ts.SyntaxKind.QuestionToken, ts.SyntaxKind.ColonToken,
125
- ts.SyntaxKind.EqualsToken, ts.SyntaxKind.EqualsGreaterThanToken,
126
- ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken,
127
- ts.SyntaxKind.ExclamationToken, ts.SyntaxKind.TildeToken,
128
- ts.SyntaxKind.CommaToken, ts.SyntaxKind.SemicolonToken,
129
- ts.SyntaxKind.DotToken, ts.SyntaxKind.QuestionDotToken,
111
+ ts.SyntaxKind.PlusToken,
112
+ ts.SyntaxKind.MinusToken,
113
+ ts.SyntaxKind.AsteriskToken,
114
+ ts.SyntaxKind.SlashToken,
115
+ ts.SyntaxKind.PercentToken,
116
+ ts.SyntaxKind.AmpersandToken,
117
+ ts.SyntaxKind.BarToken,
118
+ ts.SyntaxKind.CaretToken,
119
+ ts.SyntaxKind.LessThanToken,
120
+ ts.SyntaxKind.GreaterThanToken,
121
+ ts.SyntaxKind.LessThanEqualsToken,
122
+ ts.SyntaxKind.GreaterThanEqualsToken,
123
+ ts.SyntaxKind.EqualsEqualsToken,
124
+ ts.SyntaxKind.ExclamationEqualsToken,
125
+ ts.SyntaxKind.EqualsEqualsEqualsToken,
126
+ ts.SyntaxKind.ExclamationEqualsEqualsToken,
127
+ ts.SyntaxKind.PlusPlusToken,
128
+ ts.SyntaxKind.MinusMinusToken,
129
+ ts.SyntaxKind.PlusEqualsToken,
130
+ ts.SyntaxKind.MinusEqualsToken,
131
+ ts.SyntaxKind.AsteriskEqualsToken,
132
+ ts.SyntaxKind.SlashEqualsToken,
133
+ ts.SyntaxKind.AmpersandEqualsToken,
134
+ ts.SyntaxKind.BarEqualsToken,
135
+ ts.SyntaxKind.LessThanLessThanToken,
136
+ ts.SyntaxKind.GreaterThanGreaterThanToken,
137
+ ts.SyntaxKind.QuestionToken,
138
+ ts.SyntaxKind.ColonToken,
139
+ ts.SyntaxKind.EqualsToken,
140
+ ts.SyntaxKind.EqualsGreaterThanToken,
141
+ ts.SyntaxKind.AmpersandAmpersandToken,
142
+ ts.SyntaxKind.BarBarToken,
143
+ ts.SyntaxKind.ExclamationToken,
144
+ ts.SyntaxKind.TildeToken,
145
+ ts.SyntaxKind.CommaToken,
146
+ ts.SyntaxKind.SemicolonToken,
147
+ ts.SyntaxKind.DotToken,
148
+ ts.SyntaxKind.QuestionDotToken,
130
149
  ]);
131
150
 
132
151
  // --- Client ---
133
152
 
134
153
  export class ComplexityClient {
135
- private log: (msg: string) => void;
136
-
137
- constructor(verbose = false) {
138
- this.log = verbose
139
- ? (msg: string) => console.log(`[complexity] ${msg}`)
140
- : () => {};
141
- }
142
-
143
- /**
144
- * Check if file is supported (TS/JS)
145
- */
146
- isSupportedFile(filePath: string): boolean {
147
- const ext = path.extname(filePath).toLowerCase();
148
- return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext);
149
- }
150
-
151
- /**
152
- * Analyze complexity metrics for a file
153
- */
154
- analyzeFile(filePath: string): FileComplexity | null {
155
- const absolutePath = path.resolve(filePath);
156
- if (!fs.existsSync(absolutePath)) return null;
157
-
158
- try {
159
- const content = fs.readFileSync(absolutePath, "utf-8");
160
- const lines = content.split("\n");
161
- const sourceFile = ts.createSourceFile(
162
- filePath,
163
- content,
164
- ts.ScriptTarget.Latest,
165
- true
166
- );
167
-
168
- // Count lines of code (non-empty, non-comment)
169
- const { codeLines, commentLines } = this.countLines(sourceFile, lines);
170
-
171
- // Collect function metrics
172
- const functions: FunctionMetrics[] = [];
173
- this.collectFunctionMetrics(sourceFile, sourceFile, functions, 0);
174
-
175
- // Calculate file-level metrics
176
- const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
177
- const cyclomatic = this.calculateCyclomaticComplexity(sourceFile);
178
- const cognitive = this.calculateCognitiveComplexity(sourceFile);
179
- const halstead = this.calculateHalsteadVolume(sourceFile);
180
-
181
- // Function length stats
182
- const funcLengths = functions.map(f => f.length);
183
- const avgFunctionLength = funcLengths.length > 0
184
- ? Math.round(funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length)
185
- : 0;
186
- const maxFunctionLength = funcLengths.length > 0 ? Math.max(...funcLengths) : 0;
187
-
188
- // Function cyclomatic stats
189
- const cyclomatics = functions.map(f => f.cyclomatic);
190
- const avgCyclomatic = cyclomatics.length > 0
191
- ? Math.round(cyclomatics.reduce((a, b) => a + b, 0) / cyclomatics.length)
192
- : 1;
193
- const maxCyclomatic = cyclomatics.length > 0 ? Math.max(...cyclomatics) : 1;
194
-
195
- // Maintainability Index (simplified Microsoft formula)
196
- // MI = max(0, (171 - 5.2 * ln(Halstead) - 0.23 * Cyclomatic - 16.2 * ln(LOC)) * 100 / 171)
197
- const maintainabilityIndex = this.calculateMaintainabilityIndex(
198
- halstead,
199
- avgCyclomatic,
200
- codeLines,
201
- commentLines
202
- );
203
-
204
- // Code Entropy (Shannon entropy of code tokens)
205
- const codeEntropy = this.calculateCodeEntropy(content);
206
-
207
- // AI slop indicators
208
- const maxParamsInFunction = this.calculateMaxParams(functions);
209
- const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
210
- const singleUseFunctions = this.countSingleUseFunctions(functions);
211
- const tryCatchCount = this.countTryCatch(sourceFile);
212
-
213
- return {
214
- filePath: path.relative(process.cwd(), absolutePath),
215
- maxNestingDepth,
216
- avgFunctionLength,
217
- maxFunctionLength,
218
- functionCount: functions.length,
219
- cyclomaticComplexity: avgCyclomatic,
220
- maxCyclomaticComplexity: maxCyclomatic,
221
- cognitiveComplexity: cognitive,
222
- halsteadVolume: Math.round(halstead * 10) / 10,
223
- maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
224
- linesOfCode: codeLines,
225
- commentLines,
226
- codeEntropy: Math.round(codeEntropy * 100) / 100,
227
- maxParamsInFunction,
228
- aiCommentPatterns,
229
- singleUseFunctions,
230
- tryCatchCount,
231
- };
232
- } catch (err: any) {
233
- this.log(`Analysis error for ${filePath}: ${err.message}`);
234
- return null;
235
- }
236
- }
237
-
238
- /**
239
- * Format metrics for display
240
- */
241
- formatMetrics(metrics: FileComplexity): string {
242
- const parts: string[] = [];
243
-
244
- // Maintainability Index (most important)
245
- const miLabel = metrics.maintainabilityIndex >= 80 ? "✓" :
246
- metrics.maintainabilityIndex >= 60 ? "⚠" : "✗";
247
- parts.push(`${miLabel} Maintainability: ${metrics.maintainabilityIndex}/100`);
248
-
249
- // Complexity metrics
250
- if (metrics.cyclomaticComplexity > 5 || metrics.maxCyclomaticComplexity > 10) {
251
- const avg = metrics.cyclomaticComplexity;
252
- const max = metrics.maxCyclomaticComplexity;
253
- parts.push(` Cyclomatic: avg ${avg}, max ${max} (${metrics.functionCount} functions)`);
254
- }
255
-
256
- if (metrics.cognitiveComplexity > 15) {
257
- parts.push(` Cognitive: ${metrics.cognitiveComplexity} (high mental complexity)`);
258
- }
259
-
260
- // Nesting depth
261
- if (metrics.maxNestingDepth > 4) {
262
- parts.push(` Max nesting: ${metrics.maxNestingDepth} levels (consider extracting)`);
263
- }
264
-
265
- // Code entropy (in bits, >3.5 = risky AI-induced complexity)
266
- if (metrics.codeEntropy > 3.5) {
267
- parts.push(` Entropy: ${metrics.codeEntropy.toFixed(1)} bits (>3.5 — risky AI-induced complexity)`);
268
- }
269
-
270
- // Function length
271
- if (metrics.maxFunctionLength > 50) {
272
- parts.push(` Longest function: ${metrics.maxFunctionLength} lines (avg: ${metrics.avgFunctionLength})`);
273
- }
274
-
275
- // Halstead (only if notably high)
276
- if (metrics.halsteadVolume > 500) {
277
- parts.push(` Halstead volume: ${metrics.halsteadVolume} (high vocabulary)`);
278
- }
279
-
280
- return parts.length > 0 ? `[Complexity] ${metrics.filePath}\n${parts.join("\n")}` : "";
281
- }
282
-
283
- /**
284
- * Calculate max parameters across all functions
285
- */
286
- private calculateMaxParams(functions: FunctionMetrics[]): number {
287
- let maxParams = 0;
288
- // We stored function params in the metrics during analysis
289
- // For now, estimate based on function length (longer functions often have more params)
290
- return Math.min(10, Math.max(2, Math.round(functions.reduce((a, f) => a + f.length, 0) / Math.max(1, functions.length) / 5)));
291
- }
292
-
293
- /**
294
- * Count AI comment patterns (emojis, boilerplate phrases)
295
- */
296
- private countAICommentPatterns(sourceFile: ts.SourceFile): number {
297
- const sourceText = sourceFile.getText();
298
- let count = 0;
299
-
300
- const aiPatterns = [
301
- /[🔍✅📝🔧🐛⚠️🚀💡🎯📌🏷️🔑🏗️🧪🗑️🔄♻️📋🔖📊💬🔥💎⭐🌟🎯🎨🔧🛠️]/u,
302
- /\/\/\s*(Initialize|Setup|Clean up|Create|Define|Check if|Handle|Process|Validate|Return|Get|Set|Add|Remove|Update|Fetch)\b/i,
303
- /\/\/\s*(This function|This method|This code|Here we|Now we)\b/i,
304
- /\/\*\*?\s*(Overview|Summary|Description|Example|Usage)\s*\*?\//i,
305
- ];
306
-
307
- const lines = sourceText.split('\n');
308
- for (const line of lines) {
309
- // Only check comment lines
310
- const trimmed = line.trim();
311
- if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
312
- for (const pattern of aiPatterns) {
313
- if (pattern.test(line)) {
314
- count++;
315
- break;
316
- }
317
- }
318
- }
319
- }
320
-
321
- return count;
322
- }
323
-
324
- /**
325
- * Count functions that appear to be single-use (helper patterns)
326
- */
327
- private countSingleUseFunctions(functions: FunctionMetrics[]): number {
328
- // Heuristic: small functions (< 10 lines) with simple names are often single-use
329
- const smallHelpers = functions.filter(f =>
330
- f.length < 10 &&
331
- f.cyclomatic <= 2 &&
332
- /^(get|set|check|is|has|validate|format|parse|convert|create|make)/i.test(f.name)
333
- );
334
- return smallHelpers.length;
335
- }
336
-
337
- /**
338
- * Count try/catch blocks (generic error handling pattern)
339
- */
340
- private countTryCatch(sourceFile: ts.SourceFile): number {
341
- let count = 0;
342
-
343
- const visit = (node: ts.Node) => {
344
- if (ts.isTryStatement(node)) {
345
- count++;
346
- }
347
- ts.forEachChild(node, visit);
348
- };
349
-
350
- ts.forEachChild(sourceFile, visit);
351
- return count;
352
- }
353
-
354
- /**
355
- * Check thresholds and return actionable warnings
356
- */
357
- checkThresholds(metrics: FileComplexity): string[] {
358
- const warnings: string[] = [];
359
-
360
- if (metrics.maintainabilityIndex < 60) {
361
- warnings.push(`Maintainability dropped to ${metrics.maintainabilityIndex} — extract logic into helper functions`);
362
- }
363
-
364
- if (metrics.cyclomaticComplexity > 10) {
365
- warnings.push(`High complexity (${metrics.cyclomaticComplexity}) — use early returns or switch expressions`);
366
- }
367
-
368
- if (metrics.cognitiveComplexity > 15) {
369
- warnings.push(`Cognitive complexity (${metrics.cognitiveComplexity}) simplify logic flow`);
370
- }
371
-
372
- if (metrics.maxNestingDepth > 4) {
373
- warnings.push(`Deep nesting (${metrics.maxNestingDepth} levels) — extract nested logic into separate functions`);
374
- }
375
-
376
- if (metrics.codeEntropy > 3.5) {
377
- warnings.push(`High entropy (${metrics.codeEntropy.toFixed(1)} bits) — follow project conventions`);
378
- }
379
-
380
- // Comments ratio (>30% = excessive comments, AI slop signal)
381
- const totalLines = metrics.linesOfCode + metrics.commentLines;
382
- if (totalLines > 10 && metrics.commentLines / totalLines > 0.3) {
383
- warnings.push(`Excessive comments (${Math.round(metrics.commentLines / totalLines * 100)}%) remove obvious comments`);
384
- }
385
-
386
- // Verbose code (long functions with low complexity = overly verbose)
387
- if (metrics.avgFunctionLength > 30 && metrics.cyclomaticComplexity < 3) {
388
- warnings.push(`Verbose code (avg ${Math.round(metrics.avgFunctionLength)} lines, low complexity) — simplify or extract`);
389
- }
390
-
391
- // AI slop: Emoji/boilerplate comments
392
- if (metrics.aiCommentPatterns > 5) {
393
- warnings.push(`AI-style comments (${metrics.aiCommentPatterns}) — remove hand-holding comments`);
394
- }
395
-
396
- // AI slop: Too many try/catch blocks (lazy error handling)
397
- if (metrics.tryCatchCount > 5) {
398
- warnings.push(`Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`);
399
- }
400
-
401
- // AI slop: Over-abstraction (many single-use helper functions)
402
- if (metrics.singleUseFunctions > 3 && metrics.functionCount > 5) {
403
- warnings.push(`Over-abstraction (${metrics.singleUseFunctions} single-use helpers) — inline or consolidate`);
404
- }
405
-
406
- // AI slop: Functions with too many parameters
407
- if (metrics.maxParamsInFunction > 6) {
408
- warnings.push(`Long parameter list (${metrics.maxParamsInFunction} params) — use options object`);
409
- }
410
-
411
- return warnings;
412
- }
413
-
414
- /**
415
- * Format delta for session summary
416
- */
417
- formatDelta(previous: FileComplexity, current: FileComplexity): string {
418
- const parts: string[] = [];
419
-
420
- const miDelta = current.maintainabilityIndex - previous.maintainabilityIndex;
421
- if (Math.abs(miDelta) > 1) {
422
- const arrow = miDelta > 0 ? "↑" : "↓";
423
- const sign = miDelta > 0 ? "+" : "";
424
- parts.push(` ${arrow} ${current.filePath}: MI ${previous.maintainabilityIndex} → ${current.maintainabilityIndex} (${sign}${miDelta.toFixed(1)})`);
425
- }
426
-
427
- const cogDelta = current.cognitiveComplexity - previous.cognitiveComplexity;
428
- if (Math.abs(cogDelta) > 3) {
429
- const arrow = cogDelta > 0 ? "↑" : "↓";
430
- const sign = cogDelta > 0 ? "+" : "";
431
- parts.push(` ${arrow} ${current.filePath}: cognitive ${previous.cognitiveComplexity} ${current.cognitiveComplexity} (${sign}${cogDelta})`);
432
- }
433
-
434
- return parts.join("\n");
435
- }
436
-
437
- // --- Private: Line Counting ---
438
-
439
- private countLines(sourceFile: ts.SourceFile, lines: string[]): { codeLines: number; commentLines: number } {
440
- let commentLines = 0;
441
- const commentPositions = new Set<number>();
442
-
443
- // Find comment positions
444
- const visitComments = (node: ts.Node) => {
445
- ts.forEachChild(node, visitComments);
446
- };
447
-
448
- // Scan for comments using text
449
- const text = sourceFile.getFullText();
450
- const commentRegex = /\/\/.*$|\/\*[\s\S]*?\*\//gm;
451
- let match;
452
- while ((match = commentRegex.exec(text)) !== null) {
453
- const lineStart = text.lastIndexOf("\n", match.index) + 1;
454
- const startLine = text.substring(0, lineStart).split("\n").length - 1;
455
- const endLine = text.substring(0, match.index + match[0].length).split("\n").length - 1;
456
- for (let i = startLine; i <= endLine; i++) {
457
- commentPositions.add(i);
458
- }
459
- }
460
-
461
- commentLines = commentPositions.size;
462
- const codeLines = lines.filter((line, i) => {
463
- const trimmed = line.trim();
464
- return trimmed.length > 0 && !commentPositions.has(i);
465
- }).length;
466
-
467
- return { codeLines, commentLines };
468
- }
469
-
470
- // --- Private: Function Metrics Collection ---
471
-
472
- private collectFunctionMetrics(
473
- node: ts.Node,
474
- sourceFile: ts.SourceFile,
475
- functions: FunctionMetrics[],
476
- nestingLevel: number
477
- ): void {
478
- if (FUNCTION_LIKE_NODES.has(node.kind)) {
479
- const funcNode = node as ts.FunctionLikeDeclaration;
480
- const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line;
481
- const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line;
482
- const length = endLine - startLine + 1;
483
-
484
- const cyclomatic = this.nodeCyclomaticComplexity(node, 0);
485
- const cognitive = this.nodeCognitiveComplexity(node, nestingLevel);
486
- const maxNesting = this.calculateMaxNesting(node, 0);
487
-
488
- const name = funcNode.name
489
- ? funcNode.name.getText(sourceFile)
490
- : `<anonymous@L${startLine + 1}>`;
491
-
492
- functions.push({
493
- name,
494
- line: startLine + 1,
495
- length,
496
- cyclomatic,
497
- cognitive,
498
- nestingDepth: maxNesting,
499
- });
500
- }
501
-
502
- // Track nesting depth changes
503
- const newNesting = NESTING_NODES.has(node.kind) ? nestingLevel + 1 : nestingLevel;
504
- ts.forEachChild(node, (child) => {
505
- this.collectFunctionMetrics(child, sourceFile, functions, newNesting);
506
- });
507
- }
508
-
509
- // --- Private: Max Nesting Depth ---
510
-
511
- private calculateMaxNesting(node: ts.Node, currentDepth: number): number {
512
- let maxDepth = currentDepth;
513
-
514
- if (NESTING_NODES.has(node.kind)) {
515
- currentDepth++;
516
- maxDepth = Math.max(maxDepth, currentDepth);
517
- }
518
-
519
- ts.forEachChild(node, (child) => {
520
- const childMax = this.calculateMaxNesting(child, currentDepth);
521
- maxDepth = Math.max(maxDepth, childMax);
522
- });
523
-
524
- return maxDepth;
525
- }
526
-
527
- // --- Private: Cyclomatic Complexity ---
528
-
529
- private calculateCyclomaticComplexity(node: ts.Node): number {
530
- return this.nodeCyclomaticComplexity(node, 0);
531
- }
532
-
533
- private nodeCyclomaticComplexity(node: ts.Node, complexity: number): number {
534
- // Base increment for branching nodes
535
- if (CYCLOMAL_NODES.has(node.kind)) {
536
- complexity++;
537
- }
538
-
539
- // Binary && and || add complexity
540
- if (node.kind === ts.SyntaxKind.BinaryExpression) {
541
- const binary = node as ts.BinaryExpression;
542
- if (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
543
- binary.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
544
- complexity++;
545
- }
546
- }
547
-
548
- ts.forEachChild(node, (child) => {
549
- complexity = this.nodeCyclomaticComplexity(child, complexity);
550
- });
551
-
552
- return complexity;
553
- }
554
-
555
- // --- Private: Cognitive Complexity ---
556
- // Based on SonarSource's Cognitive Complexity specification
557
- // Increment for: if, for, while, case, catch, conditional
558
- // Additional increment for nesting
559
-
560
- private calculateCognitiveComplexity(node: ts.Node): number {
561
- return this.nodeCognitiveComplexity(node, 0);
562
- }
563
-
564
- private nodeCognitiveComplexity(node: ts.Node, nestingDepth: number): number {
565
- let complexity = 0;
566
-
567
- // Structures that contribute to cognitive complexity
568
- if (COGNITIVE_NODES.has(node.kind)) {
569
- // Base increment + nesting penalty
570
- complexity += 1 + nestingDepth;
571
- }
572
-
573
- // Break/continue with label add to complexity
574
- if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
575
- if (node.label) {
576
- complexity += 1 + nestingDepth;
577
- }
578
- }
579
-
580
- // Binary && and || contribute to complexity
581
- if (node.kind === ts.SyntaxKind.BinaryExpression) {
582
- const binary = node as ts.BinaryExpression;
583
- if (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
584
- binary.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
585
- complexity += 1;
586
- }
587
- }
588
-
589
- // Calculate nesting for children
590
- const increasesNesting = NESTING_NODES.has(node.kind);
591
- const childNesting = increasesNesting ? nestingDepth + 1 : nestingDepth;
592
-
593
- ts.forEachChild(node, (child) => {
594
- complexity += this.nodeCognitiveComplexity(child, childNesting);
595
- });
596
-
597
- return complexity;
598
- }
599
-
600
- // --- Private: Halstead Volume ---
601
- // V = N * log2(n) where N = total operators+operands, n = unique operators+operands
602
-
603
- private calculateHalsteadVolume(node: ts.Node): number {
604
- const operators = new Set<string>();
605
- const operands = new Set<string>();
606
- let totalOperators = 0;
607
- let totalOperands = 0;
608
-
609
- const visit = (n: ts.Node) => {
610
- // Check if it's an operator
611
- if (HALSTEAD_OPERATORS.has(n.kind)) {
612
- const opText = ts.SyntaxKind[n.kind];
613
- operators.add(opText);
614
- totalOperators++;
615
- }
616
- // Check for identifiers (operands)
617
- else if (ts.isIdentifier(n)) {
618
- const text = n.getText();
619
- // Skip keywords that are parsed as identifiers
620
- if (!this.isKeyword(text)) {
621
- operands.add(text);
622
- totalOperands++;
623
- }
624
- }
625
- // Check for literals (operands)
626
- else if (ts.isNumericLiteral(n) || ts.isStringLiteral(n) ||
627
- n.kind === ts.SyntaxKind.TrueKeyword ||
628
- n.kind === ts.SyntaxKind.FalseKeyword ||
629
- n.kind === ts.SyntaxKind.NullKeyword ||
630
- n.kind === ts.SyntaxKind.UndefinedKeyword) {
631
- const text = n.getText();
632
- operands.add(text);
633
- totalOperands++;
634
- }
635
-
636
- ts.forEachChild(n, visit);
637
- };
638
-
639
- visit(node);
640
-
641
- const uniqueOps = operators.size + operands.size;
642
- const totalOps = totalOperators + totalOperands;
643
-
644
- if (uniqueOps === 0 || totalOps === 0) return 0;
645
-
646
- // V = N * log2(n)
647
- return totalOps * Math.log2(uniqueOps);
648
- }
649
-
650
- /**
651
- * Calculate Shannon entropy of code tokens (in bits)
652
- * Uses log2 for entropy measured in bits
653
- * Threshold: >3.5 bits indicates risky AI-induced complexity
654
- */
655
- private calculateCodeEntropy(sourceText: string): number {
656
- // Tokenize by splitting on whitespace and common delimiters
657
- const tokens = sourceText
658
- .replace(/\/\/.*/g, "") // Remove single-line comments
659
- .replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments
660
- .replace(/["'`][^"'`]*["'`]/g, "STR") // Normalize strings
661
- .replace(/\b\d+(\.\d+)?\b/g, "NUM") // Normalize numbers
662
- .split(/[\s\n\r\t,;:()[\]{}=<>!&|+\-*/%^~?]+/)
663
- .filter(t => t.length > 0);
664
-
665
- if (tokens.length === 0) return 0;
666
-
667
- // Count token frequencies
668
- const freq = new Map<string, number>();
669
- for (const token of tokens) {
670
- freq.set(token, (freq.get(token) || 0) + 1);
671
- }
672
-
673
- // Calculate Shannon entropy in bits: H = -sum(p * log2(p))
674
- let entropy = 0;
675
- for (const count of freq.values()) {
676
- const p = count / tokens.length;
677
- if (p > 0) {
678
- entropy -= p * Math.log2(p);
679
- }
680
- }
681
-
682
- return entropy; // Return in bits, not normalized
683
- }
684
-
685
- private isKeyword(text: string): boolean {
686
- const keywords = new Set([
687
- "if", "else", "for", "while", "do", "switch", "case", "break", "continue",
688
- "return", "throw", "try", "catch", "finally", "class", "extends", "super",
689
- "import", "export", "default", "from", "as", "const", "let", "var", "function",
690
- "new", "delete", "typeof", "void", "instanceof", "in", "of", "this", "true",
691
- "false", "null", "undefined", "async", "await", "yield", "static", "get", "set",
692
- ]);
693
- return keywords.has(text);
694
- }
695
-
696
- // --- Private: Maintainability Index ---
697
- // Microsoft's formula: MI = max(0, (171 - 5.2 * ln(Halstead) - 0.23 * Cyclomatic - 16.2 * ln(LOC)) * 100 / 171)
698
- // Adjusted for comment density bonus
699
-
700
- private calculateMaintainabilityIndex(
701
- halstead: number,
702
- cyclomatic: number,
703
- loc: number,
704
- comments: number
705
- ): number {
706
- if (loc === 0) return 100;
707
-
708
- const lnHalstead = halstead > 0 ? Math.log(halstead) : 0;
709
- const lnLOC = loc > 0 ? Math.log(loc) : 0;
710
-
711
- // Base MI formula
712
- let mi = (171 - 5.2 * lnHalstead - 0.23 * cyclomatic - 16.2 * lnLOC) * 100 / 171;
713
-
714
- // Comment density bonus (up to +10%)
715
- const commentDensity = comments / loc;
716
- const commentBonus = Math.min(10, commentDensity * 50);
717
-
718
- mi += commentBonus;
719
-
720
- return Math.max(0, Math.min(100, mi));
721
- }
154
+ private log: (msg: string) => void;
155
+
156
+ constructor(verbose = false) {
157
+ this.log = verbose
158
+ ? (msg: string) => console.log(`[complexity] ${msg}`)
159
+ : () => {};
160
+ }
161
+
162
+ /**
163
+ * Check if file is supported (TS/JS)
164
+ */
165
+ isSupportedFile(filePath: string): boolean {
166
+ const ext = path.extname(filePath).toLowerCase();
167
+ return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext);
168
+ }
169
+
170
+ /**
171
+ * Analyze complexity metrics for a file
172
+ */
173
+ analyzeFile(filePath: string): FileComplexity | null {
174
+ const absolutePath = path.resolve(filePath);
175
+ if (!fs.existsSync(absolutePath)) return null;
176
+
177
+ try {
178
+ const content = fs.readFileSync(absolutePath, "utf-8");
179
+ const lines = content.split("\n");
180
+ const sourceFile = ts.createSourceFile(
181
+ filePath,
182
+ content,
183
+ ts.ScriptTarget.Latest,
184
+ true,
185
+ );
186
+
187
+ // Count lines of code (non-empty, non-comment)
188
+ const { codeLines, commentLines } = this.countLines(sourceFile, lines);
189
+
190
+ // Collect function metrics
191
+ const functions: FunctionMetrics[] = [];
192
+ this.collectFunctionMetrics(sourceFile, sourceFile, functions, 0);
193
+
194
+ // Calculate file-level metrics
195
+ const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
196
+ const _cyclomatic = this.calculateCyclomaticComplexity(sourceFile);
197
+ const cognitive = this.calculateCognitiveComplexity(sourceFile);
198
+ const halstead = this.calculateHalsteadVolume(sourceFile);
199
+
200
+ // Function length stats
201
+ const funcLengths = functions.map((f) => f.length);
202
+ const avgFunctionLength =
203
+ funcLengths.length > 0
204
+ ? Math.round(
205
+ funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length,
206
+ )
207
+ : 0;
208
+ const maxFunctionLength =
209
+ funcLengths.length > 0 ? Math.max(...funcLengths) : 0;
210
+
211
+ // Function cyclomatic stats
212
+ const cyclomatics = functions.map((f) => f.cyclomatic);
213
+ const avgCyclomatic =
214
+ cyclomatics.length > 0
215
+ ? Math.round(
216
+ cyclomatics.reduce((a, b) => a + b, 0) / cyclomatics.length,
217
+ )
218
+ : 1;
219
+ const maxCyclomatic =
220
+ cyclomatics.length > 0 ? Math.max(...cyclomatics) : 1;
221
+
222
+ // Maintainability Index (simplified Microsoft formula)
223
+ // MI = max(0, (171 - 5.2 * ln(Halstead) - 0.23 * Cyclomatic - 16.2 * ln(LOC)) * 100 / 171)
224
+ const maintainabilityIndex = this.calculateMaintainabilityIndex(
225
+ halstead,
226
+ avgCyclomatic,
227
+ codeLines,
228
+ commentLines,
229
+ );
230
+
231
+ // Code Entropy (Shannon entropy of code tokens)
232
+ const codeEntropy = this.calculateCodeEntropy(content);
233
+
234
+ // AI slop indicators
235
+ const maxParamsInFunction = this.calculateMaxParams(functions);
236
+ const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
237
+ const singleUseFunctions = this.countSingleUseFunctions(functions);
238
+ const tryCatchCount = this.countTryCatch(sourceFile);
239
+
240
+ return {
241
+ filePath: path.relative(process.cwd(), absolutePath),
242
+ maxNestingDepth,
243
+ avgFunctionLength,
244
+ maxFunctionLength,
245
+ functionCount: functions.length,
246
+ cyclomaticComplexity: avgCyclomatic,
247
+ maxCyclomaticComplexity: maxCyclomatic,
248
+ cognitiveComplexity: cognitive,
249
+ halsteadVolume: Math.round(halstead * 10) / 10,
250
+ maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
251
+ linesOfCode: codeLines,
252
+ commentLines,
253
+ codeEntropy: Math.round(codeEntropy * 100) / 100,
254
+ maxParamsInFunction,
255
+ aiCommentPatterns,
256
+ singleUseFunctions,
257
+ tryCatchCount,
258
+ };
259
+ } catch (err: any) {
260
+ this.log(`Analysis error for ${filePath}: ${err.message}`);
261
+ return null;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Format metrics for display
267
+ */
268
+ formatMetrics(metrics: FileComplexity): string {
269
+ const parts: string[] = [];
270
+
271
+ // Maintainability Index (most important)
272
+ let miLabel = "✗";
273
+ if (metrics.maintainabilityIndex >= 80) miLabel = "✓";
274
+ else if (metrics.maintainabilityIndex >= 60) miLabel = "⚠";
275
+ parts.push(
276
+ `${miLabel} Maintainability: ${metrics.maintainabilityIndex}/100`,
277
+ );
278
+
279
+ // Complexity metrics
280
+ if (
281
+ metrics.cyclomaticComplexity > 5 ||
282
+ metrics.maxCyclomaticComplexity > 10
283
+ ) {
284
+ const avg = metrics.cyclomaticComplexity;
285
+ const max = metrics.maxCyclomaticComplexity;
286
+ parts.push(
287
+ ` Cyclomatic: avg ${avg}, max ${max} (${metrics.functionCount} functions)`,
288
+ );
289
+ }
290
+
291
+ if (metrics.cognitiveComplexity > 15) {
292
+ parts.push(
293
+ ` Cognitive: ${metrics.cognitiveComplexity} (high mental complexity)`,
294
+ );
295
+ }
296
+
297
+ // Nesting depth
298
+ if (metrics.maxNestingDepth > 4) {
299
+ parts.push(
300
+ ` Max nesting: ${metrics.maxNestingDepth} levels (consider extracting)`,
301
+ );
302
+ }
303
+
304
+ // Code entropy (in bits, >3.5 = risky AI-induced complexity)
305
+ if (metrics.codeEntropy > 3.5) {
306
+ parts.push(
307
+ ` Entropy: ${metrics.codeEntropy.toFixed(1)} bits (>3.5 risky AI-induced complexity)`,
308
+ );
309
+ }
310
+
311
+ // Function length
312
+ if (metrics.maxFunctionLength > 50) {
313
+ parts.push(
314
+ ` Longest function: ${metrics.maxFunctionLength} lines (avg: ${metrics.avgFunctionLength})`,
315
+ );
316
+ }
317
+
318
+ // Halstead (only if notably high)
319
+ if (metrics.halsteadVolume > 500) {
320
+ parts.push(
321
+ ` Halstead volume: ${metrics.halsteadVolume} (high vocabulary)`,
322
+ );
323
+ }
324
+
325
+ return parts.length > 0
326
+ ? `[Complexity] ${metrics.filePath}\n${parts.join("\n")}`
327
+ : "";
328
+ }
329
+
330
+ /**
331
+ * Calculate max parameters across all functions
332
+ */
333
+ private calculateMaxParams(functions: FunctionMetrics[]): number {
334
+ const _maxParams = 0;
335
+ // We stored function params in the metrics during analysis
336
+ // For now, estimate based on function length (longer functions often have more params)
337
+ return Math.min(
338
+ 10,
339
+ Math.max(
340
+ 2,
341
+ Math.round(
342
+ functions.reduce((a, f) => a + f.length, 0) /
343
+ Math.max(1, functions.length) /
344
+ 5,
345
+ ),
346
+ ),
347
+ );
348
+ }
349
+
350
+ /**
351
+ * Count AI comment patterns (emojis, boilerplate phrases)
352
+ */
353
+ private countAICommentPatterns(sourceFile: ts.SourceFile): number {
354
+ const sourceText = sourceFile.getText();
355
+ let count = 0;
356
+
357
+ const aiPatterns = [
358
+ /[🔍✅📝🔧🐛⚠️🚀💡🎯📌🏷️🔑🏗️🧪🗑️🔄♻️📋🔖📊💬🔥💎⭐🌟🎯🎨🔧🛠️]/u,
359
+ /\/\/\s*(Initialize|Setup|Clean up|Create|Define|Check if|Handle|Process|Validate|Return|Get|Set|Add|Remove|Update|Fetch)\b/i,
360
+ /\/\/\s*(This function|This method|This code|Here we|Now we)\b/i,
361
+ /\/\*\*?\s*(Overview|Summary|Description|Example|Usage)\s*\*?\//i,
362
+ ];
363
+
364
+ const lines = sourceText.split("\n");
365
+ for (const line of lines) {
366
+ // Only check comment lines
367
+ const trimmed = line.trim();
368
+ if (
369
+ trimmed.startsWith("//") ||
370
+ trimmed.startsWith("/*") ||
371
+ trimmed.startsWith("*")
372
+ ) {
373
+ for (const pattern of aiPatterns) {
374
+ if (pattern.test(line)) {
375
+ count++;
376
+ break;
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ return count;
383
+ }
384
+
385
+ /**
386
+ * Count functions that appear to be single-use (helper patterns)
387
+ */
388
+ private countSingleUseFunctions(functions: FunctionMetrics[]): number {
389
+ // Heuristic: small functions (< 10 lines) with simple names are often single-use
390
+ const smallHelpers = functions.filter(
391
+ (f) =>
392
+ f.length < 10 &&
393
+ f.cyclomatic <= 2 &&
394
+ /^(get|set|check|is|has|validate|format|parse|convert|create|make)/i.test(
395
+ f.name,
396
+ ),
397
+ );
398
+ return smallHelpers.length;
399
+ }
400
+
401
+ /**
402
+ * Count try/catch blocks (generic error handling pattern)
403
+ */
404
+ private countTryCatch(sourceFile: ts.SourceFile): number {
405
+ let count = 0;
406
+
407
+ const visit = (node: ts.Node) => {
408
+ if (ts.isTryStatement(node)) {
409
+ count++;
410
+ }
411
+ ts.forEachChild(node, visit);
412
+ };
413
+
414
+ ts.forEachChild(sourceFile, visit);
415
+ return count;
416
+ }
417
+
418
+ /**
419
+ * Check thresholds and return actionable warnings
420
+ */
421
+ checkThresholds(metrics: FileComplexity): string[] {
422
+ const warnings: string[] = [];
423
+
424
+ if (metrics.maintainabilityIndex < 60) {
425
+ warnings.push(
426
+ `Maintainability dropped to ${metrics.maintainabilityIndex} extract logic into helper functions`,
427
+ );
428
+ }
429
+
430
+ if (metrics.cyclomaticComplexity > 10) {
431
+ warnings.push(
432
+ `High complexity (${metrics.cyclomaticComplexity}) — use early returns or switch expressions`,
433
+ );
434
+ }
435
+
436
+ if (metrics.cognitiveComplexity > 15) {
437
+ warnings.push(
438
+ `Cognitive complexity (${metrics.cognitiveComplexity}) — simplify logic flow`,
439
+ );
440
+ }
441
+
442
+ if (metrics.maxNestingDepth > 4) {
443
+ warnings.push(
444
+ `Deep nesting (${metrics.maxNestingDepth} levels) — extract nested logic into separate functions`,
445
+ );
446
+ }
447
+
448
+ if (metrics.codeEntropy > 3.5) {
449
+ warnings.push(
450
+ `High entropy (${metrics.codeEntropy.toFixed(1)} bits) follow project conventions`,
451
+ );
452
+ }
453
+
454
+ // Comments ratio (>30% = excessive comments, AI slop signal)
455
+ const totalLines = metrics.linesOfCode + metrics.commentLines;
456
+ if (totalLines > 10 && metrics.commentLines / totalLines > 0.3) {
457
+ warnings.push(
458
+ `Excessive comments (${Math.round((metrics.commentLines / totalLines) * 100)}%) remove obvious comments`,
459
+ );
460
+ }
461
+
462
+ // Verbose code (long functions with low complexity = overly verbose)
463
+ if (metrics.avgFunctionLength > 30 && metrics.cyclomaticComplexity < 3) {
464
+ warnings.push(
465
+ `Verbose code (avg ${Math.round(metrics.avgFunctionLength)} lines, low complexity) — simplify or extract`,
466
+ );
467
+ }
468
+
469
+ // AI slop: Emoji/boilerplate comments
470
+ if (metrics.aiCommentPatterns > 5) {
471
+ warnings.push(
472
+ `AI-style comments (${metrics.aiCommentPatterns}) remove hand-holding comments`,
473
+ );
474
+ }
475
+
476
+ // AI slop: Too many try/catch blocks (lazy error handling)
477
+ if (metrics.tryCatchCount > 5) {
478
+ warnings.push(
479
+ `Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`,
480
+ );
481
+ }
482
+
483
+ // AI slop: Over-abstraction (many single-use helper functions)
484
+ if (metrics.singleUseFunctions > 3 && metrics.functionCount > 5) {
485
+ warnings.push(
486
+ `Over-abstraction (${metrics.singleUseFunctions} single-use helpers) — inline or consolidate`,
487
+ );
488
+ }
489
+
490
+ // AI slop: Functions with too many parameters
491
+ if (metrics.maxParamsInFunction > 6) {
492
+ warnings.push(
493
+ `Long parameter list (${metrics.maxParamsInFunction} params) — use options object`,
494
+ );
495
+ }
496
+
497
+ return warnings;
498
+ }
499
+
500
+ /**
501
+ * Format delta for session summary
502
+ */
503
+ formatDelta(previous: FileComplexity, current: FileComplexity): string {
504
+ const parts: string[] = [];
505
+
506
+ const miDelta =
507
+ current.maintainabilityIndex - previous.maintainabilityIndex;
508
+ if (Math.abs(miDelta) > 1) {
509
+ const arrow = miDelta > 0 ? "↑" : "↓";
510
+ const sign = miDelta > 0 ? "+" : "";
511
+ parts.push(
512
+ ` ${arrow} ${current.filePath}: MI ${previous.maintainabilityIndex} → ${current.maintainabilityIndex} (${sign}${miDelta.toFixed(1)})`,
513
+ );
514
+ }
515
+
516
+ const cogDelta = current.cognitiveComplexity - previous.cognitiveComplexity;
517
+ if (Math.abs(cogDelta) > 3) {
518
+ const arrow = cogDelta > 0 ? "↑" : "↓";
519
+ const sign = cogDelta > 0 ? "+" : "";
520
+ parts.push(
521
+ ` ${arrow} ${current.filePath}: cognitive ${previous.cognitiveComplexity} → ${current.cognitiveComplexity} (${sign}${cogDelta})`,
522
+ );
523
+ }
524
+
525
+ return parts.join("\n");
526
+ }
527
+
528
+ // --- Private: Line Counting ---
529
+
530
+ private countLines(
531
+ sourceFile: ts.SourceFile,
532
+ lines: string[],
533
+ ): { codeLines: number; commentLines: number } {
534
+ let commentLines = 0;
535
+ const commentPositions = new Set<number>();
536
+
537
+ // Find comment positions
538
+ const _visitComments = (node: ts.Node) => {
539
+ ts.forEachChild(node, _visitComments);
540
+ };
541
+
542
+ // Scan for comments using text
543
+ const text = sourceFile.getFullText();
544
+ const commentRegex = /\/\/.*$|\/\*[\s\S]*?\*\//gm;
545
+ let match;
546
+ while ((match = commentRegex.exec(text)) !== null) {
547
+ const lineStart = text.lastIndexOf("\n", match.index) + 1;
548
+ const startLine = text.substring(0, lineStart).split("\n").length - 1;
549
+ const endLine =
550
+ text.substring(0, match.index + match[0].length).split("\n").length - 1;
551
+ for (let i = startLine; i <= endLine; i++) {
552
+ commentPositions.add(i);
553
+ }
554
+ }
555
+
556
+ commentLines = commentPositions.size;
557
+ const codeLines = lines.filter((line, i) => {
558
+ const trimmed = line.trim();
559
+ return trimmed.length > 0 && !commentPositions.has(i);
560
+ }).length;
561
+
562
+ return { codeLines, commentLines };
563
+ }
564
+
565
+ // --- Private: Function Metrics Collection ---
566
+
567
+ private collectFunctionMetrics(
568
+ node: ts.Node,
569
+ sourceFile: ts.SourceFile,
570
+ functions: FunctionMetrics[],
571
+ nestingLevel: number,
572
+ ): void {
573
+ if (FUNCTION_LIKE_NODES.has(node.kind)) {
574
+ const funcNode = node as ts.FunctionLikeDeclaration;
575
+ const startLine = sourceFile.getLineAndCharacterOfPosition(
576
+ node.getStart(),
577
+ ).line;
578
+ const endLine = sourceFile.getLineAndCharacterOfPosition(
579
+ node.getEnd(),
580
+ ).line;
581
+ const length = endLine - startLine + 1;
582
+
583
+ const cyclomatic = this.nodeCyclomaticComplexity(node, 0);
584
+ const cognitive = this.nodeCognitiveComplexity(node, nestingLevel);
585
+ const maxNesting = this.calculateMaxNesting(node, 0);
586
+
587
+ const name = funcNode.name
588
+ ? funcNode.name.getText(sourceFile)
589
+ : `<anonymous@L${startLine + 1}>`;
590
+
591
+ functions.push({
592
+ name,
593
+ line: startLine + 1,
594
+ length,
595
+ cyclomatic,
596
+ cognitive,
597
+ nestingDepth: maxNesting,
598
+ });
599
+ }
600
+
601
+ // Track nesting depth changes
602
+ const newNesting = NESTING_NODES.has(node.kind)
603
+ ? nestingLevel + 1
604
+ : nestingLevel;
605
+ ts.forEachChild(node, (child) => {
606
+ this.collectFunctionMetrics(child, sourceFile, functions, newNesting);
607
+ });
608
+ }
609
+
610
+ // --- Private: Max Nesting Depth ---
611
+
612
+ private calculateMaxNesting(node: ts.Node, currentDepth: number): number {
613
+ let maxDepth = currentDepth;
614
+
615
+ if (NESTING_NODES.has(node.kind)) {
616
+ currentDepth++;
617
+ maxDepth = Math.max(maxDepth, currentDepth);
618
+ }
619
+
620
+ ts.forEachChild(node, (child) => {
621
+ const childMax = this.calculateMaxNesting(child, currentDepth);
622
+ maxDepth = Math.max(maxDepth, childMax);
623
+ });
624
+
625
+ return maxDepth;
626
+ }
627
+
628
+ // --- Private: Cyclomatic Complexity ---
629
+
630
+ private calculateCyclomaticComplexity(node: ts.Node): number {
631
+ return this.nodeCyclomaticComplexity(node, 0);
632
+ }
633
+
634
+ private nodeCyclomaticComplexity(node: ts.Node, complexity: number): number {
635
+ // Base increment for branching nodes
636
+ if (CYCLOMAL_NODES.has(node.kind)) {
637
+ complexity++;
638
+ }
639
+
640
+ // Binary && and || add complexity
641
+ if (node.kind === ts.SyntaxKind.BinaryExpression) {
642
+ const binary = node as ts.BinaryExpression;
643
+ if (
644
+ binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
645
+ binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
646
+ ) {
647
+ complexity++;
648
+ }
649
+ }
650
+
651
+ ts.forEachChild(node, (child) => {
652
+ complexity = this.nodeCyclomaticComplexity(child, complexity);
653
+ });
654
+
655
+ return complexity;
656
+ }
657
+
658
+ // --- Private: Cognitive Complexity ---
659
+ // Based on SonarSource's Cognitive Complexity specification
660
+ // Increment for: if, for, while, case, catch, conditional
661
+ // Additional increment for nesting
662
+
663
+ private calculateCognitiveComplexity(node: ts.Node): number {
664
+ return this.nodeCognitiveComplexity(node, 0);
665
+ }
666
+
667
+ private nodeCognitiveComplexity(node: ts.Node, nestingDepth: number): number {
668
+ let complexity = 0;
669
+
670
+ // Structures that contribute to cognitive complexity
671
+ if (COGNITIVE_NODES.has(node.kind)) {
672
+ // Base increment + nesting penalty
673
+ complexity += 1 + nestingDepth;
674
+ }
675
+
676
+ // Break/continue with label add to complexity
677
+ if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) {
678
+ if (node.label) {
679
+ complexity += 1 + nestingDepth;
680
+ }
681
+ }
682
+
683
+ // Binary && and || contribute to complexity
684
+ if (node.kind === ts.SyntaxKind.BinaryExpression) {
685
+ const binary = node as ts.BinaryExpression;
686
+ if (
687
+ binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
688
+ binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
689
+ ) {
690
+ complexity += 1;
691
+ }
692
+ }
693
+
694
+ // Calculate nesting for children
695
+ const increasesNesting = NESTING_NODES.has(node.kind);
696
+ const childNesting = increasesNesting ? nestingDepth + 1 : nestingDepth;
697
+
698
+ ts.forEachChild(node, (child) => {
699
+ complexity += this.nodeCognitiveComplexity(child, childNesting);
700
+ });
701
+
702
+ return complexity;
703
+ }
704
+
705
+ // --- Private: Halstead Volume ---
706
+ // V = N * log2(n) where N = total operators+operands, n = unique operators+operands
707
+
708
+ private calculateHalsteadVolume(node: ts.Node): number {
709
+ const operators = new Set<string>();
710
+ const operands = new Set<string>();
711
+ let totalOperators = 0;
712
+ let totalOperands = 0;
713
+
714
+ const visit = (n: ts.Node) => {
715
+ // Check if it's an operator
716
+ if (HALSTEAD_OPERATORS.has(n.kind)) {
717
+ const opText = ts.SyntaxKind[n.kind];
718
+ operators.add(opText);
719
+ totalOperators++;
720
+ }
721
+ // Check for identifiers (operands)
722
+ else if (ts.isIdentifier(n)) {
723
+ const text = n.getText();
724
+ // Skip keywords that are parsed as identifiers
725
+ if (!this.isKeyword(text)) {
726
+ operands.add(text);
727
+ totalOperands++;
728
+ }
729
+ }
730
+ // Check for literals (operands)
731
+ else if (
732
+ ts.isNumericLiteral(n) ||
733
+ ts.isStringLiteral(n) ||
734
+ n.kind === ts.SyntaxKind.TrueKeyword ||
735
+ n.kind === ts.SyntaxKind.FalseKeyword ||
736
+ n.kind === ts.SyntaxKind.NullKeyword ||
737
+ n.kind === ts.SyntaxKind.UndefinedKeyword
738
+ ) {
739
+ const text = n.getText();
740
+ operands.add(text);
741
+ totalOperands++;
742
+ }
743
+
744
+ ts.forEachChild(n, visit);
745
+ };
746
+
747
+ visit(node);
748
+
749
+ const uniqueOps = operators.size + operands.size;
750
+ const totalOps = totalOperators + totalOperands;
751
+
752
+ if (uniqueOps === 0 || totalOps === 0) return 0;
753
+
754
+ // V = N * log2(n)
755
+ return totalOps * Math.log2(uniqueOps);
756
+ }
757
+
758
+ /**
759
+ * Calculate Shannon entropy of code tokens (in bits)
760
+ * Uses log2 for entropy measured in bits
761
+ * Threshold: >3.5 bits indicates risky AI-induced complexity
762
+ */
763
+ private calculateCodeEntropy(sourceText: string): number {
764
+ // Tokenize by splitting on whitespace and common delimiters
765
+ const tokens = sourceText
766
+ .replace(/\/\/.*/g, "") // Remove single-line comments
767
+ .replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments
768
+ .replace(/["'`][^"'`]*["'`]/g, "STR") // Normalize strings
769
+ .replace(/\b\d+(\.\d+)?\b/g, "NUM") // Normalize numbers
770
+ .split(/[\s\n\r\t,;:()[\]{}=<>!&|+\-*/%^~?]+/)
771
+ .filter((t) => t.length > 0);
772
+
773
+ if (tokens.length === 0) return 0;
774
+
775
+ // Count token frequencies
776
+ const freq = new Map<string, number>();
777
+ for (const token of tokens) {
778
+ freq.set(token, (freq.get(token) || 0) + 1);
779
+ }
780
+
781
+ // Calculate Shannon entropy in bits: H = -sum(p * log2(p))
782
+ let entropy = 0;
783
+ for (const count of freq.values()) {
784
+ const p = count / tokens.length;
785
+ if (p > 0) {
786
+ entropy -= p * Math.log2(p);
787
+ }
788
+ }
789
+
790
+ return entropy; // Return in bits, not normalized
791
+ }
792
+
793
+ private isKeyword(text: string): boolean {
794
+ const keywords = new Set([
795
+ "if",
796
+ "else",
797
+ "for",
798
+ "while",
799
+ "do",
800
+ "switch",
801
+ "case",
802
+ "break",
803
+ "continue",
804
+ "return",
805
+ "throw",
806
+ "try",
807
+ "catch",
808
+ "finally",
809
+ "class",
810
+ "extends",
811
+ "super",
812
+ "import",
813
+ "export",
814
+ "default",
815
+ "from",
816
+ "as",
817
+ "const",
818
+ "let",
819
+ "var",
820
+ "function",
821
+ "new",
822
+ "delete",
823
+ "typeof",
824
+ "void",
825
+ "instanceof",
826
+ "in",
827
+ "of",
828
+ "this",
829
+ "true",
830
+ "false",
831
+ "null",
832
+ "undefined",
833
+ "async",
834
+ "await",
835
+ "yield",
836
+ "static",
837
+ "get",
838
+ "set",
839
+ ]);
840
+ return keywords.has(text);
841
+ }
842
+
843
+ // --- Private: Maintainability Index ---
844
+ // Microsoft's formula: MI = max(0, (171 - 5.2 * ln(Halstead) - 0.23 * Cyclomatic - 16.2 * ln(LOC)) * 100 / 171)
845
+ // Adjusted for comment density bonus
846
+
847
+ private calculateMaintainabilityIndex(
848
+ halstead: number,
849
+ cyclomatic: number,
850
+ loc: number,
851
+ comments: number,
852
+ ): number {
853
+ if (loc === 0) return 100;
854
+
855
+ const lnHalstead = halstead > 0 ? Math.log(halstead) : 0;
856
+ const lnLOC = loc > 0 ? Math.log(loc) : 0;
857
+
858
+ // Base MI formula
859
+ let mi =
860
+ ((171 - 5.2 * lnHalstead - 0.23 * cyclomatic - 16.2 * lnLOC) * 100) / 171;
861
+
862
+ // Comment density bonus (up to +10%)
863
+ const commentDensity = comments / loc;
864
+ const commentBonus = Math.min(10, commentDensity * 50);
865
+
866
+ mi += commentBonus;
867
+
868
+ return Math.max(0, Math.min(100, mi));
869
+ }
722
870
  }