pi-lens 2.0.37 → 2.0.39
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 +16 -0
- package/README.md +1 -1
- package/clients/architect-client.js +50 -20
- package/clients/architect-client.ts +61 -22
- package/clients/ast-grep-client.js +32 -127
- package/clients/ast-grep-client.test.js +2 -14
- package/clients/ast-grep-client.test.ts +2 -16
- package/clients/ast-grep-client.ts +34 -150
- package/clients/auto-loop.js +117 -0
- package/clients/auto-loop.ts +171 -0
- package/clients/biome-client.js +25 -22
- package/clients/biome-client.ts +28 -25
- package/clients/complexity-client.js +111 -74
- package/clients/complexity-client.ts +149 -105
- package/clients/dependency-checker.js +16 -30
- package/clients/dependency-checker.ts +15 -35
- package/clients/fix-scanners.js +195 -0
- package/clients/fix-scanners.ts +297 -0
- package/clients/interviewer-templates.js +75 -0
- package/clients/interviewer-templates.ts +90 -0
- package/clients/interviewer.js +73 -101
- package/clients/interviewer.ts +195 -140
- package/clients/knip-client.js +12 -4
- package/clients/knip-client.ts +21 -16
- package/clients/metrics-history.js +215 -0
- package/clients/metrics-history.ts +300 -0
- package/clients/scan-architectural-debt.js +62 -72
- package/clients/scan-architectural-debt.ts +79 -64
- package/clients/scan-utils.js +98 -0
- package/clients/scan-utils.ts +112 -0
- package/clients/sg-runner.js +138 -0
- package/clients/sg-runner.ts +168 -0
- package/clients/subprocess-client.js +0 -37
- package/clients/subprocess-client.ts +0 -60
- package/clients/ts-service.ts +1 -7
- package/clients/type-safety-client.js +3 -8
- package/clients/type-safety-client.ts +10 -14
- package/clients/typescript-client.js +55 -56
- package/clients/typescript-client.ts +94 -52
- package/default-architect.yaml +87 -0
- package/index.ts +143 -1165
- package/package.json +2 -1
- package/rules/ast-grep-rules/rules/large-class.yml +6 -2
- package/rules/ast-grep-rules/rules/long-method.yml +1 -1
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
- package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
package/clients/biome-client.ts
CHANGED
|
@@ -92,14 +92,26 @@ export class BiomeClient {
|
|
|
92
92
|
].includes(ext);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// --- Internal helpers ---
|
|
96
|
+
|
|
95
97
|
/**
|
|
96
|
-
*
|
|
98
|
+
* Validate path and availability — returns path or null on failure
|
|
97
99
|
*/
|
|
98
|
-
|
|
99
|
-
if (!this.isAvailable()) return
|
|
100
|
+
private withValidatedPath(filePath: string): string | null {
|
|
101
|
+
if (!this.isAvailable()) return null;
|
|
100
102
|
|
|
101
103
|
const absolutePath = path.resolve(filePath);
|
|
102
|
-
if (!fs.existsSync(absolutePath)) return
|
|
104
|
+
if (!fs.existsSync(absolutePath)) return null;
|
|
105
|
+
|
|
106
|
+
return absolutePath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run biome check (format + lint) without fixing — returns diagnostics
|
|
111
|
+
*/
|
|
112
|
+
checkFile(filePath: string): BiomeDiagnostic[] {
|
|
113
|
+
const absolutePath = this.withValidatedPath(filePath);
|
|
114
|
+
if (!absolutePath) return [];
|
|
103
115
|
|
|
104
116
|
try {
|
|
105
117
|
const result = spawnSync(
|
|
@@ -137,12 +149,13 @@ export class BiomeClient {
|
|
|
137
149
|
changed: boolean;
|
|
138
150
|
error?: string;
|
|
139
151
|
} {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
const absolutePath = this.withValidatedPath(filePath);
|
|
153
|
+
if (!absolutePath)
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
changed: false,
|
|
157
|
+
error: this.isAvailable() ? "File not found" : "Biome not available",
|
|
158
|
+
};
|
|
146
159
|
|
|
147
160
|
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
148
161
|
|
|
@@ -184,21 +197,13 @@ export class BiomeClient {
|
|
|
184
197
|
fixed: number;
|
|
185
198
|
error?: string;
|
|
186
199
|
} {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
success: false,
|
|
190
|
-
changed: false,
|
|
191
|
-
fixed: 0,
|
|
192
|
-
error: "Biome not available",
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const absolutePath = path.resolve(filePath);
|
|
196
|
-
if (!fs.existsSync(absolutePath))
|
|
200
|
+
const absolutePath = this.withValidatedPath(filePath);
|
|
201
|
+
if (!absolutePath)
|
|
197
202
|
return {
|
|
198
203
|
success: false,
|
|
199
204
|
changed: false,
|
|
200
205
|
fixed: 0,
|
|
201
|
-
error: "File not found",
|
|
206
|
+
error: this.isAvailable() ? "File not found" : "Biome not available",
|
|
202
207
|
};
|
|
203
208
|
|
|
204
209
|
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
@@ -288,10 +293,8 @@ export class BiomeClient {
|
|
|
288
293
|
* Generate a diff-like summary of formatting changes
|
|
289
294
|
*/
|
|
290
295
|
getFormatDiff(filePath: string): string {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const absolutePath = path.resolve(filePath);
|
|
294
|
-
if (!fs.existsSync(absolutePath)) return "";
|
|
296
|
+
const absolutePath = this.withValidatedPath(filePath);
|
|
297
|
+
if (!absolutePath) return "";
|
|
295
298
|
|
|
296
299
|
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
297
300
|
|
|
@@ -126,70 +126,88 @@ export class ComplexityClient {
|
|
|
126
126
|
* Analyze complexity metrics for a file
|
|
127
127
|
*/
|
|
128
128
|
analyzeFile(filePath) {
|
|
129
|
-
const
|
|
130
|
-
if (!
|
|
129
|
+
const parsed = this.readAndParse(filePath);
|
|
130
|
+
if (!parsed)
|
|
131
131
|
return null;
|
|
132
132
|
try {
|
|
133
|
-
|
|
134
|
-
const lines = content.split("\n");
|
|
135
|
-
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
136
|
-
// Count lines of code (non-empty, non-comment)
|
|
137
|
-
const { codeLines, commentLines } = this.countLines(sourceFile, lines);
|
|
138
|
-
// Collect function metrics
|
|
139
|
-
const functions = [];
|
|
140
|
-
this.collectFunctionMetrics(sourceFile, sourceFile, functions, 0);
|
|
141
|
-
// Calculate file-level metrics
|
|
142
|
-
const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
|
|
143
|
-
const _cyclomatic = this.calculateCyclomaticComplexity(sourceFile);
|
|
144
|
-
const cognitive = this.calculateCognitiveComplexity(sourceFile);
|
|
145
|
-
const halstead = this.calculateHalsteadVolume(sourceFile);
|
|
146
|
-
// Function length stats
|
|
147
|
-
const funcLengths = functions.map((f) => f.length);
|
|
148
|
-
const avgFunctionLength = funcLengths.length > 0
|
|
149
|
-
? Math.round(funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length)
|
|
150
|
-
: 0;
|
|
151
|
-
const maxFunctionLength = funcLengths.length > 0 ? Math.max(...funcLengths) : 0;
|
|
152
|
-
// Function cyclomatic stats
|
|
153
|
-
const cyclomatics = functions.map((f) => f.cyclomatic);
|
|
154
|
-
const avgCyclomatic = cyclomatics.length > 0
|
|
155
|
-
? Math.round(cyclomatics.reduce((a, b) => a + b, 0) / cyclomatics.length)
|
|
156
|
-
: 1;
|
|
157
|
-
const maxCyclomatic = cyclomatics.length > 0 ? Math.max(...cyclomatics) : 1;
|
|
158
|
-
// Maintainability Index (simplified Microsoft formula)
|
|
159
|
-
// MI = max(0, (171 - 5.2 * ln(Halstead) - 0.23 * Cyclomatic - 16.2 * ln(LOC)) * 100 / 171)
|
|
160
|
-
const maintainabilityIndex = this.calculateMaintainabilityIndex(halstead, avgCyclomatic, codeLines, commentLines);
|
|
161
|
-
// Code Entropy (Shannon entropy of code tokens)
|
|
162
|
-
const codeEntropy = this.calculateCodeEntropy(content);
|
|
163
|
-
// AI slop indicators
|
|
164
|
-
const maxParamsInFunction = this.calculateMaxParams(functions);
|
|
165
|
-
const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
|
|
166
|
-
const singleUseFunctions = this.countSingleUseFunctions(functions);
|
|
167
|
-
const tryCatchCount = this.countTryCatch(sourceFile);
|
|
168
|
-
return {
|
|
169
|
-
filePath: path.relative(process.cwd(), absolutePath),
|
|
170
|
-
maxNestingDepth,
|
|
171
|
-
avgFunctionLength,
|
|
172
|
-
maxFunctionLength,
|
|
173
|
-
functionCount: functions.length,
|
|
174
|
-
cyclomaticComplexity: avgCyclomatic,
|
|
175
|
-
maxCyclomaticComplexity: maxCyclomatic,
|
|
176
|
-
cognitiveComplexity: cognitive,
|
|
177
|
-
halsteadVolume: Math.round(halstead * 10) / 10,
|
|
178
|
-
maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
|
|
179
|
-
linesOfCode: codeLines,
|
|
180
|
-
commentLines,
|
|
181
|
-
codeEntropy: Math.round(codeEntropy * 100) / 100,
|
|
182
|
-
maxParamsInFunction,
|
|
183
|
-
aiCommentPatterns,
|
|
184
|
-
singleUseFunctions,
|
|
185
|
-
tryCatchCount,
|
|
186
|
-
};
|
|
133
|
+
return this.computeMetrics(parsed);
|
|
187
134
|
}
|
|
188
135
|
catch (err) {
|
|
189
136
|
this.log(`Analysis error for ${filePath}: ${err.message}`);
|
|
190
137
|
return null;
|
|
191
138
|
}
|
|
192
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Read file and parse to TypeScript AST
|
|
142
|
+
*/
|
|
143
|
+
readAndParse(filePath) {
|
|
144
|
+
const absolutePath = path.resolve(filePath);
|
|
145
|
+
if (!fs.existsSync(absolutePath))
|
|
146
|
+
return null;
|
|
147
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
148
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
149
|
+
return { absolutePath, content, sourceFile };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Compute all metrics from parsed source
|
|
153
|
+
*/
|
|
154
|
+
computeMetrics(parsed) {
|
|
155
|
+
const { absolutePath, content, sourceFile } = parsed;
|
|
156
|
+
const lines = content.split("\n");
|
|
157
|
+
// Line counts and function collection
|
|
158
|
+
const { codeLines, commentLines } = this.countLines(sourceFile, lines);
|
|
159
|
+
const functions = this.collectFunctionMetrics(sourceFile);
|
|
160
|
+
// File-level complexity metrics
|
|
161
|
+
const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
|
|
162
|
+
const cognitive = this.calculateCognitiveComplexity(sourceFile);
|
|
163
|
+
const halstead = this.calculateHalsteadVolume(sourceFile);
|
|
164
|
+
// Aggregate function statistics
|
|
165
|
+
const funcStats = this.aggregateFunctionStats(functions);
|
|
166
|
+
// Derived metrics
|
|
167
|
+
const maintainabilityIndex = this.calculateMaintainabilityIndex(halstead, funcStats.avgCyclomatic, codeLines, commentLines);
|
|
168
|
+
const codeEntropy = this.calculateCodeEntropy(content);
|
|
169
|
+
// AI slop indicators
|
|
170
|
+
const maxParamsInFunction = this.calculateMaxParams(functions);
|
|
171
|
+
const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
|
|
172
|
+
const singleUseFunctions = this.countSingleUseFunctions(functions);
|
|
173
|
+
const tryCatchCount = this.countTryCatch(sourceFile);
|
|
174
|
+
return {
|
|
175
|
+
filePath: path.relative(process.cwd(), absolutePath),
|
|
176
|
+
maxNestingDepth,
|
|
177
|
+
avgFunctionLength: funcStats.avgLength,
|
|
178
|
+
maxFunctionLength: funcStats.maxLength,
|
|
179
|
+
functionCount: functions.length,
|
|
180
|
+
cyclomaticComplexity: funcStats.avgCyclomatic,
|
|
181
|
+
maxCyclomaticComplexity: funcStats.maxCyclomatic,
|
|
182
|
+
cognitiveComplexity: cognitive,
|
|
183
|
+
halsteadVolume: Math.round(halstead * 10) / 10,
|
|
184
|
+
maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
|
|
185
|
+
linesOfCode: codeLines,
|
|
186
|
+
commentLines,
|
|
187
|
+
codeEntropy: Math.round(codeEntropy * 100) / 100,
|
|
188
|
+
maxParamsInFunction,
|
|
189
|
+
aiCommentPatterns,
|
|
190
|
+
singleUseFunctions,
|
|
191
|
+
tryCatchCount,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Aggregate function metrics into summary statistics
|
|
196
|
+
*/
|
|
197
|
+
aggregateFunctionStats(functions) {
|
|
198
|
+
if (functions.length === 0) {
|
|
199
|
+
return { avgLength: 0, maxLength: 0, avgCyclomatic: 1, maxCyclomatic: 1 };
|
|
200
|
+
}
|
|
201
|
+
const lengths = functions.map((f) => f.length);
|
|
202
|
+
const cyclomatics = functions.map((f) => f.cyclomatic);
|
|
203
|
+
const sum = (arr) => arr.reduce((a, b) => a + b, 0);
|
|
204
|
+
return {
|
|
205
|
+
avgLength: Math.round(sum(lengths) / lengths.length),
|
|
206
|
+
maxLength: Math.max(...lengths),
|
|
207
|
+
avgCyclomatic: Math.max(1, Math.round(sum(cyclomatics) / cyclomatics.length)),
|
|
208
|
+
maxCyclomatic: Math.max(1, Math.max(...cyclomatics)),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
193
211
|
/**
|
|
194
212
|
* Format metrics for display
|
|
195
213
|
*/
|
|
@@ -316,9 +334,9 @@ export class ComplexityClient {
|
|
|
316
334
|
if (metrics.codeEntropy > 3.5) {
|
|
317
335
|
warnings.push(`High entropy (${metrics.codeEntropy.toFixed(1)} bits) — follow project conventions`);
|
|
318
336
|
}
|
|
319
|
-
// Comments ratio (>
|
|
337
|
+
// Comments ratio (>40% = excessive comments, AI slop signal)
|
|
320
338
|
const totalLines = metrics.linesOfCode + metrics.commentLines;
|
|
321
|
-
if (totalLines > 10 && metrics.commentLines / totalLines > 0.
|
|
339
|
+
if (totalLines > 10 && metrics.commentLines / totalLines > 0.4) {
|
|
322
340
|
warnings.push(`Excessive comments (${Math.round((metrics.commentLines / totalLines) * 100)}%) — remove obvious comments`);
|
|
323
341
|
}
|
|
324
342
|
// Verbose code (long functions with low complexity = overly verbose)
|
|
@@ -330,7 +348,7 @@ export class ComplexityClient {
|
|
|
330
348
|
warnings.push(`AI-style comments (${metrics.aiCommentPatterns}) — remove hand-holding comments`);
|
|
331
349
|
}
|
|
332
350
|
// AI slop: Too many try/catch blocks (lazy error handling)
|
|
333
|
-
if (metrics.tryCatchCount >
|
|
351
|
+
if (metrics.tryCatchCount > 15) {
|
|
334
352
|
warnings.push(`Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`);
|
|
335
353
|
}
|
|
336
354
|
// AI slop: Over-abstraction (many single-use helper functions)
|
|
@@ -385,12 +403,31 @@ export class ComplexityClient {
|
|
|
385
403
|
commentLines = commentPositions.size;
|
|
386
404
|
const codeLines = lines.filter((line, i) => {
|
|
387
405
|
const trimmed = line.trim();
|
|
388
|
-
|
|
406
|
+
if (trimmed.length === 0)
|
|
407
|
+
return false;
|
|
408
|
+
// If the line is not in commentPositions, it definitely has code
|
|
409
|
+
if (!commentPositions.has(i))
|
|
410
|
+
return true;
|
|
411
|
+
// If it IS in commentPositions, it might still have code (trailing comment)
|
|
412
|
+
// Remove the comment part and check if anything remains
|
|
413
|
+
const lineWithoutComments = line
|
|
414
|
+
.replace(/\/\/.*$/, "")
|
|
415
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
416
|
+
.trim();
|
|
417
|
+
return lineWithoutComments.length > 0;
|
|
389
418
|
}).length;
|
|
390
419
|
return { codeLines, commentLines };
|
|
391
420
|
}
|
|
392
421
|
// --- Private: Function Metrics Collection ---
|
|
393
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Collect metrics for all functions in the source file
|
|
424
|
+
*/
|
|
425
|
+
collectFunctionMetrics(sourceFile) {
|
|
426
|
+
const functions = [];
|
|
427
|
+
this.visitFunctionMetrics(sourceFile, sourceFile, functions, 0);
|
|
428
|
+
return functions;
|
|
429
|
+
}
|
|
430
|
+
visitFunctionMetrics(node, sourceFile, functions, nestingLevel) {
|
|
394
431
|
if (FUNCTION_LIKE_NODES.has(node.kind)) {
|
|
395
432
|
const funcNode = node;
|
|
396
433
|
const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line;
|
|
@@ -416,7 +453,7 @@ export class ComplexityClient {
|
|
|
416
453
|
? nestingLevel + 1
|
|
417
454
|
: nestingLevel;
|
|
418
455
|
ts.forEachChild(node, (child) => {
|
|
419
|
-
this.
|
|
456
|
+
this.visitFunctionMetrics(child, sourceFile, functions, newNesting);
|
|
420
457
|
});
|
|
421
458
|
}
|
|
422
459
|
// --- Private: Max Nesting Depth ---
|
|
@@ -436,18 +473,22 @@ export class ComplexityClient {
|
|
|
436
473
|
calculateCyclomaticComplexity(node) {
|
|
437
474
|
return this.nodeCyclomaticComplexity(node, 0);
|
|
438
475
|
}
|
|
476
|
+
isLogicalOperator(node) {
|
|
477
|
+
if (node.kind === ts.SyntaxKind.BinaryExpression) {
|
|
478
|
+
const binary = node;
|
|
479
|
+
return (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
480
|
+
binary.operatorToken.kind === ts.SyntaxKind.BarBarToken);
|
|
481
|
+
}
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
439
484
|
nodeCyclomaticComplexity(node, complexity) {
|
|
440
485
|
// Base increment for branching nodes
|
|
441
486
|
if (CYCLOMAL_NODES.has(node.kind)) {
|
|
442
487
|
complexity++;
|
|
443
488
|
}
|
|
444
489
|
// Binary && and || add complexity
|
|
445
|
-
if (node
|
|
446
|
-
|
|
447
|
-
if (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
448
|
-
binary.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
449
|
-
complexity++;
|
|
450
|
-
}
|
|
490
|
+
if (this.isLogicalOperator(node)) {
|
|
491
|
+
complexity++;
|
|
451
492
|
}
|
|
452
493
|
ts.forEachChild(node, (child) => {
|
|
453
494
|
complexity = this.nodeCyclomaticComplexity(child, complexity);
|
|
@@ -475,12 +516,8 @@ export class ComplexityClient {
|
|
|
475
516
|
}
|
|
476
517
|
}
|
|
477
518
|
// Binary && and || contribute to complexity
|
|
478
|
-
if (node
|
|
479
|
-
|
|
480
|
-
if (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
481
|
-
binary.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
482
|
-
complexity += 1;
|
|
483
|
-
}
|
|
519
|
+
if (this.isLogicalOperator(node)) {
|
|
520
|
+
complexity += 1;
|
|
484
521
|
}
|
|
485
522
|
// Calculate nesting for children
|
|
486
523
|
const increasesNesting = NESTING_NODES.has(node.kind);
|
|
@@ -171,97 +171,122 @@ export class ComplexityClient {
|
|
|
171
171
|
* Analyze complexity metrics for a file
|
|
172
172
|
*/
|
|
173
173
|
analyzeFile(filePath: string): FileComplexity | null {
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
174
|
+
const parsed = this.readAndParse(filePath);
|
|
175
|
+
if (!parsed) return null;
|
|
176
176
|
|
|
177
177
|
try {
|
|
178
|
-
|
|
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
|
-
};
|
|
178
|
+
return this.computeMetrics(parsed);
|
|
259
179
|
} catch (err: any) {
|
|
260
180
|
this.log(`Analysis error for ${filePath}: ${err.message}`);
|
|
261
181
|
return null;
|
|
262
182
|
}
|
|
263
183
|
}
|
|
264
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Read file and parse to TypeScript AST
|
|
187
|
+
*/
|
|
188
|
+
private readAndParse(
|
|
189
|
+
filePath: string,
|
|
190
|
+
): { absolutePath: string; content: string; sourceFile: ts.SourceFile } | null {
|
|
191
|
+
const absolutePath = path.resolve(filePath);
|
|
192
|
+
if (!fs.existsSync(absolutePath)) return null;
|
|
193
|
+
|
|
194
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
195
|
+
const sourceFile = ts.createSourceFile(
|
|
196
|
+
filePath,
|
|
197
|
+
content,
|
|
198
|
+
ts.ScriptTarget.Latest,
|
|
199
|
+
true,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return { absolutePath, content, sourceFile };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Compute all metrics from parsed source
|
|
207
|
+
*/
|
|
208
|
+
private computeMetrics(parsed: {
|
|
209
|
+
absolutePath: string;
|
|
210
|
+
content: string;
|
|
211
|
+
sourceFile: ts.SourceFile;
|
|
212
|
+
}): FileComplexity {
|
|
213
|
+
const { absolutePath, content, sourceFile } = parsed;
|
|
214
|
+
const lines = content.split("\n");
|
|
215
|
+
|
|
216
|
+
// Line counts and function collection
|
|
217
|
+
const { codeLines, commentLines } = this.countLines(sourceFile, lines);
|
|
218
|
+
const functions = this.collectFunctionMetrics(sourceFile);
|
|
219
|
+
|
|
220
|
+
// File-level complexity metrics
|
|
221
|
+
const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
|
|
222
|
+
const cognitive = this.calculateCognitiveComplexity(sourceFile);
|
|
223
|
+
const halstead = this.calculateHalsteadVolume(sourceFile);
|
|
224
|
+
|
|
225
|
+
// Aggregate function statistics
|
|
226
|
+
const funcStats = this.aggregateFunctionStats(functions);
|
|
227
|
+
|
|
228
|
+
// Derived metrics
|
|
229
|
+
const maintainabilityIndex = this.calculateMaintainabilityIndex(
|
|
230
|
+
halstead,
|
|
231
|
+
funcStats.avgCyclomatic,
|
|
232
|
+
codeLines,
|
|
233
|
+
commentLines,
|
|
234
|
+
);
|
|
235
|
+
const codeEntropy = this.calculateCodeEntropy(content);
|
|
236
|
+
|
|
237
|
+
// AI slop indicators
|
|
238
|
+
const maxParamsInFunction = this.calculateMaxParams(functions);
|
|
239
|
+
const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
|
|
240
|
+
const singleUseFunctions = this.countSingleUseFunctions(functions);
|
|
241
|
+
const tryCatchCount = this.countTryCatch(sourceFile);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
filePath: path.relative(process.cwd(), absolutePath),
|
|
245
|
+
maxNestingDepth,
|
|
246
|
+
avgFunctionLength: funcStats.avgLength,
|
|
247
|
+
maxFunctionLength: funcStats.maxLength,
|
|
248
|
+
functionCount: functions.length,
|
|
249
|
+
cyclomaticComplexity: funcStats.avgCyclomatic,
|
|
250
|
+
maxCyclomaticComplexity: funcStats.maxCyclomatic,
|
|
251
|
+
cognitiveComplexity: cognitive,
|
|
252
|
+
halsteadVolume: Math.round(halstead * 10) / 10,
|
|
253
|
+
maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
|
|
254
|
+
linesOfCode: codeLines,
|
|
255
|
+
commentLines,
|
|
256
|
+
codeEntropy: Math.round(codeEntropy * 100) / 100,
|
|
257
|
+
maxParamsInFunction,
|
|
258
|
+
aiCommentPatterns,
|
|
259
|
+
singleUseFunctions,
|
|
260
|
+
tryCatchCount,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Aggregate function metrics into summary statistics
|
|
266
|
+
*/
|
|
267
|
+
private aggregateFunctionStats(functions: FunctionMetrics[]): {
|
|
268
|
+
avgLength: number;
|
|
269
|
+
maxLength: number;
|
|
270
|
+
avgCyclomatic: number;
|
|
271
|
+
maxCyclomatic: number;
|
|
272
|
+
} {
|
|
273
|
+
if (functions.length === 0) {
|
|
274
|
+
return { avgLength: 0, maxLength: 0, avgCyclomatic: 1, maxCyclomatic: 1 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const lengths = functions.map((f) => f.length);
|
|
278
|
+
const cyclomatics = functions.map((f) => f.cyclomatic);
|
|
279
|
+
|
|
280
|
+
const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
avgLength: Math.round(sum(lengths) / lengths.length),
|
|
284
|
+
maxLength: Math.max(...lengths),
|
|
285
|
+
avgCyclomatic: Math.max(1, Math.round(sum(cyclomatics) / cyclomatics.length)),
|
|
286
|
+
maxCyclomatic: Math.max(1, Math.max(...cyclomatics)),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
265
290
|
/**
|
|
266
291
|
* Format metrics for display
|
|
267
292
|
*/
|
|
@@ -451,9 +476,9 @@ export class ComplexityClient {
|
|
|
451
476
|
);
|
|
452
477
|
}
|
|
453
478
|
|
|
454
|
-
// Comments ratio (>
|
|
479
|
+
// Comments ratio (>40% = excessive comments, AI slop signal)
|
|
455
480
|
const totalLines = metrics.linesOfCode + metrics.commentLines;
|
|
456
|
-
if (totalLines > 10 && metrics.commentLines / totalLines > 0.
|
|
481
|
+
if (totalLines > 10 && metrics.commentLines / totalLines > 0.4) {
|
|
457
482
|
warnings.push(
|
|
458
483
|
`Excessive comments (${Math.round((metrics.commentLines / totalLines) * 100)}%) — remove obvious comments`,
|
|
459
484
|
);
|
|
@@ -474,7 +499,7 @@ export class ComplexityClient {
|
|
|
474
499
|
}
|
|
475
500
|
|
|
476
501
|
// AI slop: Too many try/catch blocks (lazy error handling)
|
|
477
|
-
if (metrics.tryCatchCount >
|
|
502
|
+
if (metrics.tryCatchCount > 15) {
|
|
478
503
|
warnings.push(
|
|
479
504
|
`Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`,
|
|
480
505
|
);
|
|
@@ -556,7 +581,18 @@ export class ComplexityClient {
|
|
|
556
581
|
commentLines = commentPositions.size;
|
|
557
582
|
const codeLines = lines.filter((line, i) => {
|
|
558
583
|
const trimmed = line.trim();
|
|
559
|
-
|
|
584
|
+
if (trimmed.length === 0) return false;
|
|
585
|
+
|
|
586
|
+
// If the line is not in commentPositions, it definitely has code
|
|
587
|
+
if (!commentPositions.has(i)) return true;
|
|
588
|
+
|
|
589
|
+
// If it IS in commentPositions, it might still have code (trailing comment)
|
|
590
|
+
// Remove the comment part and check if anything remains
|
|
591
|
+
const lineWithoutComments = line
|
|
592
|
+
.replace(/\/\/.*$/, "")
|
|
593
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
594
|
+
.trim();
|
|
595
|
+
return lineWithoutComments.length > 0;
|
|
560
596
|
}).length;
|
|
561
597
|
|
|
562
598
|
return { codeLines, commentLines };
|
|
@@ -564,7 +600,16 @@ export class ComplexityClient {
|
|
|
564
600
|
|
|
565
601
|
// --- Private: Function Metrics Collection ---
|
|
566
602
|
|
|
567
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Collect metrics for all functions in the source file
|
|
605
|
+
*/
|
|
606
|
+
private collectFunctionMetrics(sourceFile: ts.SourceFile): FunctionMetrics[] {
|
|
607
|
+
const functions: FunctionMetrics[] = [];
|
|
608
|
+
this.visitFunctionMetrics(sourceFile, sourceFile, functions, 0);
|
|
609
|
+
return functions;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private visitFunctionMetrics(
|
|
568
613
|
node: ts.Node,
|
|
569
614
|
sourceFile: ts.SourceFile,
|
|
570
615
|
functions: FunctionMetrics[],
|
|
@@ -603,7 +648,7 @@ export class ComplexityClient {
|
|
|
603
648
|
? nestingLevel + 1
|
|
604
649
|
: nestingLevel;
|
|
605
650
|
ts.forEachChild(node, (child) => {
|
|
606
|
-
this.
|
|
651
|
+
this.visitFunctionMetrics(child, sourceFile, functions, newNesting);
|
|
607
652
|
});
|
|
608
653
|
}
|
|
609
654
|
|
|
@@ -631,6 +676,17 @@ export class ComplexityClient {
|
|
|
631
676
|
return this.nodeCyclomaticComplexity(node, 0);
|
|
632
677
|
}
|
|
633
678
|
|
|
679
|
+
private isLogicalOperator(node: ts.Node): boolean {
|
|
680
|
+
if (node.kind === ts.SyntaxKind.BinaryExpression) {
|
|
681
|
+
const binary = node as ts.BinaryExpression;
|
|
682
|
+
return (
|
|
683
|
+
binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
684
|
+
binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
634
690
|
private nodeCyclomaticComplexity(node: ts.Node, complexity: number): number {
|
|
635
691
|
// Base increment for branching nodes
|
|
636
692
|
if (CYCLOMAL_NODES.has(node.kind)) {
|
|
@@ -638,14 +694,8 @@ export class ComplexityClient {
|
|
|
638
694
|
}
|
|
639
695
|
|
|
640
696
|
// Binary && and || add complexity
|
|
641
|
-
if (node
|
|
642
|
-
|
|
643
|
-
if (
|
|
644
|
-
binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
645
|
-
binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
|
|
646
|
-
) {
|
|
647
|
-
complexity++;
|
|
648
|
-
}
|
|
697
|
+
if (this.isLogicalOperator(node)) {
|
|
698
|
+
complexity++;
|
|
649
699
|
}
|
|
650
700
|
|
|
651
701
|
ts.forEachChild(node, (child) => {
|
|
@@ -681,14 +731,8 @@ export class ComplexityClient {
|
|
|
681
731
|
}
|
|
682
732
|
|
|
683
733
|
// Binary && and || contribute to complexity
|
|
684
|
-
if (node
|
|
685
|
-
|
|
686
|
-
if (
|
|
687
|
-
binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
688
|
-
binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
|
|
689
|
-
) {
|
|
690
|
-
complexity += 1;
|
|
691
|
-
}
|
|
734
|
+
if (this.isLogicalOperator(node)) {
|
|
735
|
+
complexity += 1;
|
|
692
736
|
}
|
|
693
737
|
|
|
694
738
|
// Calculate nesting for children
|