tech-debt-score 0.1.5 → 0.1.7
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/dist/adapters/input/FileSystemReader.d.ts.map +1 -1
- package/dist/adapters/input/FileSystemReader.js +4 -5
- package/dist/adapters/input/FileSystemReader.js.map +1 -1
- package/dist/adapters/output/TerminalReporter.d.ts.map +1 -1
- package/dist/adapters/output/TerminalReporter.js +8 -0
- package/dist/adapters/output/TerminalReporter.js.map +1 -1
- package/dist/application/config/AnalysisConfig.d.ts.map +1 -1
- package/dist/application/config/AnalysisConfig.js +10 -2
- package/dist/application/config/AnalysisConfig.js.map +1 -1
- package/dist/application/services/AnalysisService.d.ts.map +1 -1
- package/dist/application/services/AnalysisService.js +29 -11
- package/dist/application/services/AnalysisService.js.map +1 -1
- package/dist/cli/commands/analyze.d.ts.map +1 -1
- package/dist/cli/commands/analyze.js +8 -7
- package/dist/cli/commands/analyze.js.map +1 -1
- package/package.json +8 -2
- package/DEVELOPMENT.md +0 -147
- package/SETUP_COMPLETE.md +0 -188
- package/TECHNICAL_DESIGN.md +0 -563
- package/src/adapters/input/FileSystemReader.ts +0 -47
- package/src/adapters/input/TypeScriptParser.ts +0 -367
- package/src/adapters/output/JsonExporter.ts +0 -48
- package/src/adapters/output/TerminalReporter.ts +0 -94
- package/src/application/config/AnalysisConfig.ts +0 -58
- package/src/application/ports/IFileReader.ts +0 -36
- package/src/application/ports/IParser.ts +0 -40
- package/src/application/ports/IReporter.ts +0 -26
- package/src/application/services/AnalysisService.ts +0 -218
- package/src/application/services/DependencyAnalyzer.ts +0 -158
- package/src/application/services/DuplicationDetector.ts +0 -207
- package/src/cli/commands/analyze.ts +0 -77
- package/src/cli/index.ts +0 -81
- package/src/domain/entities/Finding.ts +0 -79
- package/src/domain/entities/Metric.ts +0 -70
- package/src/domain/entities/Rule.ts +0 -49
- package/src/domain/entities/Score.ts +0 -94
- package/src/domain/index.ts +0 -15
- package/src/domain/rules/CircularDependencyRule.ts +0 -65
- package/src/domain/rules/ComplexityRule.ts +0 -88
- package/src/domain/rules/DuplicationRule.ts +0 -70
- package/src/domain/rules/SizeRule.ts +0 -98
- package/src/domain/rules/TypeSafetyRule.ts +0 -63
- package/src/index.ts +0 -0
- package/src/shared/types.ts +0 -18
- package/tests/application/index.test.ts +0 -12
- package/tests/domain/index.test.ts +0 -14
- package/tests/e2e/index.test.ts +0 -13
- package/tsconfig.json +0 -31
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Input Adapter: TypeScript/JavaScript Parser
|
|
3
|
-
* Implements IParser using TypeScript Compiler API for AST-based analysis
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as ts from 'typescript';
|
|
7
|
-
import type { IParser, ParseResult } from '../../application/ports/IParser.js';
|
|
8
|
-
import type { Metric } from '../../domain/entities/Metric.js';
|
|
9
|
-
import { MetricBuilder } from '../../domain/entities/Metric.js';
|
|
10
|
-
import type { SourceLocation } from '../../shared/types.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Helper class to extract metrics from AST nodes
|
|
14
|
-
*/
|
|
15
|
-
class MetricExtractor {
|
|
16
|
-
private metrics: Metric[] = [];
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
private sourceFile: ts.SourceFile,
|
|
20
|
-
private filePath: string
|
|
21
|
-
) {}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Node types that contribute to cyclomatic complexity
|
|
25
|
-
*/
|
|
26
|
-
private static readonly COMPLEXITY_KINDS = new Set<ts.SyntaxKind>([
|
|
27
|
-
ts.SyntaxKind.IfStatement,
|
|
28
|
-
ts.SyntaxKind.ForStatement,
|
|
29
|
-
ts.SyntaxKind.ForInStatement,
|
|
30
|
-
ts.SyntaxKind.ForOfStatement,
|
|
31
|
-
ts.SyntaxKind.WhileStatement,
|
|
32
|
-
ts.SyntaxKind.DoStatement,
|
|
33
|
-
ts.SyntaxKind.CaseClause,
|
|
34
|
-
ts.SyntaxKind.ConditionalExpression,
|
|
35
|
-
ts.SyntaxKind.CatchClause,
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Node types that contribute to nesting depth
|
|
40
|
-
*/
|
|
41
|
-
private static readonly NESTING_KINDS = new Set<ts.SyntaxKind>([
|
|
42
|
-
ts.SyntaxKind.Block,
|
|
43
|
-
ts.SyntaxKind.IfStatement,
|
|
44
|
-
ts.SyntaxKind.ForStatement,
|
|
45
|
-
ts.SyntaxKind.ForInStatement,
|
|
46
|
-
ts.SyntaxKind.ForOfStatement,
|
|
47
|
-
ts.SyntaxKind.WhileStatement,
|
|
48
|
-
ts.SyntaxKind.DoStatement,
|
|
49
|
-
ts.SyntaxKind.SwitchStatement,
|
|
50
|
-
ts.SyntaxKind.TryStatement,
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Extract all metrics from the source file
|
|
55
|
-
*/
|
|
56
|
-
extract(): Metric[] {
|
|
57
|
-
this.metrics = [];
|
|
58
|
-
|
|
59
|
-
// File-level metrics
|
|
60
|
-
this.extractFileMetrics();
|
|
61
|
-
|
|
62
|
-
// Function-level metrics
|
|
63
|
-
this.visit(this.sourceFile);
|
|
64
|
-
|
|
65
|
-
return this.metrics;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Extract file-level metrics
|
|
70
|
-
*/
|
|
71
|
-
private extractFileMetrics(): void {
|
|
72
|
-
const lines = this.sourceFile.getLineStarts();
|
|
73
|
-
const lineCount = lines.length;
|
|
74
|
-
|
|
75
|
-
// File length metric
|
|
76
|
-
this.metrics.push(
|
|
77
|
-
new MetricBuilder()
|
|
78
|
-
.withName('file-length')
|
|
79
|
-
.withValue(lineCount)
|
|
80
|
-
.withFilePath(this.filePath)
|
|
81
|
-
.build()
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// Count TODO/FIXME comments
|
|
85
|
-
const text = this.sourceFile.getFullText();
|
|
86
|
-
const todoCount = (text.match(/\/\/\s*TODO/gi) || []).length +
|
|
87
|
-
(text.match(/\/\*[\s\S]*?TODO[\s\S]*?\*\//gi) || []).length;
|
|
88
|
-
const fixmeCount = (text.match(/\/\/\s*FIXME/gi) || []).length +
|
|
89
|
-
(text.match(/\/\*[\s\S]*?FIXME[\s\S]*?\*\//gi) || []).length;
|
|
90
|
-
|
|
91
|
-
if (todoCount > 0) {
|
|
92
|
-
this.metrics.push(
|
|
93
|
-
new MetricBuilder()
|
|
94
|
-
.withName('todo-comments')
|
|
95
|
-
.withValue(todoCount)
|
|
96
|
-
.withFilePath(this.filePath)
|
|
97
|
-
.build()
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (fixmeCount > 0) {
|
|
102
|
-
this.metrics.push(
|
|
103
|
-
new MetricBuilder()
|
|
104
|
-
.withName('fixme-comments')
|
|
105
|
-
.withValue(fixmeCount)
|
|
106
|
-
.withFilePath(this.filePath)
|
|
107
|
-
.build()
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Visit AST nodes recursively
|
|
114
|
-
*/
|
|
115
|
-
private visit(node: ts.Node): void {
|
|
116
|
-
// Check if this is a function node
|
|
117
|
-
if (this.isFunctionNode(node)) {
|
|
118
|
-
this.extractFunctionMetrics(node as ts.FunctionLikeDeclaration);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Recurse to children
|
|
122
|
-
ts.forEachChild(node, (child) => this.visit(child));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Check if node is a function-like declaration
|
|
127
|
-
*/
|
|
128
|
-
private isFunctionNode(node: ts.Node): boolean {
|
|
129
|
-
return ts.isFunctionDeclaration(node) ||
|
|
130
|
-
ts.isMethodDeclaration(node) ||
|
|
131
|
-
ts.isArrowFunction(node) ||
|
|
132
|
-
ts.isFunctionExpression(node) ||
|
|
133
|
-
ts.isConstructorDeclaration(node);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Extract metrics from a function node
|
|
138
|
-
*/
|
|
139
|
-
private extractFunctionMetrics(node: ts.FunctionLikeDeclaration): void {
|
|
140
|
-
const functionName = this.getFunctionName(node);
|
|
141
|
-
const location = this.getLocation(node);
|
|
142
|
-
|
|
143
|
-
// Function length
|
|
144
|
-
const startLine = this.sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
145
|
-
const endLine = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
146
|
-
const functionLength = endLine - startLine + 1;
|
|
147
|
-
|
|
148
|
-
this.metrics.push(
|
|
149
|
-
new MetricBuilder()
|
|
150
|
-
.withName('function-length')
|
|
151
|
-
.withValue(functionLength)
|
|
152
|
-
.withFilePath(this.filePath)
|
|
153
|
-
.withContext(functionName)
|
|
154
|
-
.withLocation(location)
|
|
155
|
-
.build()
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// Parameter count
|
|
159
|
-
const paramCount = node.parameters.length;
|
|
160
|
-
if (paramCount > 0) {
|
|
161
|
-
this.metrics.push(
|
|
162
|
-
new MetricBuilder()
|
|
163
|
-
.withName('parameter-count')
|
|
164
|
-
.withValue(paramCount)
|
|
165
|
-
.withFilePath(this.filePath)
|
|
166
|
-
.withContext(functionName)
|
|
167
|
-
.withLocation(location)
|
|
168
|
-
.build()
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Cyclomatic complexity
|
|
173
|
-
const complexity = this.calculateComplexity(node);
|
|
174
|
-
this.metrics.push(
|
|
175
|
-
new MetricBuilder()
|
|
176
|
-
.withName('cyclomatic-complexity')
|
|
177
|
-
.withValue(complexity)
|
|
178
|
-
.withFilePath(this.filePath)
|
|
179
|
-
.withContext(functionName)
|
|
180
|
-
.withLocation(location)
|
|
181
|
-
.build()
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
// Nesting depth
|
|
185
|
-
const maxDepth = this.calculateNestingDepth(node);
|
|
186
|
-
if (maxDepth > 0) {
|
|
187
|
-
this.metrics.push(
|
|
188
|
-
new MetricBuilder()
|
|
189
|
-
.withName('nesting-depth')
|
|
190
|
-
.withValue(maxDepth)
|
|
191
|
-
.withFilePath(this.filePath)
|
|
192
|
-
.withContext(functionName)
|
|
193
|
-
.withLocation(location)
|
|
194
|
-
.build()
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Count 'any' usage in function signature
|
|
199
|
-
const anyCount = this.countAnyUsage(node);
|
|
200
|
-
if (anyCount > 0) {
|
|
201
|
-
this.metrics.push(
|
|
202
|
-
new MetricBuilder()
|
|
203
|
-
.withName('any-usage')
|
|
204
|
-
.withValue(anyCount)
|
|
205
|
-
.withFilePath(this.filePath)
|
|
206
|
-
.withContext(functionName)
|
|
207
|
-
.withLocation(location)
|
|
208
|
-
.build()
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Calculate cyclomatic complexity
|
|
215
|
-
* Formula: Number of decision points + 1
|
|
216
|
-
*/
|
|
217
|
-
private calculateComplexity(node: ts.Node): number {
|
|
218
|
-
let complexity = 1; // Base complexity
|
|
219
|
-
|
|
220
|
-
const countDecisionPoints = (n: ts.Node): void => {
|
|
221
|
-
if (MetricExtractor.COMPLEXITY_KINDS.has(n.kind)) {
|
|
222
|
-
complexity++;
|
|
223
|
-
} else if (ts.isBinaryExpression(n)) {
|
|
224
|
-
// Logical operators && and ||
|
|
225
|
-
const op = n.operatorToken.kind;
|
|
226
|
-
if (op === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
227
|
-
op === ts.SyntaxKind.BarBarToken) {
|
|
228
|
-
complexity++;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
ts.forEachChild(n, countDecisionPoints);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
countDecisionPoints(node);
|
|
236
|
-
return complexity;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Calculate maximum nesting depth
|
|
241
|
-
*/
|
|
242
|
-
private calculateNestingDepth(node: ts.Node): number {
|
|
243
|
-
let maxDepth = 0;
|
|
244
|
-
|
|
245
|
-
const traverse = (n: ts.Node, depth: number): void => {
|
|
246
|
-
let currentDepth = depth;
|
|
247
|
-
|
|
248
|
-
// Increment depth for nesting constructs using the static registry
|
|
249
|
-
if (MetricExtractor.NESTING_KINDS.has(n.kind)) {
|
|
250
|
-
currentDepth++;
|
|
251
|
-
maxDepth = Math.max(maxDepth, currentDepth);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
ts.forEachChild(n, (child) => traverse(child, currentDepth));
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
traverse(node, 0);
|
|
258
|
-
return maxDepth;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Count 'any' type usage in function signature
|
|
263
|
-
*/
|
|
264
|
-
private countAnyUsage(node: ts.FunctionLikeDeclaration): number {
|
|
265
|
-
let count = 0;
|
|
266
|
-
|
|
267
|
-
// Check parameters
|
|
268
|
-
for (const param of node.parameters) {
|
|
269
|
-
if (param.type && this.isAnyType(param.type)) {
|
|
270
|
-
count++;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Check return type
|
|
275
|
-
if (node.type && this.isAnyType(node.type)) {
|
|
276
|
-
count++;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return count;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Check if a type node represents 'any'
|
|
284
|
-
*/
|
|
285
|
-
private isAnyType(typeNode: ts.TypeNode): boolean {
|
|
286
|
-
return typeNode.kind === ts.SyntaxKind.AnyKeyword;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Get function name for context
|
|
291
|
-
*/
|
|
292
|
-
private getFunctionName(node: ts.FunctionLikeDeclaration): string {
|
|
293
|
-
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
294
|
-
return node.name.getText();
|
|
295
|
-
} else if (ts.isMethodDeclaration(node) && node.name) {
|
|
296
|
-
return node.name.getText();
|
|
297
|
-
} else if (ts.isConstructorDeclaration(node)) {
|
|
298
|
-
return 'constructor';
|
|
299
|
-
}
|
|
300
|
-
return 'anonymous function';
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Get source location from node
|
|
305
|
-
*/
|
|
306
|
-
private getLocation(node: ts.Node): SourceLocation {
|
|
307
|
-
const start = this.sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
308
|
-
const end = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
startLine: start.line + 1,
|
|
312
|
-
endLine: end.line + 1,
|
|
313
|
-
startColumn: start.character,
|
|
314
|
-
endColumn: end.character,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* TypeScript Parser using native TypeScript Compiler API
|
|
321
|
-
*/
|
|
322
|
-
export class TypeScriptParser implements IParser {
|
|
323
|
-
supports(filePath: string): boolean {
|
|
324
|
-
return filePath.endsWith('.ts') ||
|
|
325
|
-
filePath.endsWith('.tsx') ||
|
|
326
|
-
filePath.endsWith('.js') ||
|
|
327
|
-
filePath.endsWith('.jsx');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async parse(filePath: string, content: string): Promise<ParseResult> {
|
|
331
|
-
try {
|
|
332
|
-
// Create source file from content
|
|
333
|
-
const sourceFile = ts.createSourceFile(
|
|
334
|
-
filePath,
|
|
335
|
-
content,
|
|
336
|
-
ts.ScriptTarget.Latest,
|
|
337
|
-
true, // setParentNodes
|
|
338
|
-
this.getScriptKind(filePath)
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
// Extract metrics using AST traversal
|
|
342
|
-
const extractor = new MetricExtractor(sourceFile, filePath);
|
|
343
|
-
const metrics = extractor.extract();
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
metrics,
|
|
347
|
-
success: true,
|
|
348
|
-
};
|
|
349
|
-
} catch (error) {
|
|
350
|
-
return {
|
|
351
|
-
metrics: [],
|
|
352
|
-
success: false,
|
|
353
|
-
error: error instanceof Error ? error.message : 'Unknown parsing error',
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Determine script kind from file extension
|
|
360
|
-
*/
|
|
361
|
-
private getScriptKind(filePath: string): ts.ScriptKind {
|
|
362
|
-
if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX;
|
|
363
|
-
if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX;
|
|
364
|
-
if (filePath.endsWith('.ts')) return ts.ScriptKind.TS;
|
|
365
|
-
return ts.ScriptKind.JS;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output Adapter: JSON Exporter
|
|
3
|
-
* Implements IReporter for JSON file output
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { writeFile } from 'node:fs/promises';
|
|
7
|
-
import type { IReporter, AnalysisReport } from '../../application/ports/IReporter.js';
|
|
8
|
-
|
|
9
|
-
export class JsonExporter implements IReporter {
|
|
10
|
-
constructor(private readonly outputPath: string = './tech-debt-report.json') {}
|
|
11
|
-
|
|
12
|
-
async generate(report: AnalysisReport): Promise<void> {
|
|
13
|
-
const jsonReport = {
|
|
14
|
-
version: '1.0.0',
|
|
15
|
-
generatedAt: report.metadata.timestamp.toISOString(),
|
|
16
|
-
score: {
|
|
17
|
-
overall: report.score.overall,
|
|
18
|
-
categories: report.score.categories.map(cat => ({
|
|
19
|
-
name: cat.name,
|
|
20
|
-
score: cat.score,
|
|
21
|
-
weight: cat.weight,
|
|
22
|
-
description: cat.description,
|
|
23
|
-
})),
|
|
24
|
-
},
|
|
25
|
-
findings: report.findings.map(finding => ({
|
|
26
|
-
ruleId: finding.ruleId,
|
|
27
|
-
severity: finding.severity,
|
|
28
|
-
message: finding.message,
|
|
29
|
-
filePath: finding.filePath,
|
|
30
|
-
location: finding.location ? {
|
|
31
|
-
startLine: finding.location.startLine,
|
|
32
|
-
endLine: finding.location.endLine,
|
|
33
|
-
startColumn: finding.location.startColumn,
|
|
34
|
-
endColumn: finding.location.endColumn,
|
|
35
|
-
} : undefined,
|
|
36
|
-
suggestion: finding.suggestion,
|
|
37
|
-
})),
|
|
38
|
-
metadata: {
|
|
39
|
-
filesAnalyzed: report.metadata.filesAnalyzed,
|
|
40
|
-
durationMs: report.metadata.duration,
|
|
41
|
-
timestamp: report.metadata.timestamp.toISOString(),
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
await writeFile(this.outputPath, JSON.stringify(jsonReport, null, 2), 'utf-8');
|
|
46
|
-
console.log(`💾 JSON report saved to: ${this.outputPath}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output Adapter: Terminal Reporter
|
|
3
|
-
* Implements IReporter for console output
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IReporter, AnalysisReport } from '../../application/ports/IReporter.js';
|
|
7
|
-
|
|
8
|
-
export class TerminalReporter implements IReporter {
|
|
9
|
-
async generate(report: AnalysisReport): Promise<void> {
|
|
10
|
-
console.log('\n');
|
|
11
|
-
console.log('═'.repeat(60));
|
|
12
|
-
console.log(' TECHNICAL DEBT SCORE REPORT');
|
|
13
|
-
console.log('═'.repeat(60));
|
|
14
|
-
console.log('\n');
|
|
15
|
-
|
|
16
|
-
// Overall Score
|
|
17
|
-
const scoreColor = this.getScoreColor(report.score.overall);
|
|
18
|
-
console.log(`📊 Overall Score: ${scoreColor}${report.score.overall.toFixed(1)}/100${this.reset()}`);
|
|
19
|
-
console.log(this.getScoreLabel(report.score.overall));
|
|
20
|
-
console.log('\n');
|
|
21
|
-
|
|
22
|
-
// Category Breakdown
|
|
23
|
-
console.log('📈 Category Breakdown:');
|
|
24
|
-
console.log('─'.repeat(60));
|
|
25
|
-
for (const category of report.score.categories) {
|
|
26
|
-
const bar = this.createBar(category.score);
|
|
27
|
-
const weight = (category.weight * 100).toFixed(0);
|
|
28
|
-
console.log(` ${category.name.padEnd(20)} ${bar} ${category.score.toFixed(1)} (${weight}%)`);
|
|
29
|
-
}
|
|
30
|
-
console.log('\n');
|
|
31
|
-
|
|
32
|
-
// Summary Statistics
|
|
33
|
-
console.log('📋 Analysis Summary:');
|
|
34
|
-
console.log('─'.repeat(60));
|
|
35
|
-
console.log(` Files Analyzed: ${report.metadata.filesAnalyzed}`);
|
|
36
|
-
console.log(` Total Findings: ${report.findings.length}`);
|
|
37
|
-
console.log(` Duration: ${(report.metadata.duration / 1000).toFixed(2)}s`);
|
|
38
|
-
console.log(` Timestamp: ${report.metadata.timestamp.toISOString()}`);
|
|
39
|
-
console.log('\n');
|
|
40
|
-
|
|
41
|
-
// Top Issues
|
|
42
|
-
if (report.findings.length > 0) {
|
|
43
|
-
console.log('⚠️ Top Issues:');
|
|
44
|
-
console.log('─'.repeat(60));
|
|
45
|
-
const topFindings = report.findings
|
|
46
|
-
.sort((a, b) => {
|
|
47
|
-
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
48
|
-
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
49
|
-
})
|
|
50
|
-
.slice(0, 10);
|
|
51
|
-
|
|
52
|
-
for (const finding of topFindings) {
|
|
53
|
-
const icon = finding.severity === 'high' ? '🔴' : finding.severity === 'medium' ? '🟡' : '🟢';
|
|
54
|
-
console.log(` ${icon} ${finding.message}`);
|
|
55
|
-
console.log(` ${finding.filePath}${finding.location ? `:${finding.location.startLine}` : ''}`);
|
|
56
|
-
}
|
|
57
|
-
console.log('\n');
|
|
58
|
-
} else if (report.metadata.filesAnalyzed > 0) {
|
|
59
|
-
console.log('✨ No issues found! Your codebase looks clean.');
|
|
60
|
-
console.log('\n');
|
|
61
|
-
} else {
|
|
62
|
-
console.log('❓ No files were analyzed. Check your configuration or directory.');
|
|
63
|
-
console.log('\n');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.log('═'.repeat(60));
|
|
67
|
-
console.log('\n');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private createBar(score: number, length = 20): string {
|
|
71
|
-
const filled = Math.round((score / 100) * length);
|
|
72
|
-
const empty = length - filled;
|
|
73
|
-
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private getScoreColor(score: number): string {
|
|
77
|
-
// ANSI color codes (placeholder - could use chalk library)
|
|
78
|
-
if (score >= 80) return '\x1b[32m'; // Green
|
|
79
|
-
if (score >= 60) return '\x1b[33m'; // Yellow
|
|
80
|
-
return '\x1b[31m'; // Red
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private reset(): string {
|
|
84
|
-
return '\x1b[0m';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private getScoreLabel(score: number): string {
|
|
88
|
-
if (score >= 90) return ' ✨ Excellent - Very low technical debt';
|
|
89
|
-
if (score >= 80) return ' ✅ Good - Manageable technical debt';
|
|
90
|
-
if (score >= 60) return ' ⚠️ Fair - Moderate technical debt';
|
|
91
|
-
if (score >= 40) return ' ❗ Poor - High technical debt';
|
|
92
|
-
return ' 🚨 Critical - Very high technical debt';
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Application Configuration
|
|
3
|
-
* Configuration for running code analysis
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface AnalysisConfig {
|
|
7
|
-
/**
|
|
8
|
-
* Root directory to analyze
|
|
9
|
-
*/
|
|
10
|
-
rootPath: string;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* File patterns to include (glob patterns)
|
|
14
|
-
* Default: ['src/**\/*.ts', 'src/**\/*.js']
|
|
15
|
-
*/
|
|
16
|
-
patterns: string[];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Patterns to ignore
|
|
20
|
-
* Default: ['node_modules', 'dist', 'build', 'coverage', '.git']
|
|
21
|
-
*/
|
|
22
|
-
ignore: string[];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Category weights for score calculation
|
|
26
|
-
* Should sum to 1.0
|
|
27
|
-
*/
|
|
28
|
-
weights?: {
|
|
29
|
-
complexity: number;
|
|
30
|
-
size: number;
|
|
31
|
-
typeSafety: number;
|
|
32
|
-
codeQuality: number;
|
|
33
|
-
structure: number;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Default configuration values
|
|
39
|
-
*/
|
|
40
|
-
export const DEFAULT_CONFIG: Omit<AnalysisConfig, 'rootPath'> = {
|
|
41
|
-
patterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
42
|
-
ignore: [
|
|
43
|
-
'**/node_modules/**',
|
|
44
|
-
'**/dist/**',
|
|
45
|
-
'**/build/**',
|
|
46
|
-
'**/coverage/**',
|
|
47
|
-
'**/.git/**',
|
|
48
|
-
'**/.next/**',
|
|
49
|
-
'**/out/**',
|
|
50
|
-
],
|
|
51
|
-
weights: {
|
|
52
|
-
complexity: 0.30,
|
|
53
|
-
size: 0.25,
|
|
54
|
-
typeSafety: 0.20,
|
|
55
|
-
codeQuality: 0.15,
|
|
56
|
-
structure: 0.10,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Application Port: File Reader Interface
|
|
3
|
-
* Input adapter interface for reading files from the filesystem
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { FileMetadata } from '../../shared/types.js';
|
|
7
|
-
|
|
8
|
-
export interface FileReadResult {
|
|
9
|
-
content: string;
|
|
10
|
-
metadata: FileMetadata;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface IFileReader {
|
|
14
|
-
/**
|
|
15
|
-
* Scan a directory and return matching file paths
|
|
16
|
-
*
|
|
17
|
-
* @param rootPath - Root directory to scan
|
|
18
|
-
* @param patterns - Glob patterns to match (e.g., ['src/**\/*.ts'])
|
|
19
|
-
* @param ignore - Patterns to ignore (e.g., ['node_modules', 'dist'])
|
|
20
|
-
*/
|
|
21
|
-
scan(rootPath: string, patterns: string[], ignore: string[]): Promise<string[]>;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Read a single file and return its content
|
|
25
|
-
*
|
|
26
|
-
* @param filePath - Absolute path to the file
|
|
27
|
-
*/
|
|
28
|
-
read(filePath: string): Promise<FileReadResult>;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Read multiple files in batch
|
|
32
|
-
*
|
|
33
|
-
* @param filePaths - Array of absolute file paths
|
|
34
|
-
*/
|
|
35
|
-
readBatch(filePaths: string[]): Promise<FileReadResult[]>;
|
|
36
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Application Port: Parser Interface
|
|
3
|
-
* Input adapter interface for parsing source code into AST and extracting metrics
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Metric } from '../../domain/entities/Metric.js';
|
|
7
|
-
|
|
8
|
-
export interface ParseResult {
|
|
9
|
-
/**
|
|
10
|
-
* Array of metrics extracted from the source code
|
|
11
|
-
*/
|
|
12
|
-
metrics: Metric[];
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Indicates if parsing was successful
|
|
16
|
-
*/
|
|
17
|
-
success: boolean;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Error message if parsing failed
|
|
21
|
-
*/
|
|
22
|
-
error?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface IParser {
|
|
26
|
-
/**
|
|
27
|
-
* Parse source code and extract metrics
|
|
28
|
-
*
|
|
29
|
-
* @param filePath - Path to the file being parsed
|
|
30
|
-
* @param content - Source code content
|
|
31
|
-
*/
|
|
32
|
-
parse(filePath: string, content: string): Promise<ParseResult>;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Check if this parser supports a given file
|
|
36
|
-
*
|
|
37
|
-
* @param filePath - Path to check
|
|
38
|
-
*/
|
|
39
|
-
supports(filePath: string): boolean;
|
|
40
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Application Port: Reporter Interface
|
|
3
|
-
* Output adapter interface for reporting analysis results
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Score } from '../../domain/entities/Score.js';
|
|
7
|
-
import type { Finding } from '../../domain/entities/Finding.js';
|
|
8
|
-
|
|
9
|
-
export interface AnalysisReport {
|
|
10
|
-
score: Score;
|
|
11
|
-
findings: Finding[];
|
|
12
|
-
metadata: {
|
|
13
|
-
filesAnalyzed: number;
|
|
14
|
-
duration: number;
|
|
15
|
-
timestamp: Date;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface IReporter {
|
|
20
|
-
/**
|
|
21
|
-
* Generate and output a report
|
|
22
|
-
*
|
|
23
|
-
* @param report - The analysis report data
|
|
24
|
-
*/
|
|
25
|
-
generate(report: AnalysisReport): Promise<void>;
|
|
26
|
-
}
|