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,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Application Service: Analysis Service
|
|
3
|
-
* Orchestrates the entire code analysis workflow
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { IFileReader } from '../ports/IFileReader.js';
|
|
7
|
-
import type { IParser } from '../ports/IParser.js';
|
|
8
|
-
import type { IReporter, AnalysisReport } from '../ports/IReporter.js';
|
|
9
|
-
import type { AnalysisConfig } from '../config/AnalysisConfig.js';
|
|
10
|
-
import type { Rule } from '../../domain/entities/Rule.js';
|
|
11
|
-
import type { Metric } from '../../domain/entities/Metric.js';
|
|
12
|
-
import type { Finding } from '../../domain/entities/Finding.js';
|
|
13
|
-
import type { CategoryScore, Score } from '../../domain/entities/Score.js';
|
|
14
|
-
import { ScoreCalculator } from '../../domain/entities/Score.js';
|
|
15
|
-
|
|
16
|
-
export class AnalysisService {
|
|
17
|
-
constructor(
|
|
18
|
-
private readonly fileReader: IFileReader,
|
|
19
|
-
private readonly parser: IParser,
|
|
20
|
-
private readonly rules: Rule[],
|
|
21
|
-
private readonly reporter: IReporter
|
|
22
|
-
) {}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Run the complete analysis workflow
|
|
26
|
-
*/
|
|
27
|
-
async analyze(config: AnalysisConfig): Promise<AnalysisReport> {
|
|
28
|
-
const startTime = Date.now();
|
|
29
|
-
|
|
30
|
-
// 1. Scan for files
|
|
31
|
-
console.log('📁 Scanning files...');
|
|
32
|
-
const filePaths = await this.fileReader.scan(
|
|
33
|
-
config.rootPath,
|
|
34
|
-
config.patterns,
|
|
35
|
-
config.ignore
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
if (filePaths.length === 0) {
|
|
39
|
-
console.log(' ❌ No files found matching patterns.');
|
|
40
|
-
console.log(` Root: ${config.rootPath}`);
|
|
41
|
-
console.log(` Patterns: ${config.patterns.join(', ')}`);
|
|
42
|
-
console.log(' Check your directory structure and ensure files exist.');
|
|
43
|
-
} else {
|
|
44
|
-
console.log(` Found ${filePaths.length} files`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 2. Read and parse files
|
|
48
|
-
console.log('🔍 Parsing files...');
|
|
49
|
-
const allMetrics: Metric[] = [];
|
|
50
|
-
const fileContents = new Map<string, string>();
|
|
51
|
-
|
|
52
|
-
let supportedFilesCount = 0;
|
|
53
|
-
for (const filePath of filePaths) {
|
|
54
|
-
try {
|
|
55
|
-
const fileResult = await this.fileReader.read(filePath);
|
|
56
|
-
fileContents.set(filePath, fileResult.content);
|
|
57
|
-
|
|
58
|
-
if (this.parser.supports(filePath)) {
|
|
59
|
-
supportedFilesCount++;
|
|
60
|
-
const parseResult = await this.parser.parse(filePath, fileResult.content);
|
|
61
|
-
if (parseResult.success) {
|
|
62
|
-
allMetrics.push(...parseResult.metrics);
|
|
63
|
-
} else {
|
|
64
|
-
console.warn(` ⚠️ Failed to parse ${filePath}: ${parseResult.error}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.warn(` ⚠️ Error reading ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (filePaths.length > 0 && supportedFilesCount === 0) {
|
|
73
|
-
console.log(' ⚠️ None of the found files are supported by the parser (.ts, .js, etc)');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log(` Extracted ${allMetrics.length} metrics from ${supportedFilesCount} files`);
|
|
77
|
-
|
|
78
|
-
// 3. Analyze dependencies and duplication
|
|
79
|
-
console.log('🔗 Analyzing dependencies and duplication...');
|
|
80
|
-
const additionalMetrics = await this.analyzeAdvancedMetrics(fileContents);
|
|
81
|
-
allMetrics.push(...additionalMetrics);
|
|
82
|
-
console.log(` Added ${additionalMetrics.length} advanced metrics`);
|
|
83
|
-
|
|
84
|
-
// 4. Apply rules and collect findings
|
|
85
|
-
console.log('📊 Applying rules...');
|
|
86
|
-
const allFindings: Finding[] = [];
|
|
87
|
-
const rawCategoryScores: CategoryScore[] = [];
|
|
88
|
-
|
|
89
|
-
for (const rule of this.rules) {
|
|
90
|
-
const findings = rule.evaluate(allMetrics);
|
|
91
|
-
allFindings.push(...findings);
|
|
92
|
-
|
|
93
|
-
const score = rule.calculateScore(findings);
|
|
94
|
-
rawCategoryScores.push({
|
|
95
|
-
name: rule.name,
|
|
96
|
-
score,
|
|
97
|
-
weight: this.getCategoryWeight(rule.id, config),
|
|
98
|
-
description: rule.description,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
console.log(` Generated ${allFindings.length} findings`);
|
|
102
|
-
|
|
103
|
-
// Normalize weights to sum to 1.0 (in case not all categories are active)
|
|
104
|
-
const categoryScores = this.normalizeWeights(rawCategoryScores);
|
|
105
|
-
|
|
106
|
-
// 5. Calculate overall score
|
|
107
|
-
console.log('🎯 Calculating score...');
|
|
108
|
-
const overallScore = ScoreCalculator.calculateOverall(categoryScores);
|
|
109
|
-
|
|
110
|
-
const finalScore: Score = {
|
|
111
|
-
overall: overallScore,
|
|
112
|
-
categories: categoryScores,
|
|
113
|
-
timestamp: new Date(),
|
|
114
|
-
metadata: {
|
|
115
|
-
filesAnalyzed: filePaths.length,
|
|
116
|
-
totalMetrics: allMetrics.length,
|
|
117
|
-
totalFindings: allFindings.length,
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// 6. Prepare report
|
|
122
|
-
const duration = Date.now() - startTime;
|
|
123
|
-
const report: AnalysisReport = {
|
|
124
|
-
score: finalScore,
|
|
125
|
-
findings: allFindings,
|
|
126
|
-
metadata: {
|
|
127
|
-
filesAnalyzed: filePaths.length,
|
|
128
|
-
duration,
|
|
129
|
-
timestamp: new Date(),
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
// 7. Generate output
|
|
134
|
-
console.log('📋 Generating report...');
|
|
135
|
-
await this.reporter.generate(report);
|
|
136
|
-
|
|
137
|
-
return report;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Analyze dependencies and code duplication
|
|
142
|
-
*/
|
|
143
|
-
private async analyzeAdvancedMetrics(fileContents: Map<string, string>): Promise<Metric[]> {
|
|
144
|
-
const { DependencyAnalyzer } = await import('./DependencyAnalyzer.js');
|
|
145
|
-
const { DuplicationDetector } = await import('./DuplicationDetector.js');
|
|
146
|
-
|
|
147
|
-
const depAnalyzer = new DependencyAnalyzer();
|
|
148
|
-
const dupDetector = new DuplicationDetector();
|
|
149
|
-
const metrics: Metric[] = [];
|
|
150
|
-
|
|
151
|
-
// Analyze all files
|
|
152
|
-
for (const [filePath, content] of fileContents.entries()) {
|
|
153
|
-
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx') ||
|
|
154
|
-
filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
|
|
155
|
-
depAnalyzer.analyzeDependencies(filePath, content);
|
|
156
|
-
dupDetector.analyzeFile(filePath, content);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Detect circular dependencies
|
|
161
|
-
const circularDeps = depAnalyzer.detectCircularDependencies();
|
|
162
|
-
metrics.push(...circularDeps);
|
|
163
|
-
|
|
164
|
-
// Detect code duplication
|
|
165
|
-
const duplicates = dupDetector.detectDuplicates();
|
|
166
|
-
metrics.push(...duplicates);
|
|
167
|
-
|
|
168
|
-
return metrics;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private getCategoryWeight(ruleId: string, config: AnalysisConfig): number {
|
|
172
|
-
const weights = config.weights ?? {
|
|
173
|
-
complexity: 0.30,
|
|
174
|
-
size: 0.25,
|
|
175
|
-
typeSafety: 0.20,
|
|
176
|
-
codeQuality: 0.15,
|
|
177
|
-
structure: 0.10,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
switch (ruleId) {
|
|
181
|
-
case 'complexity':
|
|
182
|
-
return weights.complexity;
|
|
183
|
-
case 'size':
|
|
184
|
-
return weights.size;
|
|
185
|
-
case 'type-safety':
|
|
186
|
-
return weights.typeSafety;
|
|
187
|
-
case 'duplication':
|
|
188
|
-
return weights.codeQuality;
|
|
189
|
-
case 'circular-dependency':
|
|
190
|
-
return weights.structure;
|
|
191
|
-
default:
|
|
192
|
-
return 0.05; // Default weight for unknown categories
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Normalize weights so they sum to 1.0
|
|
198
|
-
* This handles cases where not all categories are active
|
|
199
|
-
*/
|
|
200
|
-
private normalizeWeights(categories: CategoryScore[]): CategoryScore[] {
|
|
201
|
-
const totalWeight = categories.reduce((sum, cat) => sum + cat.weight, 0);
|
|
202
|
-
|
|
203
|
-
if (totalWeight === 0) {
|
|
204
|
-
// If all weights are 0, distribute evenly
|
|
205
|
-
const evenWeight = 1.0 / categories.length;
|
|
206
|
-
return categories.map(cat => ({
|
|
207
|
-
...cat,
|
|
208
|
-
weight: evenWeight,
|
|
209
|
-
}));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Normalize proportionally
|
|
213
|
-
return categories.map(cat => ({
|
|
214
|
-
...cat,
|
|
215
|
-
weight: cat.weight / totalWeight,
|
|
216
|
-
}));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Application Service: Dependency Analyzer
|
|
3
|
-
* Analyzes import/require statements to detect circular dependencies
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as ts from 'typescript';
|
|
7
|
-
import { resolve, dirname, join } from 'node:path';
|
|
8
|
-
import type { Metric } from '../../domain/entities/Metric.js';
|
|
9
|
-
import { MetricBuilder } from '../../domain/entities/Metric.js';
|
|
10
|
-
|
|
11
|
-
interface DependencyNode {
|
|
12
|
-
filePath: string;
|
|
13
|
-
imports: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class DependencyAnalyzer {
|
|
17
|
-
private dependencyGraph: Map<string, DependencyNode> = new Map();
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Analyze files for import statements and build dependency graph
|
|
21
|
-
*/
|
|
22
|
-
analyzeDependencies(filePath: string, content: string): void {
|
|
23
|
-
const sourceFile = ts.createSourceFile(
|
|
24
|
-
filePath,
|
|
25
|
-
content,
|
|
26
|
-
ts.ScriptTarget.Latest,
|
|
27
|
-
true
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const imports: string[] = [];
|
|
31
|
-
|
|
32
|
-
// Extract import statements
|
|
33
|
-
const visit = (node: ts.Node): void => {
|
|
34
|
-
if (ts.isImportDeclaration(node)) {
|
|
35
|
-
const moduleSpecifier = node.moduleSpecifier;
|
|
36
|
-
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
37
|
-
const importPath = this.resolveImportPath(filePath, moduleSpecifier.text);
|
|
38
|
-
if (importPath) {
|
|
39
|
-
imports.push(importPath);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
ts.forEachChild(node, visit);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
visit(sourceFile);
|
|
47
|
-
|
|
48
|
-
this.dependencyGraph.set(filePath, {
|
|
49
|
-
filePath,
|
|
50
|
-
imports,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Detect circular dependencies using DFS
|
|
56
|
-
*/
|
|
57
|
-
detectCircularDependencies(): Metric[] {
|
|
58
|
-
const metrics: Metric[] = [];
|
|
59
|
-
const visited = new Set<string>();
|
|
60
|
-
const recursionStack = new Set<string>();
|
|
61
|
-
const cycles = new Set<string>(); // Track unique cycles
|
|
62
|
-
|
|
63
|
-
const dfs = (filePath: string, path: string[]): void => {
|
|
64
|
-
if (cycles.has(filePath)) return; // Already found this cycle
|
|
65
|
-
|
|
66
|
-
visited.add(filePath);
|
|
67
|
-
recursionStack.add(filePath);
|
|
68
|
-
|
|
69
|
-
const node = this.dependencyGraph.get(filePath);
|
|
70
|
-
if (!node) {
|
|
71
|
-
recursionStack.delete(filePath);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const importPath of node.imports) {
|
|
76
|
-
if (!visited.has(importPath)) {
|
|
77
|
-
dfs(importPath, [...path, filePath]);
|
|
78
|
-
} else if (recursionStack.has(importPath)) {
|
|
79
|
-
// Found a cycle!
|
|
80
|
-
const cycleStart = path.indexOf(importPath);
|
|
81
|
-
if (cycleStart !== -1) {
|
|
82
|
-
const cycle = [...path.slice(cycleStart), importPath];
|
|
83
|
-
const cycleKey = [...cycle].sort().join('->');
|
|
84
|
-
|
|
85
|
-
if (!cycles.has(cycleKey)) {
|
|
86
|
-
cycles.add(cycleKey);
|
|
87
|
-
|
|
88
|
-
// Create metric for this cycle
|
|
89
|
-
metrics.push(
|
|
90
|
-
new MetricBuilder()
|
|
91
|
-
.withName('circular-dependency')
|
|
92
|
-
.withValue(cycle.length)
|
|
93
|
-
.withFilePath(filePath)
|
|
94
|
-
.withContext(cycle.map(p => this.getFileName(p)).join(' → '))
|
|
95
|
-
.build()
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
recursionStack.delete(filePath);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Check all nodes for cycles
|
|
106
|
-
for (const filePath of this.dependencyGraph.keys()) {
|
|
107
|
-
if (!visited.has(filePath)) {
|
|
108
|
-
dfs(filePath, []);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return metrics;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Resolve relative import to absolute path
|
|
117
|
-
*/
|
|
118
|
-
private resolveImportPath(fromFile: string, importSpecifier: string): string | null {
|
|
119
|
-
// Skip node_modules and external packages
|
|
120
|
-
if (!importSpecifier.startsWith('.')) {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const dir = dirname(fromFile);
|
|
126
|
-
let resolvedPath = resolve(dir, importSpecifier);
|
|
127
|
-
|
|
128
|
-
// Try adding extensions if file doesn't exist
|
|
129
|
-
const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js'];
|
|
130
|
-
for (const ext of extensions) {
|
|
131
|
-
const pathWithExt = resolvedPath + ext;
|
|
132
|
-
// We can't check if file exists in this context, so just normalize
|
|
133
|
-
if (ext.startsWith('/')) {
|
|
134
|
-
return resolvedPath + ext;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return resolvedPath + '.ts'; // Default to .ts
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get file name from path for display
|
|
146
|
-
*/
|
|
147
|
-
private getFileName(filePath: string): string {
|
|
148
|
-
const parts = filePath.split('/');
|
|
149
|
-
return parts[parts.length - 1] ?? filePath;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Clear the dependency graph (useful for new analysis)
|
|
154
|
-
*/
|
|
155
|
-
clear(): void {
|
|
156
|
-
this.dependencyGraph.clear();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Application Service: Code Duplication Detector
|
|
3
|
-
* Detects duplicate code blocks using token-based similarity
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as ts from 'typescript';
|
|
7
|
-
import type { Metric } from '../../domain/entities/Metric.js';
|
|
8
|
-
import { MetricBuilder } from '../../domain/entities/Metric.js';
|
|
9
|
-
|
|
10
|
-
interface CodeBlock {
|
|
11
|
-
filePath: string;
|
|
12
|
-
tokens: string;
|
|
13
|
-
startLine: number;
|
|
14
|
-
endLine: number;
|
|
15
|
-
functionName?: string | undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class DuplicationDetector {
|
|
19
|
-
private codeBlocks: CodeBlock[] = [];
|
|
20
|
-
private readonly MIN_BLOCK_SIZE = 50; // Minimum token count
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Analyze file for code blocks
|
|
24
|
-
*/
|
|
25
|
-
analyzeFile(filePath: string, content: string): void {
|
|
26
|
-
const sourceFile = ts.createSourceFile(
|
|
27
|
-
filePath,
|
|
28
|
-
content,
|
|
29
|
-
ts.ScriptTarget.Latest,
|
|
30
|
-
true
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// Extract function bodies as code blocks
|
|
34
|
-
const visit = (node: ts.Node): void => {
|
|
35
|
-
if (this.isFunctionNode(node)) {
|
|
36
|
-
const block = this.extractCodeBlock(sourceFile, filePath, node);
|
|
37
|
-
if (block && block.tokens.length >= this.MIN_BLOCK_SIZE) {
|
|
38
|
-
this.codeBlocks.push(block);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
ts.forEachChild(node, visit);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
visit(sourceFile);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Detect duplicates across all analyzed files
|
|
49
|
-
*/
|
|
50
|
-
detectDuplicates(): Metric[] {
|
|
51
|
-
const metrics: Metric[] = [];
|
|
52
|
-
const seen = new Set<number>();
|
|
53
|
-
|
|
54
|
-
for (let i = 0; i < this.codeBlocks.length; i++) {
|
|
55
|
-
if (seen.has(i)) continue;
|
|
56
|
-
|
|
57
|
-
const block1 = this.codeBlocks[i];
|
|
58
|
-
if (!block1) continue;
|
|
59
|
-
|
|
60
|
-
let duplicateCount = 0;
|
|
61
|
-
|
|
62
|
-
for (let j = i + 1; j < this.codeBlocks.length; j++) {
|
|
63
|
-
if (seen.has(j)) continue;
|
|
64
|
-
|
|
65
|
-
const block2 = this.codeBlocks[j];
|
|
66
|
-
if (!block2) continue;
|
|
67
|
-
|
|
68
|
-
const similarity = this.calculateSimilarity(block1.tokens, block2.tokens);
|
|
69
|
-
|
|
70
|
-
if (similarity >= 0.85) { // 85% similarity threshold
|
|
71
|
-
duplicateCount++;
|
|
72
|
-
seen.add(j);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (duplicateCount > 0) {
|
|
77
|
-
metrics.push(
|
|
78
|
-
new MetricBuilder()
|
|
79
|
-
.withName('code-duplication')
|
|
80
|
-
.withValue(duplicateCount)
|
|
81
|
-
.withFilePath(block1.filePath)
|
|
82
|
-
.withContext(block1.functionName || 'code block')
|
|
83
|
-
.withLocation({
|
|
84
|
-
startLine: block1.startLine,
|
|
85
|
-
endLine: block1.endLine,
|
|
86
|
-
startColumn: 0,
|
|
87
|
-
endColumn: 0,
|
|
88
|
-
})
|
|
89
|
-
.build()
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return metrics;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Extract code block from function node
|
|
99
|
-
*/
|
|
100
|
-
private extractCodeBlock(
|
|
101
|
-
sourceFile: ts.SourceFile,
|
|
102
|
-
filePath: string,
|
|
103
|
-
node: ts.Node
|
|
104
|
-
): CodeBlock | null {
|
|
105
|
-
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
106
|
-
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
107
|
-
|
|
108
|
-
// Tokenize the code (simple whitespace normalization)
|
|
109
|
-
const text = node.getText(sourceFile);
|
|
110
|
-
const tokens = this.tokenize(text);
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
filePath,
|
|
114
|
-
tokens,
|
|
115
|
-
startLine: start.line + 1,
|
|
116
|
-
endLine: end.line + 1,
|
|
117
|
-
functionName: this.getFunctionName(node),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Simple tokenization (normalize whitespace)
|
|
123
|
-
*/
|
|
124
|
-
private tokenize(code: string): string {
|
|
125
|
-
return code
|
|
126
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
127
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
128
|
-
.replace(/\/\/.*/g, '') // Remove line comments
|
|
129
|
-
.trim();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Calculate similarity between two token strings
|
|
134
|
-
*/
|
|
135
|
-
private calculateSimilarity(tokens1: string, tokens2: string): number {
|
|
136
|
-
if (tokens1 === tokens2) return 1.0;
|
|
137
|
-
|
|
138
|
-
// Use Levenshtein distance for similarity
|
|
139
|
-
const len1 = tokens1.length;
|
|
140
|
-
const len2 = tokens2.length;
|
|
141
|
-
const maxLen = Math.max(len1, len2);
|
|
142
|
-
|
|
143
|
-
if (maxLen === 0) return 1.0;
|
|
144
|
-
|
|
145
|
-
const distance = this.levenshteinDistance(tokens1, tokens2);
|
|
146
|
-
return 1 - distance / maxLen;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Calculate Levenshtein distance
|
|
151
|
-
*/
|
|
152
|
-
private levenshteinDistance(str1: string, str2: string): number {
|
|
153
|
-
const len1 = str1.length;
|
|
154
|
-
const len2 = str2.length;
|
|
155
|
-
const matrix: number[][] = [];
|
|
156
|
-
|
|
157
|
-
for (let i = 0; i <= len1; i++) {
|
|
158
|
-
matrix[i] = [i];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
for (let j = 0; j <= len2; j++) {
|
|
162
|
-
matrix[0]![j] = j;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (let i = 1; i <= len1; i++) {
|
|
166
|
-
for (let j = 1; j <= len2; j++) {
|
|
167
|
-
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
168
|
-
matrix[i]![j] = Math.min(
|
|
169
|
-
matrix[i - 1]![j]! + 1, // deletion
|
|
170
|
-
matrix[i]![j - 1]! + 1, // insertion
|
|
171
|
-
matrix[i - 1]![j - 1]! + cost // substitution
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return matrix[len1]![len2]!;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Check if node is a function
|
|
181
|
-
*/
|
|
182
|
-
private isFunctionNode(node: ts.Node): boolean {
|
|
183
|
-
return ts.isFunctionDeclaration(node) ||
|
|
184
|
-
ts.isMethodDeclaration(node) ||
|
|
185
|
-
ts.isArrowFunction(node) ||
|
|
186
|
-
ts.isFunctionExpression(node);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Get function name for context
|
|
191
|
-
*/
|
|
192
|
-
private getFunctionName(node: ts.Node): string | undefined {
|
|
193
|
-
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
194
|
-
return node.name.getText();
|
|
195
|
-
} else if (ts.isMethodDeclaration(node) && node.name) {
|
|
196
|
-
return node.name.getText();
|
|
197
|
-
}
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Clear stored blocks
|
|
203
|
-
*/
|
|
204
|
-
clear(): void {
|
|
205
|
-
this.codeBlocks = [];
|
|
206
|
-
}
|
|
207
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { AnalysisService } from '../../application/services/AnalysisService.js';
|
|
4
|
-
import { FileSystemReader } from '../../adapters/input/FileSystemReader.js';
|
|
5
|
-
import { TypeScriptParser } from '../../adapters/input/TypeScriptParser.js';
|
|
6
|
-
import { TerminalReporter } from '../../adapters/output/TerminalReporter.js';
|
|
7
|
-
import { JsonExporter } from '../../adapters/output/JsonExporter.js';
|
|
8
|
-
import type { IReporter, AnalysisReport } from '../../application/ports/IReporter.js';
|
|
9
|
-
|
|
10
|
-
import { ComplexityRule } from '../../domain/rules/ComplexityRule.js';
|
|
11
|
-
import { SizeRule } from '../../domain/rules/SizeRule.js';
|
|
12
|
-
import { TypeSafetyRule } from '../../domain/rules/TypeSafetyRule.js';
|
|
13
|
-
import { DuplicationRule } from '../../domain/rules/DuplicationRule.js';
|
|
14
|
-
import { CircularDependencyRule } from '../../domain/rules/CircularDependencyRule.js';
|
|
15
|
-
import { DEFAULT_CONFIG } from '../../application/config/AnalysisConfig.js';
|
|
16
|
-
import type { AnalysisConfig } from '../../application/config/AnalysisConfig.js';
|
|
17
|
-
|
|
18
|
-
// Helper to broadcast to multiple reporters
|
|
19
|
-
class CompositeReporter implements IReporter {
|
|
20
|
-
constructor(private reporters: IReporter[]) {}
|
|
21
|
-
|
|
22
|
-
async generate(report: AnalysisReport): Promise<void> {
|
|
23
|
-
await Promise.all(this.reporters.map(r => r.generate(report)));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function analyzeCommand(rootPath: string, jsonOutputPath?: string): Promise<void> {
|
|
28
|
-
console.log('🚀 Starting technical debt analysis...\n');
|
|
29
|
-
|
|
30
|
-
// Wiring: Create all adapters and services (dependency injection)
|
|
31
|
-
const fileReader = new FileSystemReader();
|
|
32
|
-
const parser = new TypeScriptParser();
|
|
33
|
-
|
|
34
|
-
// Use both terminal and JSON reporters
|
|
35
|
-
const reporter = new CompositeReporter([
|
|
36
|
-
new TerminalReporter(),
|
|
37
|
-
new JsonExporter(jsonOutputPath)
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
const rules = [
|
|
41
|
-
// ... rules
|
|
42
|
-
|
|
43
|
-
new ComplexityRule(),
|
|
44
|
-
new SizeRule(),
|
|
45
|
-
new TypeSafetyRule(),
|
|
46
|
-
new DuplicationRule(),
|
|
47
|
-
new CircularDependencyRule(),
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
const analysisService = new AnalysisService(
|
|
51
|
-
fileReader,
|
|
52
|
-
parser,
|
|
53
|
-
rules,
|
|
54
|
-
reporter
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Build configuration
|
|
58
|
-
// Use broad patterns by default and rely on ignore list for exclusions.
|
|
59
|
-
// This makes the tool structure-agnostic (works for src/, lib/, or root files).
|
|
60
|
-
const defaultPatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'];
|
|
61
|
-
|
|
62
|
-
const config: AnalysisConfig = {
|
|
63
|
-
rootPath,
|
|
64
|
-
...DEFAULT_CONFIG,
|
|
65
|
-
patterns: defaultPatterns,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Log scan start for transparency
|
|
69
|
-
if (config.patterns.length > 0) {
|
|
70
|
-
console.log(`📂 Scanning for patterns: ${config.patterns.join(', ')}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Execute analysis
|
|
74
|
-
await analysisService.analyze(config);
|
|
75
|
-
|
|
76
|
-
console.log('✅ Analysis complete!\n');
|
|
77
|
-
}
|