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.
@@ -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
- const functionNode = {
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 &bull; A comprehensive TypeScript code analyzer
154
+ </div>
155
+ </div>
156
+ </body>
157
+ </html>
158
+ `;
159
+ }
@@ -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
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['test/**/*.test.ts'],
8
+ },
9
+ });
@@ -1,3 +0,0 @@
1
- {
2
- "safety": false
3
- }