@loxia-labs/loxia-autopilot-one 1.0.1
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/LICENSE +267 -0
- package/README.md +509 -0
- package/bin/cli.js +117 -0
- package/package.json +94 -0
- package/scripts/install-scanners.js +236 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +266 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +203 -0
- package/src/analyzers/codeCloneDetector/index.js +160 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +59 -0
- package/src/core/agentPool.js +1474 -0
- package/src/core/agentScheduler.js +2147 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/messageProcessor.js +732 -0
- package/src/core/orchestrator.js +548 -0
- package/src/core/stateManager.js +877 -0
- package/src/index.js +631 -0
- package/src/interfaces/cli.js +549 -0
- package/src/interfaces/webServer.js +2162 -0
- package/src/modules/fileExplorer/controller.js +280 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +125 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/aiService.js +1232 -0
- package/src/services/apiKeyManager.js +164 -0
- package/src/services/benchmarkService.js +366 -0
- package/src/services/budgetService.js +539 -0
- package/src/services/contextInjectionService.js +247 -0
- package/src/services/conversationCompactionService.js +637 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +544 -0
- package/src/services/modelRouterService.js +366 -0
- package/src/services/modelsService.js +322 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/tokenCountingService.js +536 -0
- package/src/tools/agentCommunicationTool.js +1344 -0
- package/src/tools/agentDelayTool.js +485 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +800 -0
- package/src/tools/browserTool.js +920 -0
- package/src/tools/cloneDetectionTool.js +621 -0
- package/src/tools/dependencyResolverTool.js +1215 -0
- package/src/tools/fileContentReplaceTool.js +875 -0
- package/src/tools/fileSystemTool.js +1107 -0
- package/src/tools/fileTreeTool.js +853 -0
- package/src/tools/imageTool.js +901 -0
- package/src/tools/importAnalyzerTool.js +1060 -0
- package/src/tools/jobDoneTool.js +248 -0
- package/src/tools/seekTool.js +956 -0
- package/src/tools/staticAnalysisTool.js +1778 -0
- package/src/tools/taskManagerTool.js +2873 -0
- package/src/tools/terminalTool.js +2304 -0
- package/src/tools/webTool.js +1430 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +292 -0
- package/src/utilities/configManager.js +582 -0
- package/src/utilities/constants.js +722 -0
- package/src/utilities/directoryAccessManager.js +535 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/tagParser.js +1246 -0
- package/src/utilities/toolConstants.js +317 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-CjkkcnFA.js +344 -0
- package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects code clones across files
|
|
3
|
+
*/
|
|
4
|
+
export class CloneDetector {
|
|
5
|
+
constructor(config, parser) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.parser = parser;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find all clones in parsed files
|
|
12
|
+
* @param {Array} parsedFiles - Array of parsed file objects
|
|
13
|
+
* @returns {Array} Array of clone groups
|
|
14
|
+
*/
|
|
15
|
+
detectClones(parsedFiles) {
|
|
16
|
+
console.log('Detecting clones...');
|
|
17
|
+
|
|
18
|
+
const clones = [];
|
|
19
|
+
const allBlocks = [];
|
|
20
|
+
|
|
21
|
+
// Collect all code blocks
|
|
22
|
+
for (const file of parsedFiles) {
|
|
23
|
+
allBlocks.push(...file.blocks);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`Analyzing ${allBlocks.length} code blocks`);
|
|
27
|
+
|
|
28
|
+
// Group blocks by hash for exact clones
|
|
29
|
+
const hashGroups = this.groupByHash(allBlocks);
|
|
30
|
+
|
|
31
|
+
// Find exact clones
|
|
32
|
+
for (const [hash, blocks] of Object.entries(hashGroups)) {
|
|
33
|
+
if (blocks.length > 1) {
|
|
34
|
+
clones.push({
|
|
35
|
+
type: 'exact',
|
|
36
|
+
confidence: 1.0,
|
|
37
|
+
blocks,
|
|
38
|
+
tokenCount: blocks[0].tokens.length
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find similar clones (more expensive)
|
|
44
|
+
const similarClones = this.findSimilarClones(allBlocks);
|
|
45
|
+
clones.push(...similarClones);
|
|
46
|
+
|
|
47
|
+
console.log(`Found ${clones.length} clone groups`);
|
|
48
|
+
return clones;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Group blocks by hash
|
|
53
|
+
*/
|
|
54
|
+
groupByHash(blocks) {
|
|
55
|
+
const groups = {};
|
|
56
|
+
|
|
57
|
+
for (const block of blocks) {
|
|
58
|
+
if (block.tokens.length < this.config.minTokens) continue;
|
|
59
|
+
|
|
60
|
+
if (!groups[block.hash]) {
|
|
61
|
+
groups[block.hash] = [];
|
|
62
|
+
}
|
|
63
|
+
groups[block.hash].push(block);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find similar (but not exact) clones
|
|
71
|
+
*/
|
|
72
|
+
findSimilarClones(blocks) {
|
|
73
|
+
const similarClones = [];
|
|
74
|
+
const processed = new Set();
|
|
75
|
+
|
|
76
|
+
// Filter blocks that meet minimum size
|
|
77
|
+
const eligibleBlocks = blocks.filter(b =>
|
|
78
|
+
b.tokens.length >= this.config.minTokens
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < eligibleBlocks.length; i++) {
|
|
82
|
+
const block1 = eligibleBlocks[i];
|
|
83
|
+
const cloneGroup = [block1];
|
|
84
|
+
|
|
85
|
+
if (processed.has(block1.id)) continue;
|
|
86
|
+
|
|
87
|
+
for (let j = i + 1; j < eligibleBlocks.length; j++) {
|
|
88
|
+
const block2 = eligibleBlocks[j];
|
|
89
|
+
|
|
90
|
+
if (processed.has(block2.id)) continue;
|
|
91
|
+
|
|
92
|
+
// Skip if already exact match
|
|
93
|
+
if (block1.hash === block2.hash) continue;
|
|
94
|
+
|
|
95
|
+
// Calculate similarity
|
|
96
|
+
const similarity = this.calculateBlockSimilarity(block1, block2);
|
|
97
|
+
|
|
98
|
+
if (similarity >= this.config.similarityThreshold) {
|
|
99
|
+
cloneGroup.push(block2);
|
|
100
|
+
processed.add(block2.id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (cloneGroup.length > 1) {
|
|
105
|
+
processed.add(block1.id);
|
|
106
|
+
|
|
107
|
+
similarClones.push({
|
|
108
|
+
type: 'similar',
|
|
109
|
+
confidence: this.calculateGroupConfidence(cloneGroup),
|
|
110
|
+
blocks: cloneGroup,
|
|
111
|
+
tokenCount: Math.max(...cloneGroup.map(b => b.tokens.length))
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return similarClones;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Calculate similarity between two blocks
|
|
121
|
+
*/
|
|
122
|
+
calculateBlockSimilarity(block1, block2) {
|
|
123
|
+
const tokens1 = block1.tokens;
|
|
124
|
+
const tokens2 = block2.tokens;
|
|
125
|
+
|
|
126
|
+
// Length similarity
|
|
127
|
+
const lengthRatio = Math.min(tokens1.length, tokens2.length) /
|
|
128
|
+
Math.max(tokens1.length, tokens2.length);
|
|
129
|
+
|
|
130
|
+
if (lengthRatio < 0.7) return 0; // Too different in size
|
|
131
|
+
|
|
132
|
+
// Token sequence similarity using longest common subsequence
|
|
133
|
+
const lcs = this.longestCommonSubsequence(tokens1, tokens2);
|
|
134
|
+
const lcsRatio = (2 * lcs) / (tokens1.length + tokens2.length);
|
|
135
|
+
|
|
136
|
+
return lcsRatio;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Longest common subsequence length
|
|
141
|
+
*/
|
|
142
|
+
longestCommonSubsequence(seq1, seq2) {
|
|
143
|
+
const m = seq1.length;
|
|
144
|
+
const n = seq2.length;
|
|
145
|
+
|
|
146
|
+
// Use space-optimized version for large sequences
|
|
147
|
+
if (m * n > 100000) {
|
|
148
|
+
return this.approximateLCS(seq1, seq2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
|
|
152
|
+
|
|
153
|
+
for (let i = 1; i <= m; i++) {
|
|
154
|
+
for (let j = 1; j <= n; j++) {
|
|
155
|
+
if (seq1[i - 1] === seq2[j - 1]) {
|
|
156
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
157
|
+
} else {
|
|
158
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return dp[m][n];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Approximate LCS for large sequences
|
|
168
|
+
*/
|
|
169
|
+
approximateLCS(seq1, seq2) {
|
|
170
|
+
// Use windowed approach for efficiency
|
|
171
|
+
const windowSize = 100;
|
|
172
|
+
let commonTokens = 0;
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < seq1.length; i += windowSize) {
|
|
175
|
+
const window1 = seq1.slice(i, i + windowSize);
|
|
176
|
+
const set2 = new Set(seq2.slice(Math.max(0, i - windowSize), i + windowSize * 2));
|
|
177
|
+
|
|
178
|
+
commonTokens += window1.filter(t => set2.has(t)).length;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return commonTokens;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Calculate confidence for a clone group
|
|
186
|
+
*/
|
|
187
|
+
calculateGroupConfidence(blocks) {
|
|
188
|
+
if (blocks.length === 0) return 0;
|
|
189
|
+
|
|
190
|
+
// Calculate average pairwise similarity
|
|
191
|
+
let totalSimilarity = 0;
|
|
192
|
+
let comparisons = 0;
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
195
|
+
for (let j = i + 1; j < blocks.length; j++) {
|
|
196
|
+
totalSimilarity += this.calculateBlockSimilarity(blocks[i], blocks[j]);
|
|
197
|
+
comparisons++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return comparisons > 0 ? totalSimilarity / comparisons : 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// import { Command } from 'commander'; // Not needed for programmatic use
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { FileScanner } from './scanner.js';
|
|
5
|
+
import { CodeParser } from './parser.js';
|
|
6
|
+
import { CloneDetector } from './detector.js';
|
|
7
|
+
import { RefactoringAnalyzer } from './analyzer.js';
|
|
8
|
+
import { Reporter } from './reporter.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Main clone detection orchestrator
|
|
12
|
+
*/
|
|
13
|
+
class CloneDetectionTool {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.scanner = new FileScanner(config);
|
|
17
|
+
this.parser = new CodeParser(config);
|
|
18
|
+
this.detector = new CloneDetector(config, this.parser);
|
|
19
|
+
this.analyzer = new RefactoringAnalyzer(config);
|
|
20
|
+
this.reporter = new Reporter();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run the complete clone detection pipeline
|
|
25
|
+
*/
|
|
26
|
+
async run(projectPath, outputPath) {
|
|
27
|
+
console.log('Starting Code Clone Detection...\n');
|
|
28
|
+
console.log(`Project: ${projectPath}`);
|
|
29
|
+
console.log(`Config: minTokens=${this.config.minTokens}, minLines=${this.config.minLines}\n`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Step 1: Scan files
|
|
33
|
+
console.log('[1/5] Scanning project files...');
|
|
34
|
+
const files = await this.scanner.scanProject(projectPath);
|
|
35
|
+
|
|
36
|
+
if (files.length === 0) {
|
|
37
|
+
console.log('No files found to analyze.');
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 2: Parse and tokenize
|
|
42
|
+
console.log('[2/5] Parsing and tokenizing code...');
|
|
43
|
+
const parsedFiles = files.map(file => this.parser.parseFile(file));
|
|
44
|
+
|
|
45
|
+
// Step 3: Detect clones
|
|
46
|
+
console.log('[3/5] Detecting code clones...');
|
|
47
|
+
const clones = this.detector.detectClones(parsedFiles);
|
|
48
|
+
|
|
49
|
+
if (clones.length === 0) {
|
|
50
|
+
console.log('No significant clones detected.');
|
|
51
|
+
return this.reporter.generateReport([], parsedFiles);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 4: Analyze and generate refactoring advice
|
|
55
|
+
console.log('[4/5] Analyzing clones and generating refactoring advice...');
|
|
56
|
+
const analyzedClones = this.analyzer.analyzeClones(clones);
|
|
57
|
+
|
|
58
|
+
// Step 5: Generate report
|
|
59
|
+
console.log('[5/5] Generating report...');
|
|
60
|
+
const report = this.reporter.generateReport(analyzedClones, parsedFiles);
|
|
61
|
+
|
|
62
|
+
// Save report
|
|
63
|
+
if (outputPath) {
|
|
64
|
+
this.reporter.saveReport(report, outputPath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Print summary
|
|
68
|
+
this.reporter.printSummary(report);
|
|
69
|
+
|
|
70
|
+
// Print AI summary
|
|
71
|
+
console.log('\n📋 AI Agent Summary:');
|
|
72
|
+
console.log(this.reporter.generateAISummary(report));
|
|
73
|
+
console.log();
|
|
74
|
+
|
|
75
|
+
return report;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('\n❌ Error during clone detection:', error.message);
|
|
78
|
+
console.error(error.stack);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* CLI Interface (disabled for now)
|
|
86
|
+
*/
|
|
87
|
+
async function main() {
|
|
88
|
+
// CLI functionality disabled - use CloneDetectionTool programmatically
|
|
89
|
+
console.error('CLI not available in this version. Use CloneDetectionTool programmatically.');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
// const program = new Command();
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.name('code-clone-detector')
|
|
95
|
+
.description('AI-powered code clone detection and refactoring advisor')
|
|
96
|
+
.version('1.0.0')
|
|
97
|
+
.argument('<project-path>', 'Path to the project directory to analyze')
|
|
98
|
+
.option('-o, --output <path>', 'Output file path for JSON report', 'clone-report.json')
|
|
99
|
+
.option('-c, --config <path>', 'Path to config file', 'config.json')
|
|
100
|
+
.option('--min-tokens <number>', 'Minimum token count for clones', parseInt)
|
|
101
|
+
.option('--min-lines <number>', 'Minimum line count for clones', parseInt)
|
|
102
|
+
.action(async (projectPath, options) => {
|
|
103
|
+
try {
|
|
104
|
+
// Load config
|
|
105
|
+
let config;
|
|
106
|
+
const configPath = path.resolve(options.config);
|
|
107
|
+
|
|
108
|
+
if (fs.existsSync(configPath)) {
|
|
109
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
110
|
+
console.log(`Loaded config from: ${configPath}`);
|
|
111
|
+
} else {
|
|
112
|
+
console.log('Using default configuration');
|
|
113
|
+
config = {
|
|
114
|
+
minTokens: 50,
|
|
115
|
+
minLines: 5,
|
|
116
|
+
include: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
|
|
117
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
118
|
+
similarityThreshold: 0.85,
|
|
119
|
+
maxFileSize: 500000
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Override with CLI options
|
|
124
|
+
if (options.minTokens) config.minTokens = options.minTokens;
|
|
125
|
+
if (options.minLines) config.minLines = options.minLines;
|
|
126
|
+
|
|
127
|
+
// Resolve paths
|
|
128
|
+
const absProjectPath = path.resolve(projectPath);
|
|
129
|
+
const absOutputPath = path.resolve(options.output);
|
|
130
|
+
|
|
131
|
+
if (!fs.existsSync(absProjectPath)) {
|
|
132
|
+
console.error(`Error: Project path does not exist: ${absProjectPath}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Run detection
|
|
137
|
+
const tool = new CloneDetectionTool(config);
|
|
138
|
+
const report = await tool.run(absProjectPath, absOutputPath);
|
|
139
|
+
|
|
140
|
+
if (report) {
|
|
141
|
+
console.log(`✅ Analysis complete! Review the report at: ${absOutputPath}`);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
} else {
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('Fatal error:', error.message);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
program.parse();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Run CLI if executed directly
|
|
156
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
157
|
+
main();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { CloneDetectionTool };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as parser from '@babel/parser';
|
|
2
|
+
import traverse from '@babel/traverse';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses and tokenizes source code
|
|
7
|
+
*/
|
|
8
|
+
export class CodeParser {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse file and extract tokens
|
|
15
|
+
* @param {Object} file - File object with content
|
|
16
|
+
* @returns {Object} Parsed file with tokens and AST info
|
|
17
|
+
*/
|
|
18
|
+
parseFile(file) {
|
|
19
|
+
const extension = file.extension;
|
|
20
|
+
|
|
21
|
+
// Handle JavaScript/TypeScript files
|
|
22
|
+
if (['.js', '.jsx', '.ts', '.tsx', '.vue'].includes(extension)) {
|
|
23
|
+
return this.parseJavaScript(file);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fallback to simple tokenization
|
|
27
|
+
return this.simpleTokenize(file);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse JavaScript/TypeScript with Babel
|
|
32
|
+
*/
|
|
33
|
+
parseJavaScript(file) {
|
|
34
|
+
try {
|
|
35
|
+
const ast = parser.parse(file.content, {
|
|
36
|
+
sourceType: 'module',
|
|
37
|
+
plugins: [
|
|
38
|
+
'jsx',
|
|
39
|
+
'typescript',
|
|
40
|
+
'decorators-legacy',
|
|
41
|
+
'classProperties',
|
|
42
|
+
'optionalChaining',
|
|
43
|
+
'nullishCoalescingOperator'
|
|
44
|
+
],
|
|
45
|
+
errorRecovery: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const tokens = [];
|
|
49
|
+
const blocks = [];
|
|
50
|
+
let blockId = 0;
|
|
51
|
+
|
|
52
|
+
// Extract meaningful code blocks (functions, classes, etc.)
|
|
53
|
+
traverse.default(ast, {
|
|
54
|
+
FunctionDeclaration: (path) => this.extractBlock(path, file, blocks, tokens, blockId++),
|
|
55
|
+
FunctionExpression: (path) => this.extractBlock(path, file, blocks, tokens, blockId++),
|
|
56
|
+
ArrowFunctionExpression: (path) => this.extractBlock(path, file, blocks, tokens, blockId++),
|
|
57
|
+
ClassMethod: (path) => this.extractBlock(path, file, blocks, tokens, blockId++),
|
|
58
|
+
ClassDeclaration: (path) => this.extractBlock(path, file, blocks, tokens, blockId++),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Also create a flat token sequence for the entire file
|
|
62
|
+
const fileTokens = this.extractTokenSequence(ast);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...file,
|
|
66
|
+
ast,
|
|
67
|
+
tokens: fileTokens,
|
|
68
|
+
blocks,
|
|
69
|
+
parsed: true
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`Parse error in ${file.path}:`, error.message);
|
|
73
|
+
return this.simpleTokenize(file);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract a code block with token sequence
|
|
79
|
+
*/
|
|
80
|
+
extractBlock(path, file, blocks, tokens, blockId) {
|
|
81
|
+
const node = path.node;
|
|
82
|
+
const loc = node.loc;
|
|
83
|
+
|
|
84
|
+
if (!loc) return;
|
|
85
|
+
|
|
86
|
+
// Get the source code for this block
|
|
87
|
+
const lines = file.content.split('\n');
|
|
88
|
+
const blockCode = lines.slice(loc.start.line - 1, loc.end.line).join('\n');
|
|
89
|
+
|
|
90
|
+
// Create token sequence for this block
|
|
91
|
+
const blockTokens = this.tokenizeCode(blockCode);
|
|
92
|
+
|
|
93
|
+
if (blockTokens.length < this.config.minTokens) return;
|
|
94
|
+
|
|
95
|
+
blocks.push({
|
|
96
|
+
id: `${file.path}:block${blockId}`,
|
|
97
|
+
file: file.path,
|
|
98
|
+
startLine: loc.start.line,
|
|
99
|
+
endLine: loc.end.line,
|
|
100
|
+
code: blockCode,
|
|
101
|
+
tokens: blockTokens,
|
|
102
|
+
hash: this.hashTokens(blockTokens),
|
|
103
|
+
type: node.type
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract token sequence from AST
|
|
109
|
+
*/
|
|
110
|
+
extractTokenSequence(ast) {
|
|
111
|
+
const tokens = [];
|
|
112
|
+
|
|
113
|
+
traverse.default(ast, {
|
|
114
|
+
enter(path) {
|
|
115
|
+
const node = path.node;
|
|
116
|
+
|
|
117
|
+
// Normalize identifiers but keep structure
|
|
118
|
+
if (node.type === 'Identifier') {
|
|
119
|
+
tokens.push('IDENT');
|
|
120
|
+
} else if (node.type === 'Literal' || node.type === 'StringLiteral' ||
|
|
121
|
+
node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') {
|
|
122
|
+
tokens.push('LIT');
|
|
123
|
+
} else {
|
|
124
|
+
tokens.push(node.type);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return tokens;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Simple tokenization for non-JS files
|
|
134
|
+
*/
|
|
135
|
+
simpleTokenize(file) {
|
|
136
|
+
const tokens = this.tokenizeCode(file.content);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...file,
|
|
140
|
+
tokens,
|
|
141
|
+
blocks: [{
|
|
142
|
+
id: `${file.path}:full`,
|
|
143
|
+
file: file.path,
|
|
144
|
+
startLine: 1,
|
|
145
|
+
endLine: file.content.split('\n').length,
|
|
146
|
+
code: file.content,
|
|
147
|
+
tokens,
|
|
148
|
+
hash: this.hashTokens(tokens),
|
|
149
|
+
type: 'File'
|
|
150
|
+
}],
|
|
151
|
+
parsed: false
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Tokenize code string (language-agnostic)
|
|
157
|
+
*/
|
|
158
|
+
tokenizeCode(code) {
|
|
159
|
+
// Remove comments and normalize
|
|
160
|
+
const cleaned = code
|
|
161
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Block comments
|
|
162
|
+
.replace(/\/\/.*/g, '') // Line comments
|
|
163
|
+
.replace(/\s+/g, ' '); // Normalize whitespace
|
|
164
|
+
|
|
165
|
+
// Simple tokenization
|
|
166
|
+
const tokens = cleaned.match(/[a-zA-Z_$][a-zA-Z0-9_$]*|[{}()\[\];,.]|[+\-*/%=<>!&|]+|"[^"]*"|'[^']*'|`[^`]*`|\d+/g) || [];
|
|
167
|
+
|
|
168
|
+
return tokens;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create hash of token sequence
|
|
173
|
+
*/
|
|
174
|
+
hashTokens(tokens) {
|
|
175
|
+
return crypto
|
|
176
|
+
.createHash('md5')
|
|
177
|
+
.update(tokens.join(','))
|
|
178
|
+
.digest('hex');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate similarity between token sequences
|
|
183
|
+
*/
|
|
184
|
+
calculateSimilarity(tokens1, tokens2) {
|
|
185
|
+
const len1 = tokens1.length;
|
|
186
|
+
const len2 = tokens2.length;
|
|
187
|
+
|
|
188
|
+
if (len1 === 0 || len2 === 0) return 0;
|
|
189
|
+
|
|
190
|
+
// Use Jaccard similarity for token sets
|
|
191
|
+
const set1 = new Set(tokens1);
|
|
192
|
+
const set2 = new Set(tokens2);
|
|
193
|
+
|
|
194
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
195
|
+
const union = new Set([...set1, ...set2]);
|
|
196
|
+
|
|
197
|
+
return intersection.size / union.size;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formats and outputs clone detection results
|
|
5
|
+
*/
|
|
6
|
+
export class Reporter {
|
|
7
|
+
/**
|
|
8
|
+
* Generate comprehensive report
|
|
9
|
+
* @param {Array} clones - Analyzed clones with refactoring advice
|
|
10
|
+
* @param {Array} files - Parsed files
|
|
11
|
+
* @returns {Object} Report object
|
|
12
|
+
*/
|
|
13
|
+
generateReport(clones, files) {
|
|
14
|
+
const summary = this.generateSummary(clones, files);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
summary,
|
|
18
|
+
clones: clones.map(clone => this.formatClone(clone)),
|
|
19
|
+
metadata: {
|
|
20
|
+
generatedAt: new Date().toISOString(),
|
|
21
|
+
tool: 'code-clone-detector',
|
|
22
|
+
version: '1.0.0'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate summary statistics
|
|
29
|
+
*/
|
|
30
|
+
generateSummary(clones, files) {
|
|
31
|
+
const totalFiles = files.length;
|
|
32
|
+
const totalClones = clones.length;
|
|
33
|
+
|
|
34
|
+
// Calculate total duplicated lines
|
|
35
|
+
const totalDuplicatedLines = clones.reduce((sum, clone) =>
|
|
36
|
+
sum + clone.metrics.duplicatedLines, 0
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Calculate total lines of code
|
|
40
|
+
const totalLines = files.reduce((sum, file) =>
|
|
41
|
+
sum + file.content.split('\n').length, 0
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const duplicationPercentage = totalLines > 0
|
|
45
|
+
? ((totalDuplicatedLines / totalLines) * 100).toFixed(2)
|
|
46
|
+
: 0;
|
|
47
|
+
|
|
48
|
+
// Priority breakdown
|
|
49
|
+
const priorityCounts = {
|
|
50
|
+
high: clones.filter(c => c.refactoringAdvice.priority === 'high').length,
|
|
51
|
+
medium: clones.filter(c => c.refactoringAdvice.priority === 'medium').length,
|
|
52
|
+
low: clones.filter(c => c.refactoringAdvice.priority === 'low').length
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
totalFiles,
|
|
57
|
+
totalClones,
|
|
58
|
+
totalDuplicatedLines,
|
|
59
|
+
duplicationPercentage: parseFloat(duplicationPercentage),
|
|
60
|
+
priorityCounts,
|
|
61
|
+
topRefactoringOpportunities: clones.slice(0, 5).map(c => c.id)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format clone for output
|
|
67
|
+
*/
|
|
68
|
+
formatClone(clone) {
|
|
69
|
+
return {
|
|
70
|
+
id: clone.id,
|
|
71
|
+
type: clone.type,
|
|
72
|
+
confidence: clone.confidence,
|
|
73
|
+
instances: clone.instances,
|
|
74
|
+
metrics: clone.metrics,
|
|
75
|
+
refactoringAdvice: clone.refactoringAdvice
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Save report to file
|
|
81
|
+
*/
|
|
82
|
+
saveReport(report, outputPath) {
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
|
|
85
|
+
console.log(`\nReport saved to: ${outputPath}`);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error saving report:', error.message);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Print summary to console
|
|
95
|
+
*/
|
|
96
|
+
printSummary(report) {
|
|
97
|
+
const { summary } = report;
|
|
98
|
+
|
|
99
|
+
console.log('\n' + '='.repeat(60));
|
|
100
|
+
console.log('CODE CLONE DETECTION SUMMARY');
|
|
101
|
+
console.log('='.repeat(60));
|
|
102
|
+
console.log(`Total Files Analyzed: ${summary.totalFiles}`);
|
|
103
|
+
console.log(`Total Clone Groups Found: ${summary.totalClones}`);
|
|
104
|
+
console.log(`Total Duplicated Lines: ${summary.totalDuplicatedLines}`);
|
|
105
|
+
console.log(`Duplication Percentage: ${summary.duplicationPercentage}%`);
|
|
106
|
+
console.log('\nPriority Breakdown:');
|
|
107
|
+
console.log(` High Priority: ${summary.priorityCounts.high}`);
|
|
108
|
+
console.log(` Medium Priority: ${summary.priorityCounts.medium}`);
|
|
109
|
+
console.log(` Low Priority: ${summary.priorityCounts.low}`);
|
|
110
|
+
|
|
111
|
+
if (report.clones.length > 0) {
|
|
112
|
+
console.log('\n' + '-'.repeat(60));
|
|
113
|
+
console.log('TOP REFACTORING OPPORTUNITIES');
|
|
114
|
+
console.log('-'.repeat(60));
|
|
115
|
+
|
|
116
|
+
report.clones.slice(0, 3).forEach((clone, idx) => {
|
|
117
|
+
console.log(`\n${idx + 1}. ${clone.id} [${clone.refactoringAdvice.priority.toUpperCase()} PRIORITY]`);
|
|
118
|
+
console.log(` Type: ${clone.type} (${(clone.confidence * 100).toFixed(0)}% confidence)`);
|
|
119
|
+
console.log(` Instances: ${clone.metrics.instanceCount} copies across ${clone.metrics.filesCovered} files`);
|
|
120
|
+
console.log(` Size: ${clone.metrics.lineCount} lines, ${clone.metrics.tokenCount} tokens`);
|
|
121
|
+
console.log(` Impact Score: ${clone.metrics.impactScore}`);
|
|
122
|
+
console.log(` Strategy: ${clone.refactoringAdvice.strategy}`);
|
|
123
|
+
console.log(` Suggested Name: ${clone.refactoringAdvice.suggestedName}`);
|
|
124
|
+
console.log(` Reasoning: ${clone.refactoringAdvice.reasoning}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log('\n' + '='.repeat(60));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate AI-friendly summary
|
|
133
|
+
*/
|
|
134
|
+
generateAISummary(report) {
|
|
135
|
+
const topClone = report.clones[0];
|
|
136
|
+
|
|
137
|
+
if (!topClone) {
|
|
138
|
+
return 'No significant code duplication found in the project.';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let summary = `Found ${report.summary.totalClones} code clone groups with ${report.summary.duplicationPercentage}% duplication. `;
|
|
142
|
+
summary += `Top priority: ${topClone.refactoringAdvice.suggestedName} appears ${topClone.metrics.instanceCount} times. `;
|
|
143
|
+
summary += `Recommended action: ${topClone.refactoringAdvice.strategy}. `;
|
|
144
|
+
summary += topClone.refactoringAdvice.reasoning;
|
|
145
|
+
|
|
146
|
+
return summary;
|
|
147
|
+
}
|
|
148
|
+
}
|