ts-analyzer 1.2.0 → 1.3.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/.github/workflows/release.yml +44 -0
- package/README.md +18 -330
- package/bin/cli.ts +30 -2
- package/dist/bin/cli.js +28 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/code-complexity.d.ts +12 -0
- package/dist/src/code-complexity.js +84 -9
- package/dist/src/code-complexity.js.map +1 -1
- package/dist/src/html-formatter.d.ts +2 -0
- package/dist/src/html-formatter.js +156 -0
- package/dist/src/html-formatter.js.map +1 -0
- package/dist/src/typescript-safety.js.map +1 -1
- package/package.json +8 -4
- package/src/code-complexity.ts +108 -10
- package/src/html-formatter.ts +159 -0
- package/src/typescript-safety.ts +3 -1
- package/test/complexity.test.ts +76 -0
- package/test/html-formatter.test.ts +62 -0
- package/test/line-counting.test.ts +45 -0
- package/test/ts-safety.test.ts +50 -0
- package/vitest.config.ts +9 -0
- package/ts-analyzer.config.json +0 -3
package/src/code-complexity.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface FileComplexityMetrics {
|
|
|
40
40
|
functionCount: number;
|
|
41
41
|
complexityScore: number;
|
|
42
42
|
complexityRating: string;
|
|
43
|
+
codeSmells: {
|
|
44
|
+
magicNumbers: number;
|
|
45
|
+
callbackHell: number;
|
|
46
|
+
godFiles: number; // For God Classes/Files
|
|
47
|
+
excessiveParameters: number;
|
|
48
|
+
};
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
export interface CodeComplexityMetrics {
|
|
@@ -52,6 +58,12 @@ export interface CodeComplexityMetrics {
|
|
|
52
58
|
totalFunctions: number;
|
|
53
59
|
complexFiles: number;
|
|
54
60
|
overallComplexity: string;
|
|
61
|
+
codeSmells: {
|
|
62
|
+
magicNumbers: number;
|
|
63
|
+
callbackHell: number;
|
|
64
|
+
godFiles: number;
|
|
65
|
+
excessiveParameters: number;
|
|
66
|
+
};
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
// Calculate cyclomatic complexity
|
|
@@ -171,8 +183,8 @@ function analyzeFunctions(ast: any): FunctionData[] {
|
|
|
171
183
|
functions.push({
|
|
172
184
|
type: node.type,
|
|
173
185
|
name: node.id ? node.id.name : 'anonymous',
|
|
174
|
-
complexity,
|
|
175
|
-
nestingDepth,
|
|
186
|
+
complexity: node.complexity || complexity,
|
|
187
|
+
nestingDepth: node.nestingDepth || nestingDepth,
|
|
176
188
|
paramCount,
|
|
177
189
|
lineCount
|
|
178
190
|
});
|
|
@@ -202,6 +214,13 @@ function parseTypeScriptFile(content: string): SimplifiedProgram {
|
|
|
202
214
|
|
|
203
215
|
// Function to visit TypeScript nodes and extract function information
|
|
204
216
|
function visit(node: ts.Node) {
|
|
217
|
+
if (ts.isLiteralExpression(node) && node.kind === ts.SyntaxKind.NumericLiteral) {
|
|
218
|
+
simplifiedAst.body.push({
|
|
219
|
+
type: 'Literal',
|
|
220
|
+
value: parseFloat(node.text)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
if (ts.isFunctionDeclaration(node) ||
|
|
206
225
|
ts.isMethodDeclaration(node) ||
|
|
207
226
|
ts.isArrowFunction(node) ||
|
|
@@ -210,7 +229,47 @@ function parseTypeScriptFile(content: string): SimplifiedProgram {
|
|
|
210
229
|
const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
211
230
|
const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
212
231
|
|
|
213
|
-
|
|
232
|
+
// Calculate complexity and nesting for this TS function
|
|
233
|
+
let complexity = 1;
|
|
234
|
+
let maxNesting = 0;
|
|
235
|
+
let currentNesting = 0;
|
|
236
|
+
|
|
237
|
+
function checkNode(n: ts.Node) {
|
|
238
|
+
// Complexity
|
|
239
|
+
if (ts.isIfStatement(n) || ts.isConditionalExpression(n) || ts.isCaseClause(n) ||
|
|
240
|
+
ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
|
|
241
|
+
ts.isWhileStatement(n) || ts.isDoStatement(n)) {
|
|
242
|
+
complexity++;
|
|
243
|
+
}
|
|
244
|
+
if (ts.isBinaryExpression(n)) {
|
|
245
|
+
if (n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
246
|
+
n.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
247
|
+
complexity++;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Nesting
|
|
252
|
+
const isNestingNode = ts.isBlock(n) || ts.isIfStatement(n) || ts.isSwitchStatement(n) ||
|
|
253
|
+
ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
|
|
254
|
+
ts.isWhileStatement(n) || ts.isDoStatement(n) || ts.isTryStatement(n);
|
|
255
|
+
|
|
256
|
+
if (isNestingNode) {
|
|
257
|
+
currentNesting++;
|
|
258
|
+
maxNesting = Math.max(maxNesting, currentNesting);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ts.forEachChild(n, checkNode);
|
|
262
|
+
|
|
263
|
+
if (isNestingNode) {
|
|
264
|
+
currentNesting--;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (node.body) {
|
|
269
|
+
checkNode(node.body);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const functionNode: any = {
|
|
214
273
|
type: ts.isFunctionDeclaration(node) ? 'FunctionDeclaration' :
|
|
215
274
|
ts.isArrowFunction(node) ? 'ArrowFunctionExpression' : 'FunctionExpression',
|
|
216
275
|
id: ts.isFunctionDeclaration(node) && node.name ? { name: node.name.text } : undefined,
|
|
@@ -219,10 +278,13 @@ function parseTypeScriptFile(content: string): SimplifiedProgram {
|
|
|
219
278
|
loc: {
|
|
220
279
|
start: { line: startLine },
|
|
221
280
|
end: { line: endLine }
|
|
222
|
-
}
|
|
281
|
+
},
|
|
282
|
+
complexity,
|
|
283
|
+
nestingDepth: maxNesting
|
|
223
284
|
};
|
|
224
285
|
|
|
225
286
|
simplifiedAst.body.push(functionNode);
|
|
287
|
+
return; // Don't recurse into function body for the main visit (already handled by checkNode)
|
|
226
288
|
}
|
|
227
289
|
|
|
228
290
|
ts.forEachChild(node, visit);
|
|
@@ -271,7 +333,8 @@ export async function analyzeFileComplexity(filePath: string): Promise<FileCompl
|
|
|
271
333
|
avgParams: '0',
|
|
272
334
|
functionCount: 0,
|
|
273
335
|
complexityScore: 0,
|
|
274
|
-
complexityRating: 'N/A'
|
|
336
|
+
complexityRating: 'N/A',
|
|
337
|
+
codeSmells: { magicNumbers: 0, callbackHell: 0, godFiles: 0, excessiveParameters: 0 }
|
|
275
338
|
};
|
|
276
339
|
}
|
|
277
340
|
}
|
|
@@ -279,6 +342,29 @@ export async function analyzeFileComplexity(filePath: string): Promise<FileCompl
|
|
|
279
342
|
// Analyze the functions in the file
|
|
280
343
|
const functions = analyzeFunctions(ast);
|
|
281
344
|
|
|
345
|
+
let magicNumbers = 0;
|
|
346
|
+
estraverse.traverse(ast, {
|
|
347
|
+
enter(node: any) {
|
|
348
|
+
if (node.type === 'Literal' && typeof node.value === 'number') {
|
|
349
|
+
// A magic number could be considered > 10
|
|
350
|
+
if (node.value > 10 || node.value < -10) {
|
|
351
|
+
magicNumbers++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Check for god file (> 500 lines)
|
|
358
|
+
const lineCount = content.split('\n').length;
|
|
359
|
+
const godFiles = lineCount > 500 ? 1 : 0;
|
|
360
|
+
|
|
361
|
+
let callbackHell = 0;
|
|
362
|
+
let excessiveParameters = 0;
|
|
363
|
+
functions.forEach(fn => {
|
|
364
|
+
if (fn.nestingDepth > 3) callbackHell++;
|
|
365
|
+
if (fn.paramCount > 4) excessiveParameters++;
|
|
366
|
+
});
|
|
367
|
+
|
|
282
368
|
if (functions.length === 0) {
|
|
283
369
|
return {
|
|
284
370
|
avgComplexity: '0',
|
|
@@ -289,7 +375,8 @@ export async function analyzeFileComplexity(filePath: string): Promise<FileCompl
|
|
|
289
375
|
avgParams: '0',
|
|
290
376
|
functionCount: 0,
|
|
291
377
|
complexityScore: 0,
|
|
292
|
-
complexityRating: 'N/A'
|
|
378
|
+
complexityRating: 'N/A',
|
|
379
|
+
codeSmells: { magicNumbers, callbackHell, godFiles, excessiveParameters }
|
|
293
380
|
};
|
|
294
381
|
}
|
|
295
382
|
|
|
@@ -340,7 +427,8 @@ export async function analyzeFileComplexity(filePath: string): Promise<FileCompl
|
|
|
340
427
|
avgParams: avgParams.toFixed(1),
|
|
341
428
|
functionCount: functions.length,
|
|
342
429
|
complexityScore: Math.round(totalScore),
|
|
343
|
-
complexityRating
|
|
430
|
+
complexityRating,
|
|
431
|
+
codeSmells: { magicNumbers, callbackHell, godFiles, excessiveParameters }
|
|
344
432
|
};
|
|
345
433
|
} catch (error) {
|
|
346
434
|
console.error(`Error analyzing file complexity for ${filePath}:`, error);
|
|
@@ -353,7 +441,8 @@ export async function analyzeFileComplexity(filePath: string): Promise<FileCompl
|
|
|
353
441
|
avgParams: '0',
|
|
354
442
|
functionCount: 0,
|
|
355
443
|
complexityScore: 0,
|
|
356
|
-
complexityRating: 'N/A'
|
|
444
|
+
complexityRating: 'N/A',
|
|
445
|
+
codeSmells: { magicNumbers: 0, callbackHell: 0, godFiles: 0, excessiveParameters: 0 }
|
|
357
446
|
};
|
|
358
447
|
}
|
|
359
448
|
}
|
|
@@ -372,7 +461,8 @@ export async function calculateProjectComplexity(projectPath: string, fileList:
|
|
|
372
461
|
avgFunctionSize: '0.0',
|
|
373
462
|
totalFunctions: 0,
|
|
374
463
|
complexFiles: 0,
|
|
375
|
-
overallComplexity: 'N/A'
|
|
464
|
+
overallComplexity: 'N/A',
|
|
465
|
+
codeSmells: { magicNumbers: 0, callbackHell: 0, godFiles: 0, excessiveParameters: 0 }
|
|
376
466
|
};
|
|
377
467
|
}
|
|
378
468
|
|
|
@@ -384,6 +474,8 @@ export async function calculateProjectComplexity(projectPath: string, fileList:
|
|
|
384
474
|
let totalFunctions = 0;
|
|
385
475
|
let complexFiles = 0;
|
|
386
476
|
|
|
477
|
+
let totalSmells = { magicNumbers: 0, callbackHell: 0, godFiles: 0, excessiveParameters: 0 };
|
|
478
|
+
|
|
387
479
|
for (const file of jsFiles) {
|
|
388
480
|
const filePath = path.join(projectPath, file);
|
|
389
481
|
const metrics = await analyzeFileComplexity(filePath);
|
|
@@ -395,6 +487,11 @@ export async function calculateProjectComplexity(projectPath: string, fileList:
|
|
|
395
487
|
maxNestingDepthGlobal = Math.max(maxNestingDepthGlobal, metrics.maxNestingDepth);
|
|
396
488
|
totalFunctions += metrics.functionCount;
|
|
397
489
|
|
|
490
|
+
totalSmells.magicNumbers += metrics.codeSmells.magicNumbers;
|
|
491
|
+
totalSmells.callbackHell += metrics.codeSmells.callbackHell;
|
|
492
|
+
totalSmells.godFiles += metrics.codeSmells.godFiles;
|
|
493
|
+
totalSmells.excessiveParameters += metrics.codeSmells.excessiveParameters;
|
|
494
|
+
|
|
398
495
|
if (metrics.complexityRating === 'High') {
|
|
399
496
|
complexFiles++;
|
|
400
497
|
}
|
|
@@ -426,6 +523,7 @@ export async function calculateProjectComplexity(projectPath: string, fileList:
|
|
|
426
523
|
avgFunctionSize,
|
|
427
524
|
totalFunctions,
|
|
428
525
|
complexFiles,
|
|
429
|
-
overallComplexity
|
|
526
|
+
overallComplexity,
|
|
527
|
+
codeSmells: totalSmells
|
|
430
528
|
};
|
|
431
529
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { ProjectStats } from './index.js';
|
|
2
|
+
import process from 'process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export function generateHtmlReport(stats: ProjectStats): string {
|
|
6
|
+
return `
|
|
7
|
+
<!DOCTYPE html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="UTF-8">
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
12
|
+
<title>ts-analyzer Report</title>
|
|
13
|
+
<style>
|
|
14
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; background-color: #f8f9fa; color: #212529; }
|
|
15
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border: 1px solid #e9ecef; }
|
|
16
|
+
h1, h2, h3 { color: #343a40; }
|
|
17
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
18
|
+
.card { background: #fff; padding: 20px; border-radius: 6px; border: 1px solid #dee2e6; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
|
19
|
+
.card h3 { margin-top: 0; margin-bottom: 10px; font-size: 15px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
20
|
+
.card .value { font-size: 32px; font-weight: bold; color: #007bff; }
|
|
21
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
|
|
22
|
+
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #dee2e6; }
|
|
23
|
+
th { background-color: #f8f9fa; font-weight: 600; color: #495057; }
|
|
24
|
+
tr:hover { background-color: #f8f9fa; }
|
|
25
|
+
.progress-bar { width: 100%; background-color: #e9ecef; border-radius: 4px; height: 24px; overflow: hidden; margin-top: 10px; }
|
|
26
|
+
.progress { height: 100%; background-color: #28a745; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: bold; }
|
|
27
|
+
.progress.warning { background-color: #ffc107; color: #212529; }
|
|
28
|
+
.progress.danger { background-color: #dc3545; }
|
|
29
|
+
.footer { margin-top: 40px; text-align: center; color: #6c757d; font-size: 14px; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div class="container">
|
|
34
|
+
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #f8f9fa; padding-bottom: 20px; margin-bottom: 30px;">
|
|
35
|
+
<h1 style="margin: 0;">TS Analyzer Report</h1>
|
|
36
|
+
<div style="color: #6c757d;">Generated: ${new Date().toLocaleString()}</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<h2>Project Summary</h2>
|
|
40
|
+
<div class="grid">
|
|
41
|
+
<div class="card">
|
|
42
|
+
<h3>Total Files</h3>
|
|
43
|
+
<div class="value">${stats.formatNumber(stats.files)}</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="card">
|
|
46
|
+
<h3>Total Lines</h3>
|
|
47
|
+
<div class="value">${stats.formatNumber(stats.totalLines)}</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="card">
|
|
50
|
+
<h3>Code Lines</h3>
|
|
51
|
+
<div class="value" style="color: #28a745">${stats.formatNumber(stats.codeLines)}</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="card">
|
|
54
|
+
<h3>Comment Lines</h3>
|
|
55
|
+
<div class="value" style="color: #6c757d">${stats.formatNumber(stats.commentLines)}</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<h2>Files by Type</h2>
|
|
60
|
+
<table>
|
|
61
|
+
<thead>
|
|
62
|
+
<tr>
|
|
63
|
+
<th>Extension</th>
|
|
64
|
+
<th>Files</th>
|
|
65
|
+
<th>Total Lines</th>
|
|
66
|
+
<th>Code Lines</th>
|
|
67
|
+
<th>% of Codebase</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
${stats.formattedFileTypes?.map(type => `
|
|
72
|
+
<tr>
|
|
73
|
+
<td><strong>${type['Extension']}</strong></td>
|
|
74
|
+
<td>${type['Files']}</td>
|
|
75
|
+
<td>${type['Total Lines']}</td>
|
|
76
|
+
<td>${type['Code Lines']}</td>
|
|
77
|
+
<td>
|
|
78
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
79
|
+
<div style="flex-grow: 1; height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden;">
|
|
80
|
+
<div style="height: 100%; width: ${type['% of Codebase']}; background: #007bff;"></div>
|
|
81
|
+
</div>
|
|
82
|
+
<span style="font-size: 14px; width: 50px;">${type['% of Codebase']}</span>
|
|
83
|
+
</div>
|
|
84
|
+
</td>
|
|
85
|
+
</tr>
|
|
86
|
+
`).join('') || ''}
|
|
87
|
+
</tbody>
|
|
88
|
+
</table>
|
|
89
|
+
|
|
90
|
+
${stats.typescriptSafety ? `
|
|
91
|
+
<h2>TypeScript Safety</h2>
|
|
92
|
+
<div class="grid">
|
|
93
|
+
<div class="card">
|
|
94
|
+
<h3>Type Coverage</h3>
|
|
95
|
+
<div class="value">${stats.typescriptSafety.avgTypeCoverage}%</div>
|
|
96
|
+
<div class="progress-bar">
|
|
97
|
+
<div class="progress ${parseFloat(stats.typescriptSafety.avgTypeCoverage) < 80 ? 'warning' : ''}" style="width: ${stats.typescriptSafety.avgTypeCoverage}%"></div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="card">
|
|
101
|
+
<h3>Type Safety Score</h3>
|
|
102
|
+
<div class="value">${stats.typescriptSafety.avgTypeSafetyScore} <span style="font-size: 16px; color: #6c757d">/ 100</span></div>
|
|
103
|
+
<div class="progress-bar">
|
|
104
|
+
<div class="progress ${stats.typescriptSafety.avgTypeSafetyScore < 80 ? 'warning' : ''}" style="width: ${stats.typescriptSafety.avgTypeSafetyScore}%"></div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="card">
|
|
108
|
+
<h3>'any' Type Usage</h3>
|
|
109
|
+
<div class="value" style="color: ${stats.typescriptSafety.totalAnyCount > 10 ? '#dc3545' : '#28a745'}">${stats.formatNumber(stats.typescriptSafety.totalAnyCount)}</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
` : ''}
|
|
113
|
+
|
|
114
|
+
${stats.codeComplexity ? `
|
|
115
|
+
<h2>Code Complexity</h2>
|
|
116
|
+
<div class="grid">
|
|
117
|
+
<div class="card">
|
|
118
|
+
<h3>Average Complexity</h3>
|
|
119
|
+
<div class="value" style="color: ${parseFloat(stats.codeComplexity.avgComplexity) > 5 ? '#ffc107' : '#28a745'}">${stats.codeComplexity.avgComplexity}</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="card">
|
|
122
|
+
<h3>Max Complexity</h3>
|
|
123
|
+
<div class="value" style="color: ${stats.codeComplexity.maxComplexity > 15 ? '#dc3545' : '#28a745'}">${stats.formatNumber(stats.codeComplexity.maxComplexity)}</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="card">
|
|
126
|
+
<h3>Complex Files</h3>
|
|
127
|
+
<div class="value" style="color: ${stats.codeComplexity.complexFiles > 0 ? '#dc3545' : '#28a745'}">${stats.formatNumber(stats.codeComplexity.complexFiles)}</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<h2>Anti-Patterns & Code Smells</h2>
|
|
132
|
+
<div class="grid">
|
|
133
|
+
<div class="card">
|
|
134
|
+
<h3>Callback Hell</h3>
|
|
135
|
+
<div class="value" style="color: ${stats.codeComplexity.codeSmells.callbackHell > 0 ? '#dc3545' : '#28a745'}">${stats.formatNumber(stats.codeComplexity.codeSmells.callbackHell)}</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="card">
|
|
138
|
+
<h3>God Files (> 500 lines)</h3>
|
|
139
|
+
<div class="value" style="color: ${stats.codeComplexity.codeSmells.godFiles > 0 ? '#dc3545' : '#28a745'}">${stats.formatNumber(stats.codeComplexity.codeSmells.godFiles)}</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="card">
|
|
142
|
+
<h3>Excessive Params (> 4)</h3>
|
|
143
|
+
<div class="value" style="color: ${stats.codeComplexity.codeSmells.excessiveParameters > 0 ? '#dc3545' : '#28a745'}">${stats.formatNumber(stats.codeComplexity.codeSmells.excessiveParameters)}</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="card">
|
|
146
|
+
<h3>Magic Numbers</h3>
|
|
147
|
+
<div class="value" style="color: ${stats.codeComplexity.codeSmells.magicNumbers > 0 ? '#ffc107' : '#28a745'}">${stats.formatNumber(stats.codeComplexity.codeSmells.magicNumbers)}</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
` : ''}
|
|
151
|
+
|
|
152
|
+
<div class="footer">
|
|
153
|
+
Generated by ts-analyzer • A comprehensive TypeScript code analyzer
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
158
|
+
`;
|
|
159
|
+
}
|
package/src/typescript-safety.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as ts from 'typescript';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { constrainedMemory } from 'process';
|
|
5
6
|
|
|
6
7
|
export interface TypeScriptMetrics {
|
|
7
8
|
totalTypeableNodes: number;
|
|
@@ -14,6 +15,7 @@ export interface TypeScriptMetrics {
|
|
|
14
15
|
genericsCount: number;
|
|
15
16
|
typeSafetyScore: number;
|
|
16
17
|
typeComplexity: string;
|
|
18
|
+
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface TypeScriptSafetyMetrics {
|
|
@@ -33,7 +35,7 @@ export interface TypeScriptSafetyMetrics {
|
|
|
33
35
|
export async function analyzeTypeScriptSafety(filePath: string): Promise<TypeScriptMetrics> {
|
|
34
36
|
try {
|
|
35
37
|
const content = await fs.readFile(filePath, 'utf8');
|
|
36
|
-
|
|
38
|
+
|
|
37
39
|
// Create a source file
|
|
38
40
|
const sourceFile = ts.createSourceFile(
|
|
39
41
|
filePath,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { analyzeFileComplexity } from '../src/code-complexity.js';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
describe('analyzeFileComplexity', () => {
|
|
7
|
+
const testFilePath = path.join(process.cwd(), 'temp_complexity_test.ts');
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
try {
|
|
11
|
+
await fs.unlink(testFilePath);
|
|
12
|
+
} catch (e) {}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should calculate basic cyclomatic complexity', async () => {
|
|
16
|
+
const content = `
|
|
17
|
+
function complexFunc(a: number) {
|
|
18
|
+
if (a > 0) {
|
|
19
|
+
return 1;
|
|
20
|
+
} else {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
await fs.writeFile(testFilePath, content);
|
|
26
|
+
const metrics = await analyzeFileComplexity(testFilePath);
|
|
27
|
+
|
|
28
|
+
expect(metrics.maxComplexity).toBe(2); // One if statement
|
|
29
|
+
expect(metrics.functionCount).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should detect callback hell (nesting depth)', async () => {
|
|
33
|
+
const content = `
|
|
34
|
+
function nestedFunc() {
|
|
35
|
+
if (a) {
|
|
36
|
+
if (b) {
|
|
37
|
+
if (c) {
|
|
38
|
+
if (d) {
|
|
39
|
+
console.log('Deeply nested');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
await fs.writeFile(testFilePath, content);
|
|
47
|
+
const metrics = await analyzeFileComplexity(testFilePath);
|
|
48
|
+
|
|
49
|
+
expect(metrics.maxNestingDepth).toBeGreaterThanOrEqual(4);
|
|
50
|
+
expect(metrics.codeSmells.callbackHell).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should detect magic numbers', async () => {
|
|
54
|
+
const content = `
|
|
55
|
+
const a = 1234; // Magic number (> 10)
|
|
56
|
+
const b = -5678; // Magic number (< -10)
|
|
57
|
+
const c = 5; // Not a magic number
|
|
58
|
+
`;
|
|
59
|
+
await fs.writeFile(testFilePath, content);
|
|
60
|
+
const metrics = await analyzeFileComplexity(testFilePath);
|
|
61
|
+
|
|
62
|
+
expect(metrics.codeSmells.magicNumbers).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should detect excessive parameters', async () => {
|
|
66
|
+
const content = `
|
|
67
|
+
function manyParams(a, b, c, d, e, f) {
|
|
68
|
+
return a + b + c + d + e + f;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
await fs.writeFile(testFilePath, content);
|
|
72
|
+
const metrics = await analyzeFileComplexity(testFilePath);
|
|
73
|
+
|
|
74
|
+
expect(metrics.codeSmells.excessiveParameters).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateHtmlReport } from '../src/html-formatter.js';
|
|
3
|
+
import { ProjectStats } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
describe('generateHtmlReport', () => {
|
|
6
|
+
it('should generate an HTML string with stats', () => {
|
|
7
|
+
const mockStats: Partial<ProjectStats> = {
|
|
8
|
+
files: 10,
|
|
9
|
+
totalLines: 1000,
|
|
10
|
+
codeLines: 800,
|
|
11
|
+
commentLines: 100,
|
|
12
|
+
emptyLines: 100,
|
|
13
|
+
formatNumber: (n) => n.toString(),
|
|
14
|
+
formattedFileTypes: [
|
|
15
|
+
{
|
|
16
|
+
'Extension': '.ts',
|
|
17
|
+
'Files': '10',
|
|
18
|
+
'Total Lines': '1000',
|
|
19
|
+
'Code Lines': '800',
|
|
20
|
+
'Comment Lines': '100',
|
|
21
|
+
'Empty Lines': '100',
|
|
22
|
+
'% of Codebase': '80%'
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
typescriptSafety: {
|
|
26
|
+
tsFileCount: 10,
|
|
27
|
+
tsPercentage: '100',
|
|
28
|
+
avgTypeCoverage: '90',
|
|
29
|
+
totalAnyCount: 5,
|
|
30
|
+
totalAssertions: 2,
|
|
31
|
+
totalNonNullAssertions: 1,
|
|
32
|
+
avgTypeSafetyScore: 85,
|
|
33
|
+
overallComplexity: 'Low'
|
|
34
|
+
},
|
|
35
|
+
codeComplexity: {
|
|
36
|
+
analyzedFiles: 10,
|
|
37
|
+
avgComplexity: '2',
|
|
38
|
+
maxComplexity: 5,
|
|
39
|
+
avgNestingDepth: '1.5',
|
|
40
|
+
maxNestingDepth: 3,
|
|
41
|
+
avgFunctionSize: '20',
|
|
42
|
+
totalFunctions: 50,
|
|
43
|
+
complexFiles: 0,
|
|
44
|
+
overallComplexity: 'Low',
|
|
45
|
+
codeSmells: {
|
|
46
|
+
magicNumbers: 0,
|
|
47
|
+
callbackHell: 0,
|
|
48
|
+
godFiles: 0,
|
|
49
|
+
excessiveParameters: 0
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const html = generateHtmlReport(mockStats as ProjectStats);
|
|
55
|
+
|
|
56
|
+
expect(typeof html).toBe('string');
|
|
57
|
+
expect(html).toContain('<!DOCTYPE html>');
|
|
58
|
+
expect(html).toContain('TS Analyzer Report');
|
|
59
|
+
expect(html).toContain('TypeScript Safety');
|
|
60
|
+
expect(html).toContain('Code Complexity');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { countLines } from '../src/index.js';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
describe('countLines', () => {
|
|
7
|
+
const testFilePath = path.join(process.cwd(), 'temp_test_file.ts');
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
try {
|
|
11
|
+
await fs.unlink(testFilePath);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
// Ignore if file doesn't exist
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should correctly count lines in a simple file', async () => {
|
|
18
|
+
const content = `// Line 1: Comment
|
|
19
|
+
import { x } from 'y';
|
|
20
|
+
|
|
21
|
+
/* Line 4: Block comment
|
|
22
|
+
Line 5: Still block comment */
|
|
23
|
+
|
|
24
|
+
const a = 1;
|
|
25
|
+
|
|
26
|
+
// Line 9: Comment
|
|
27
|
+
const b = 2;
|
|
28
|
+
`;
|
|
29
|
+
await fs.writeFile(testFilePath, content);
|
|
30
|
+
|
|
31
|
+
const stats = await countLines(testFilePath);
|
|
32
|
+
|
|
33
|
+
expect(stats.total).toBe(11);
|
|
34
|
+
expect(stats.empty).toBe(4);
|
|
35
|
+
expect(stats.comments).toBe(3);
|
|
36
|
+
expect(stats.code).toBe(4);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle empty files', async () => {
|
|
40
|
+
await fs.writeFile(testFilePath, '');
|
|
41
|
+
const stats = await countLines(testFilePath);
|
|
42
|
+
expect(stats.total).toBe(1); // fs.readFile('...') on empty string often gives 1 line array [""]
|
|
43
|
+
expect(stats.code).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { analyzeTypeScriptSafety } from '../src/typescript-safety.js';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
describe('analyzeTypeScriptSafety', () => {
|
|
7
|
+
const testFilePath = path.join(process.cwd(), 'temp_safety_test.ts');
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
try {
|
|
11
|
+
await fs.unlink(testFilePath);
|
|
12
|
+
} catch (e) {}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should detect basic type coverage', async () => {
|
|
16
|
+
const content = `
|
|
17
|
+
const a: number = 1;
|
|
18
|
+
function b(x: string): void {}
|
|
19
|
+
let c = 'inferred';
|
|
20
|
+
`;
|
|
21
|
+
await fs.writeFile(testFilePath, content);
|
|
22
|
+
const metrics = await analyzeTypeScriptSafety(testFilePath);
|
|
23
|
+
|
|
24
|
+
expect(parseFloat(metrics.typeCoverage)).toBeGreaterThan(0);
|
|
25
|
+
expect(metrics.totalTypeableNodes).toBeGreaterThan(0);
|
|
26
|
+
expect(metrics.anyTypeCount).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should detect "any" usage', async () => {
|
|
30
|
+
const content = `
|
|
31
|
+
const a: any = 1;
|
|
32
|
+
function b(x: any): void {}
|
|
33
|
+
`;
|
|
34
|
+
await fs.writeFile(testFilePath, content);
|
|
35
|
+
const metrics = await analyzeTypeScriptSafety(testFilePath);
|
|
36
|
+
|
|
37
|
+
expect(metrics.anyTypeCount).toBe(2);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should detect type assertions', async () => {
|
|
41
|
+
const content = `
|
|
42
|
+
const a = 1 as any;
|
|
43
|
+
const b = <string>"foo";
|
|
44
|
+
`;
|
|
45
|
+
await fs.writeFile(testFilePath, content);
|
|
46
|
+
const metrics = await analyzeTypeScriptSafety(testFilePath);
|
|
47
|
+
|
|
48
|
+
expect(metrics.typeAssertions).toBeGreaterThan(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/vitest.config.ts
ADDED
package/ts-analyzer.config.json
DELETED