ts-analyzer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +356 -0
- package/bin/cli.js +4 -0
- package/bin/cli.ts +241 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +215 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/code-complexity.d.ts +24 -0
- package/dist/src/code-complexity.js +338 -0
- package/dist/src/code-complexity.js.map +1 -0
- package/dist/src/index.d.ts +47 -0
- package/dist/src/index.js +115 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/table-formatter.d.ts +6 -0
- package/dist/src/table-formatter.js +82 -0
- package/dist/src/table-formatter.js.map +1 -0
- package/dist/src/typescript-safety.d.ts +27 -0
- package/dist/src/typescript-safety.js +287 -0
- package/dist/src/typescript-safety.js.map +1 -0
- package/package.json +51 -0
- package/src/code-complexity.ts +431 -0
- package/src/index.ts +180 -0
- package/src/table-formatter.ts +94 -0
- package/src/typescript-safety.ts +346 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// src/code-complexity.ts
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import * as espree from 'espree';
|
|
5
|
+
import * as estraverse from 'estraverse';
|
|
6
|
+
import * as ts from 'typescript';
|
|
7
|
+
import { Node as EstreeNode } from 'estree';
|
|
8
|
+
|
|
9
|
+
// Define our own simplified Node interface
|
|
10
|
+
interface CustomNode {
|
|
11
|
+
type: string;
|
|
12
|
+
body?: any;
|
|
13
|
+
loc?: { start: { line: number }, end: { line: number } };
|
|
14
|
+
params?: any[];
|
|
15
|
+
id?: { name: string };
|
|
16
|
+
operator?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SimplifiedProgram {
|
|
20
|
+
type: string;
|
|
21
|
+
body: any[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FunctionData {
|
|
25
|
+
type: string;
|
|
26
|
+
name: string;
|
|
27
|
+
complexity: number;
|
|
28
|
+
nestingDepth: number;
|
|
29
|
+
paramCount: number;
|
|
30
|
+
lineCount: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FileComplexityMetrics {
|
|
34
|
+
avgComplexity: string;
|
|
35
|
+
maxComplexity: number;
|
|
36
|
+
avgNestingDepth: string;
|
|
37
|
+
maxNestingDepth: number;
|
|
38
|
+
avgFunctionSize: string;
|
|
39
|
+
avgParams: string;
|
|
40
|
+
functionCount: number;
|
|
41
|
+
complexityScore: number;
|
|
42
|
+
complexityRating: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CodeComplexityMetrics {
|
|
46
|
+
analyzedFiles: number;
|
|
47
|
+
avgComplexity: string;
|
|
48
|
+
maxComplexity: number;
|
|
49
|
+
avgNestingDepth: string;
|
|
50
|
+
maxNestingDepth: number;
|
|
51
|
+
avgFunctionSize: string;
|
|
52
|
+
totalFunctions: number;
|
|
53
|
+
complexFiles: number;
|
|
54
|
+
overallComplexity: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Calculate cyclomatic complexity
|
|
58
|
+
function calculateCyclomaticComplexity(ast: any): number {
|
|
59
|
+
let complexity = 1; // Start with 1 (one path through the function)
|
|
60
|
+
|
|
61
|
+
estraverse.traverse(ast, {
|
|
62
|
+
enter(node: any) {
|
|
63
|
+
// Count conditional statements and operators that create new paths
|
|
64
|
+
switch (node.type) {
|
|
65
|
+
case 'IfStatement':
|
|
66
|
+
case 'ConditionalExpression':
|
|
67
|
+
case 'SwitchCase':
|
|
68
|
+
case 'ForStatement':
|
|
69
|
+
case 'ForInStatement':
|
|
70
|
+
case 'ForOfStatement':
|
|
71
|
+
case 'WhileStatement':
|
|
72
|
+
case 'DoWhileStatement':
|
|
73
|
+
complexity++;
|
|
74
|
+
break;
|
|
75
|
+
case 'LogicalExpression':
|
|
76
|
+
if (node.operator === '&&' || node.operator === '||') {
|
|
77
|
+
complexity++;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return complexity;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Calculate nesting depth
|
|
88
|
+
function calculateNestingDepth(ast: any): number {
|
|
89
|
+
let maxDepth = 0;
|
|
90
|
+
let currentDepth = 0;
|
|
91
|
+
|
|
92
|
+
estraverse.traverse(ast, {
|
|
93
|
+
enter(node: any) {
|
|
94
|
+
// Count nesting for blocks and control structures
|
|
95
|
+
switch (node.type) {
|
|
96
|
+
case 'BlockStatement':
|
|
97
|
+
case 'IfStatement':
|
|
98
|
+
case 'SwitchStatement':
|
|
99
|
+
case 'ForStatement':
|
|
100
|
+
case 'ForInStatement':
|
|
101
|
+
case 'ForOfStatement':
|
|
102
|
+
case 'WhileStatement':
|
|
103
|
+
case 'DoWhileStatement':
|
|
104
|
+
case 'TryStatement':
|
|
105
|
+
currentDepth++;
|
|
106
|
+
maxDepth = Math.max(maxDepth, currentDepth);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
leave(node: any) {
|
|
111
|
+
// Decrement depth when leaving a block
|
|
112
|
+
switch (node.type) {
|
|
113
|
+
case 'BlockStatement':
|
|
114
|
+
case 'IfStatement':
|
|
115
|
+
case 'SwitchStatement':
|
|
116
|
+
case 'ForStatement':
|
|
117
|
+
case 'ForInStatement':
|
|
118
|
+
case 'ForOfStatement':
|
|
119
|
+
case 'WhileStatement':
|
|
120
|
+
case 'DoWhileStatement':
|
|
121
|
+
case 'TryStatement':
|
|
122
|
+
currentDepth--;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return maxDepth;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Find function declarations and calculate their metrics
|
|
132
|
+
function analyzeFunctions(ast: any): FunctionData[] {
|
|
133
|
+
const functions: FunctionData[] = [];
|
|
134
|
+
|
|
135
|
+
estraverse.traverse(ast, {
|
|
136
|
+
enter(node: any) {
|
|
137
|
+
if (node.type === 'FunctionDeclaration' ||
|
|
138
|
+
node.type === 'FunctionExpression' ||
|
|
139
|
+
node.type === 'ArrowFunctionExpression') {
|
|
140
|
+
|
|
141
|
+
// Skip functions without a body
|
|
142
|
+
if (!node.body) return;
|
|
143
|
+
|
|
144
|
+
// Get the function's source code
|
|
145
|
+
let functionAst = node;
|
|
146
|
+
if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') {
|
|
147
|
+
functionAst = {
|
|
148
|
+
type: 'ArrowFunctionExpression',
|
|
149
|
+
body: {
|
|
150
|
+
type: 'BlockStatement',
|
|
151
|
+
body: [{ type: 'ReturnStatement', argument: node.body }]
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Calculate complexity
|
|
157
|
+
const complexity = calculateCyclomaticComplexity(functionAst);
|
|
158
|
+
|
|
159
|
+
// Calculate nesting
|
|
160
|
+
const nestingDepth = calculateNestingDepth(functionAst);
|
|
161
|
+
|
|
162
|
+
// Count function parameters
|
|
163
|
+
const paramCount = node.params ? node.params.length : 0;
|
|
164
|
+
|
|
165
|
+
// Estimate function size (lines)
|
|
166
|
+
let lineCount = 0;
|
|
167
|
+
if (node.loc) {
|
|
168
|
+
lineCount = node.loc.end.line - node.loc.start.line + 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
functions.push({
|
|
172
|
+
type: node.type,
|
|
173
|
+
name: node.id ? node.id.name : 'anonymous',
|
|
174
|
+
complexity,
|
|
175
|
+
nestingDepth,
|
|
176
|
+
paramCount,
|
|
177
|
+
lineCount
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return functions;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Parse a TypeScript file using the TypeScript compiler
|
|
187
|
+
function parseTypeScriptFile(content: string): SimplifiedProgram {
|
|
188
|
+
try {
|
|
189
|
+
// Create a source file
|
|
190
|
+
const sourceFile = ts.createSourceFile(
|
|
191
|
+
'temp.ts',
|
|
192
|
+
content,
|
|
193
|
+
ts.ScriptTarget.Latest,
|
|
194
|
+
true
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Create a simplified program AST structure
|
|
198
|
+
const simplifiedAst: SimplifiedProgram = {
|
|
199
|
+
type: 'Program',
|
|
200
|
+
body: []
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Function to visit TypeScript nodes and extract function information
|
|
204
|
+
function visit(node: ts.Node) {
|
|
205
|
+
if (ts.isFunctionDeclaration(node) ||
|
|
206
|
+
ts.isMethodDeclaration(node) ||
|
|
207
|
+
ts.isArrowFunction(node) ||
|
|
208
|
+
ts.isFunctionExpression(node)) {
|
|
209
|
+
|
|
210
|
+
const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
211
|
+
const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
212
|
+
|
|
213
|
+
const functionNode = {
|
|
214
|
+
type: ts.isFunctionDeclaration(node) ? 'FunctionDeclaration' :
|
|
215
|
+
ts.isArrowFunction(node) ? 'ArrowFunctionExpression' : 'FunctionExpression',
|
|
216
|
+
id: ts.isFunctionDeclaration(node) && node.name ? { name: node.name.text } : undefined,
|
|
217
|
+
params: node.parameters ? node.parameters.map(() => ({})) : [],
|
|
218
|
+
body: { type: 'BlockStatement', body: [] as any[] },
|
|
219
|
+
loc: {
|
|
220
|
+
start: { line: startLine },
|
|
221
|
+
end: { line: endLine }
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
simplifiedAst.body.push(functionNode);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ts.forEachChild(node, visit);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
visit(sourceFile);
|
|
232
|
+
|
|
233
|
+
return simplifiedAst;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Error parsing TypeScript file:', error);
|
|
236
|
+
return { type: 'Program', body: [] };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function analyzeFileComplexity(filePath: string): Promise<FileComplexityMetrics> {
|
|
241
|
+
try {
|
|
242
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
243
|
+
const extension = path.extname(filePath);
|
|
244
|
+
|
|
245
|
+
// Choose the appropriate parser based on file extension
|
|
246
|
+
let ast: any;
|
|
247
|
+
|
|
248
|
+
if (extension === '.ts' || extension === '.tsx') {
|
|
249
|
+
// Use TypeScript parser for .ts and .tsx files
|
|
250
|
+
ast = parseTypeScriptFile(content);
|
|
251
|
+
} else {
|
|
252
|
+
// Use espree for JavaScript files
|
|
253
|
+
try {
|
|
254
|
+
const parserOptions = {
|
|
255
|
+
ecmaVersion: 2022,
|
|
256
|
+
sourceType: 'module',
|
|
257
|
+
ecmaFeatures: {
|
|
258
|
+
jsx: extension === '.jsx'
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
ast = espree.parse(content, parserOptions as any);
|
|
263
|
+
} catch (parseError) {
|
|
264
|
+
console.error(`Parsing error in ${filePath}: ${(parseError as Error).message}`);
|
|
265
|
+
return {
|
|
266
|
+
avgComplexity: '0',
|
|
267
|
+
maxComplexity: 0,
|
|
268
|
+
avgNestingDepth: '0',
|
|
269
|
+
maxNestingDepth: 0,
|
|
270
|
+
avgFunctionSize: '0',
|
|
271
|
+
avgParams: '0',
|
|
272
|
+
functionCount: 0,
|
|
273
|
+
complexityScore: 0,
|
|
274
|
+
complexityRating: 'N/A'
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Analyze the functions in the file
|
|
280
|
+
const functions = analyzeFunctions(ast);
|
|
281
|
+
|
|
282
|
+
if (functions.length === 0) {
|
|
283
|
+
return {
|
|
284
|
+
avgComplexity: '0',
|
|
285
|
+
maxComplexity: 0,
|
|
286
|
+
avgNestingDepth: '0',
|
|
287
|
+
maxNestingDepth: 0,
|
|
288
|
+
avgFunctionSize: '0',
|
|
289
|
+
avgParams: '0',
|
|
290
|
+
functionCount: 0,
|
|
291
|
+
complexityScore: 0,
|
|
292
|
+
complexityRating: 'N/A'
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Calculate average and maximum values
|
|
297
|
+
const totalComplexity = functions.reduce((sum, fn) => sum + fn.complexity, 0);
|
|
298
|
+
const maxComplexity = Math.max(...functions.map(fn => fn.complexity));
|
|
299
|
+
|
|
300
|
+
const totalNestingDepth = functions.reduce((sum, fn) => sum + fn.nestingDepth, 0);
|
|
301
|
+
const maxNestingDepth = Math.max(...functions.map(fn => fn.nestingDepth));
|
|
302
|
+
|
|
303
|
+
const totalLineCount = functions.reduce((sum, fn) => sum + fn.lineCount, 0);
|
|
304
|
+
const totalParams = functions.reduce((sum, fn) => sum + fn.paramCount, 0);
|
|
305
|
+
|
|
306
|
+
const avgComplexity = totalComplexity / functions.length;
|
|
307
|
+
const avgNestingDepth = totalNestingDepth / functions.length;
|
|
308
|
+
const avgFunctionSize = totalLineCount / functions.length;
|
|
309
|
+
const avgParams = totalParams / functions.length;
|
|
310
|
+
|
|
311
|
+
// Calculate a complexity score (0-100, higher means more complex)
|
|
312
|
+
const complexityWeight = 0.4;
|
|
313
|
+
const nestingWeight = 0.3;
|
|
314
|
+
const sizeWeight = 0.2;
|
|
315
|
+
const paramsWeight = 0.1;
|
|
316
|
+
|
|
317
|
+
// Normalized scores (0-100)
|
|
318
|
+
const complexityScore = Math.min((avgComplexity / 10) * 100, 100);
|
|
319
|
+
const nestingScore = Math.min((avgNestingDepth / 5) * 100, 100);
|
|
320
|
+
const sizeScore = Math.min((avgFunctionSize / 30) * 100, 100);
|
|
321
|
+
const paramsScore = Math.min((avgParams / 6) * 100, 100);
|
|
322
|
+
|
|
323
|
+
const totalScore = (complexityScore * complexityWeight) +
|
|
324
|
+
(nestingScore * nestingWeight) +
|
|
325
|
+
(sizeScore * sizeWeight) +
|
|
326
|
+
(paramsScore * paramsWeight);
|
|
327
|
+
|
|
328
|
+
// Define complexity rating
|
|
329
|
+
let complexityRating: string;
|
|
330
|
+
if (totalScore < 30) complexityRating = 'Low';
|
|
331
|
+
else if (totalScore < 60) complexityRating = 'Medium';
|
|
332
|
+
else complexityRating = 'High';
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
avgComplexity: avgComplexity.toFixed(1),
|
|
336
|
+
maxComplexity,
|
|
337
|
+
avgNestingDepth: avgNestingDepth.toFixed(1),
|
|
338
|
+
maxNestingDepth,
|
|
339
|
+
avgFunctionSize: avgFunctionSize.toFixed(1),
|
|
340
|
+
avgParams: avgParams.toFixed(1),
|
|
341
|
+
functionCount: functions.length,
|
|
342
|
+
complexityScore: Math.round(totalScore),
|
|
343
|
+
complexityRating
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(`Error analyzing file complexity for ${filePath}:`, error);
|
|
347
|
+
return {
|
|
348
|
+
avgComplexity: '0',
|
|
349
|
+
maxComplexity: 0,
|
|
350
|
+
avgNestingDepth: '0',
|
|
351
|
+
maxNestingDepth: 0,
|
|
352
|
+
avgFunctionSize: '0',
|
|
353
|
+
avgParams: '0',
|
|
354
|
+
functionCount: 0,
|
|
355
|
+
complexityScore: 0,
|
|
356
|
+
complexityRating: 'N/A'
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function calculateProjectComplexity(projectPath: string, fileList: string[]): Promise<CodeComplexityMetrics> {
|
|
362
|
+
const jsExtensions = ['.js', '.jsx', '.ts', '.tsx'];
|
|
363
|
+
const jsFiles = fileList.filter(file => jsExtensions.includes(path.extname(file)));
|
|
364
|
+
|
|
365
|
+
if (jsFiles.length === 0) {
|
|
366
|
+
return {
|
|
367
|
+
analyzedFiles: 0,
|
|
368
|
+
avgComplexity: '0.0',
|
|
369
|
+
maxComplexity: 0,
|
|
370
|
+
avgNestingDepth: '0.0',
|
|
371
|
+
maxNestingDepth: 0,
|
|
372
|
+
avgFunctionSize: '0.0',
|
|
373
|
+
totalFunctions: 0,
|
|
374
|
+
complexFiles: 0,
|
|
375
|
+
overallComplexity: 'N/A'
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let totalComplexity = 0;
|
|
380
|
+
let totalNestingDepth = 0;
|
|
381
|
+
let totalFunctionSize = 0;
|
|
382
|
+
let maxComplexityGlobal = 0;
|
|
383
|
+
let maxNestingDepthGlobal = 0;
|
|
384
|
+
let totalFunctions = 0;
|
|
385
|
+
let complexFiles = 0;
|
|
386
|
+
|
|
387
|
+
for (const file of jsFiles) {
|
|
388
|
+
const filePath = path.join(projectPath, file);
|
|
389
|
+
const metrics = await analyzeFileComplexity(filePath);
|
|
390
|
+
|
|
391
|
+
totalComplexity += parseFloat(metrics.avgComplexity) * metrics.functionCount;
|
|
392
|
+
totalNestingDepth += parseFloat(metrics.avgNestingDepth) * metrics.functionCount;
|
|
393
|
+
totalFunctionSize += parseFloat(metrics.avgFunctionSize) * metrics.functionCount;
|
|
394
|
+
maxComplexityGlobal = Math.max(maxComplexityGlobal, metrics.maxComplexity);
|
|
395
|
+
maxNestingDepthGlobal = Math.max(maxNestingDepthGlobal, metrics.maxNestingDepth);
|
|
396
|
+
totalFunctions += metrics.functionCount;
|
|
397
|
+
|
|
398
|
+
if (metrics.complexityRating === 'High') {
|
|
399
|
+
complexFiles++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Calculate project averages
|
|
404
|
+
const avgComplexity = totalFunctions > 0 ? (totalComplexity / totalFunctions).toFixed(1) : '0.0';
|
|
405
|
+
const avgNestingDepth = totalFunctions > 0 ? (totalNestingDepth / totalFunctions).toFixed(1) : '0.0';
|
|
406
|
+
const avgFunctionSize = totalFunctions > 0 ? (totalFunctionSize / totalFunctions).toFixed(1) : '0.0';
|
|
407
|
+
|
|
408
|
+
// Determine overall complexity
|
|
409
|
+
let overallComplexity: string;
|
|
410
|
+
const complexFilesPercentage = (complexFiles / jsFiles.length) * 100;
|
|
411
|
+
|
|
412
|
+
if (parseFloat(avgComplexity) < 4 && complexFilesPercentage < 10) {
|
|
413
|
+
overallComplexity = 'Low';
|
|
414
|
+
} else if (parseFloat(avgComplexity) < 8 && complexFilesPercentage < 25) {
|
|
415
|
+
overallComplexity = 'Medium';
|
|
416
|
+
} else {
|
|
417
|
+
overallComplexity = 'High';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
analyzedFiles: jsFiles.length,
|
|
422
|
+
avgComplexity,
|
|
423
|
+
maxComplexity: maxComplexityGlobal,
|
|
424
|
+
avgNestingDepth,
|
|
425
|
+
maxNestingDepth: maxNestingDepthGlobal,
|
|
426
|
+
avgFunctionSize,
|
|
427
|
+
totalFunctions,
|
|
428
|
+
complexFiles,
|
|
429
|
+
overallComplexity
|
|
430
|
+
};
|
|
431
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { calculateProjectTypeSafety, TypeScriptSafetyMetrics } from './typescript-safety.js';
|
|
5
|
+
import { calculateProjectComplexity, CodeComplexityMetrics } from './code-complexity.js';
|
|
6
|
+
|
|
7
|
+
export const REACT_EXTENSIONS = [
|
|
8
|
+
'.js', '.jsx', '.ts', '.tsx',
|
|
9
|
+
'.css', '.scss', '.sass',
|
|
10
|
+
'.html', '.json'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_IGNORE = [
|
|
14
|
+
'node_modules',
|
|
15
|
+
'build',
|
|
16
|
+
'dist',
|
|
17
|
+
'.git',
|
|
18
|
+
'coverage',
|
|
19
|
+
'.next',
|
|
20
|
+
'out'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export interface LineCount {
|
|
24
|
+
total: number;
|
|
25
|
+
nonEmpty: number;
|
|
26
|
+
code: number;
|
|
27
|
+
comments: number;
|
|
28
|
+
empty: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FileTypeData {
|
|
32
|
+
files: number;
|
|
33
|
+
totalLines: number;
|
|
34
|
+
codeLines: number;
|
|
35
|
+
commentLines: number;
|
|
36
|
+
emptyLines: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FileTypeFormatted {
|
|
40
|
+
'Extension': string;
|
|
41
|
+
'Files': string;
|
|
42
|
+
'Total Lines': string;
|
|
43
|
+
'Code Lines': string;
|
|
44
|
+
'Comment Lines': string;
|
|
45
|
+
'Empty Lines': string;
|
|
46
|
+
'% of Codebase': string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ProjectOptions {
|
|
50
|
+
excludePatterns?: string[];
|
|
51
|
+
additionalExtensions?: string[];
|
|
52
|
+
analyzeSafety?: boolean;
|
|
53
|
+
analyzeComplexity?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ProjectStats {
|
|
57
|
+
files: number;
|
|
58
|
+
totalLines: number;
|
|
59
|
+
codeLines: number;
|
|
60
|
+
commentLines: number;
|
|
61
|
+
emptyLines: number;
|
|
62
|
+
fileTypes: Record<string, FileTypeData>;
|
|
63
|
+
formattedFileTypes?: FileTypeFormatted[];
|
|
64
|
+
typescriptSafety: TypeScriptSafetyMetrics | null;
|
|
65
|
+
codeComplexity: CodeComplexityMetrics | null;
|
|
66
|
+
formatNumber: (num: number) => string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function countLines(filePath: string): Promise<LineCount> {
|
|
70
|
+
try {
|
|
71
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
72
|
+
const lines: string[] = content.split('\n');
|
|
73
|
+
const nonEmptyLines = lines.filter(line => line.trim().length > 0);
|
|
74
|
+
const nonCommentLines = nonEmptyLines.filter(line => {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
return !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
total: lines.length,
|
|
81
|
+
nonEmpty: nonEmptyLines.length,
|
|
82
|
+
code: nonCommentLines.length,
|
|
83
|
+
comments: nonEmptyLines.length - nonCommentLines.length,
|
|
84
|
+
empty: lines.length - nonEmptyLines.length
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Error reading file ${filePath}:`, error);
|
|
88
|
+
return { total: 0, nonEmpty: 0, code: 0, comments: 0, empty: 0 };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function analyzeProject(projectPath = '.', options: ProjectOptions = {}): Promise<ProjectStats> {
|
|
93
|
+
const stats: ProjectStats = {
|
|
94
|
+
files: 0,
|
|
95
|
+
totalLines: 0,
|
|
96
|
+
codeLines: 0,
|
|
97
|
+
commentLines: 0,
|
|
98
|
+
emptyLines: 0,
|
|
99
|
+
fileTypes: {},
|
|
100
|
+
typescriptSafety: null,
|
|
101
|
+
codeComplexity: null,
|
|
102
|
+
formatNumber: (num: number) => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const extensions = [...REACT_EXTENSIONS, ...(options.additionalExtensions || [])];
|
|
106
|
+
const ignorePatterns = [...DEFAULT_IGNORE, ...(options.excludePatterns || [])];
|
|
107
|
+
|
|
108
|
+
const allFilePaths: string[] = [];
|
|
109
|
+
|
|
110
|
+
async function traverse(currentPath: string): Promise<void> {
|
|
111
|
+
const files = await fs.readdir(currentPath);
|
|
112
|
+
|
|
113
|
+
for (const file of files) {
|
|
114
|
+
const fullPath = path.join(currentPath, file);
|
|
115
|
+
const stat = await fs.stat(fullPath);
|
|
116
|
+
|
|
117
|
+
if (stat.isDirectory()) {
|
|
118
|
+
if (!ignorePatterns.includes(file)) {
|
|
119
|
+
await traverse(fullPath);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const ext = path.extname(file);
|
|
125
|
+
if (!extensions.includes(ext)) continue;
|
|
126
|
+
|
|
127
|
+
stats.files++;
|
|
128
|
+
const relativePath = path.relative(projectPath, fullPath);
|
|
129
|
+
allFilePaths.push(relativePath);
|
|
130
|
+
|
|
131
|
+
if (!stats.fileTypes[ext]) {
|
|
132
|
+
stats.fileTypes[ext] = {
|
|
133
|
+
files: 0,
|
|
134
|
+
totalLines: 0,
|
|
135
|
+
codeLines: 0,
|
|
136
|
+
commentLines: 0,
|
|
137
|
+
emptyLines: 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { total, code, comments, empty } = await countLines(fullPath);
|
|
142
|
+
|
|
143
|
+
stats.totalLines += total;
|
|
144
|
+
stats.codeLines += code;
|
|
145
|
+
stats.commentLines += comments;
|
|
146
|
+
stats.emptyLines += empty;
|
|
147
|
+
|
|
148
|
+
stats.fileTypes[ext].files++;
|
|
149
|
+
stats.fileTypes[ext].totalLines += total;
|
|
150
|
+
stats.fileTypes[ext].codeLines += code;
|
|
151
|
+
stats.fileTypes[ext].commentLines += comments;
|
|
152
|
+
stats.fileTypes[ext].emptyLines += empty;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await traverse(projectPath);
|
|
157
|
+
|
|
158
|
+
stats.formattedFileTypes = Object.entries(stats.fileTypes)
|
|
159
|
+
.sort(([, a], [, b]) => b.files - a.files)
|
|
160
|
+
.map(([ext, data]) => ({
|
|
161
|
+
'Extension': ext,
|
|
162
|
+
'Files': stats.formatNumber(data.files),
|
|
163
|
+
'Total Lines': stats.formatNumber(data.totalLines),
|
|
164
|
+
'Code Lines': stats.formatNumber(data.codeLines),
|
|
165
|
+
'Comment Lines': stats.formatNumber(data.commentLines),
|
|
166
|
+
'Empty Lines': stats.formatNumber(data.emptyLines),
|
|
167
|
+
'% of Codebase': `${((data.codeLines / stats.codeLines) * 100).toFixed(1)}%`
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
// Now analyze TypeScript safety and code complexity if requested
|
|
171
|
+
if (options.analyzeSafety !== false) {
|
|
172
|
+
stats.typescriptSafety = await calculateProjectTypeSafety(projectPath, allFilePaths);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (options.analyzeComplexity !== false) {
|
|
176
|
+
stats.codeComplexity = await calculateProjectComplexity(projectPath, allFilePaths);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return stats;
|
|
180
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/table-formatter.ts
|
|
2
|
+
/**
|
|
3
|
+
* Simple table formatter that handles ANSI color codes correctly
|
|
4
|
+
*/
|
|
5
|
+
export function formatTable(data: Array<Record<string, any>>, options?: { title?: string }): void {
|
|
6
|
+
if (!data || data.length === 0) {
|
|
7
|
+
console.log('No data to display');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Print the title if provided
|
|
12
|
+
if (options?.title) {
|
|
13
|
+
console.log(`\n${options.title}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Extract all unique keys from the data
|
|
17
|
+
const allKeys = new Set<string>();
|
|
18
|
+
data.forEach(item => {
|
|
19
|
+
Object.keys(item).forEach(key => allKeys.add(key));
|
|
20
|
+
});
|
|
21
|
+
const headers = Array.from(allKeys);
|
|
22
|
+
|
|
23
|
+
// Function to get visible string length (excluding ANSI color codes)
|
|
24
|
+
const getVisibleLength = (str: string): number => {
|
|
25
|
+
// Remove ANSI color codes for width calculation
|
|
26
|
+
return str.replace(/\x1B\[\d+m/g, '').length;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Calculate the width of each column
|
|
30
|
+
const colWidths: Record<string, number> = {};
|
|
31
|
+
headers.forEach(header => {
|
|
32
|
+
// Start with the header width
|
|
33
|
+
colWidths[header] = header.length;
|
|
34
|
+
|
|
35
|
+
// Check each row for this column's value and update width if needed
|
|
36
|
+
data.forEach(row => {
|
|
37
|
+
const value = row[header] !== undefined ? String(row[header]) : '';
|
|
38
|
+
colWidths[header] = Math.max(colWidths[header], getVisibleLength(value));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Function to pad a string to desired visible length, accounting for ANSI codes
|
|
43
|
+
const padString = (str: string, length: number, padChar = ' '): string => {
|
|
44
|
+
const visibleLength = getVisibleLength(str);
|
|
45
|
+
const paddingNeeded = Math.max(0, length - visibleLength);
|
|
46
|
+
return str + padChar.repeat(paddingNeeded);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Create the header row
|
|
50
|
+
let headerRow = '│ ';
|
|
51
|
+
headers.forEach((header, index) => {
|
|
52
|
+
const isLast = index === headers.length - 1;
|
|
53
|
+
headerRow += padString(header, colWidths[header]) + (isLast ? ' │' : ' │ ');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Create a separator row
|
|
57
|
+
let separator = '├─';
|
|
58
|
+
headers.forEach((header, index) => {
|
|
59
|
+
const isLast = index === headers.length - 1;
|
|
60
|
+
separator += '─'.repeat(colWidths[header]) + (isLast ? '─┤' : '─┼─');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Create the top border
|
|
64
|
+
let topBorder = '┌─';
|
|
65
|
+
headers.forEach((header, index) => {
|
|
66
|
+
const isLast = index === headers.length - 1;
|
|
67
|
+
topBorder += '─'.repeat(colWidths[header]) + (isLast ? '─┐' : '─┬─');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Create the bottom border
|
|
71
|
+
let bottomBorder = '└─';
|
|
72
|
+
headers.forEach((header, index) => {
|
|
73
|
+
const isLast = index === headers.length - 1;
|
|
74
|
+
bottomBorder += '─'.repeat(colWidths[header]) + (isLast ? '─┘' : '─┴─');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Print the table
|
|
78
|
+
console.log(topBorder);
|
|
79
|
+
console.log(headerRow);
|
|
80
|
+
console.log(separator);
|
|
81
|
+
|
|
82
|
+
// Print each data row
|
|
83
|
+
data.forEach(row => {
|
|
84
|
+
let dataRow = '│ ';
|
|
85
|
+
headers.forEach((header, index) => {
|
|
86
|
+
const isLast = index === headers.length - 1;
|
|
87
|
+
const value = row[header] !== undefined ? String(row[header]) : '';
|
|
88
|
+
dataRow += padString(value, colWidths[header]) + (isLast ? ' │' : ' │ ');
|
|
89
|
+
});
|
|
90
|
+
console.log(dataRow);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log(bottomBorder);
|
|
94
|
+
}
|