agent-ide 0.1.9 → 0.2.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 +103 -17
- package/dist/application/services/module-coordinator.service.d.ts +0 -1
- package/dist/application/services/module-coordinator.service.d.ts.map +1 -1
- package/dist/application/services/module-coordinator.service.js +2 -8
- package/dist/application/services/module-coordinator.service.js.map +1 -1
- package/dist/core/analysis/index.d.ts +1 -4
- package/dist/core/analysis/index.d.ts.map +1 -1
- package/dist/core/analysis/index.js +1 -7
- package/dist/core/analysis/index.js.map +1 -1
- package/dist/core/dependency/dependency-analyzer.d.ts.map +1 -1
- package/dist/core/dependency/dependency-analyzer.js +10 -0
- package/dist/core/dependency/dependency-analyzer.js.map +1 -1
- package/dist/core/indexing/index-engine.d.ts +4 -0
- package/dist/core/indexing/index-engine.d.ts.map +1 -1
- package/dist/core/indexing/index-engine.js +25 -1
- package/dist/core/indexing/index-engine.js.map +1 -1
- package/dist/core/indexing/symbol-index.d.ts +4 -0
- package/dist/core/indexing/symbol-index.d.ts.map +1 -1
- package/dist/core/indexing/symbol-index.js +17 -0
- package/dist/core/indexing/symbol-index.js.map +1 -1
- package/dist/core/move/import-resolver.d.ts.map +1 -1
- package/dist/core/move/import-resolver.js +8 -0
- package/dist/core/move/import-resolver.js.map +1 -1
- package/dist/core/move/move-service.js +7 -7
- package/dist/core/move/move-service.js.map +1 -1
- package/dist/core/refactor/swift-extractor.d.ts +98 -0
- package/dist/core/refactor/swift-extractor.d.ts.map +1 -0
- package/dist/core/refactor/swift-extractor.js +283 -0
- package/dist/core/refactor/swift-extractor.js.map +1 -0
- package/dist/core/rename/reference-updater.d.ts.map +1 -1
- package/dist/core/rename/reference-updater.js +16 -8
- package/dist/core/rename/reference-updater.js.map +1 -1
- package/dist/core/search/engines/text-engine.js +1 -1
- package/dist/core/search/engines/text-engine.js.map +1 -1
- package/dist/core/shit-score/grading.d.ts +39 -0
- package/dist/core/shit-score/grading.d.ts.map +1 -0
- package/dist/core/shit-score/grading.js +253 -0
- package/dist/core/shit-score/grading.js.map +1 -0
- package/dist/core/shit-score/index.d.ts +9 -0
- package/dist/core/shit-score/index.d.ts.map +1 -0
- package/dist/core/shit-score/index.js +8 -0
- package/dist/core/shit-score/index.js.map +1 -0
- package/dist/core/shit-score/score-calculator.d.ts +75 -0
- package/dist/core/shit-score/score-calculator.d.ts.map +1 -0
- package/dist/core/shit-score/score-calculator.js +240 -0
- package/dist/core/shit-score/score-calculator.js.map +1 -0
- package/dist/core/shit-score/shit-score-analyzer.d.ts +84 -0
- package/dist/core/shit-score/shit-score-analyzer.d.ts.map +1 -0
- package/dist/core/shit-score/shit-score-analyzer.js +595 -0
- package/dist/core/shit-score/shit-score-analyzer.js.map +1 -0
- package/dist/core/shit-score/types.d.ts +231 -0
- package/dist/core/shit-score/types.d.ts.map +1 -0
- package/dist/core/shit-score/types.js +73 -0
- package/dist/core/shit-score/types.js.map +1 -0
- package/dist/core/snapshot/code-compressor.d.ts +39 -0
- package/dist/core/snapshot/code-compressor.d.ts.map +1 -0
- package/dist/core/snapshot/code-compressor.js +211 -0
- package/dist/core/snapshot/code-compressor.js.map +1 -0
- package/dist/core/snapshot/config.d.ts +60 -0
- package/dist/core/snapshot/config.d.ts.map +1 -0
- package/dist/core/snapshot/config.js +136 -0
- package/dist/core/snapshot/config.js.map +1 -0
- package/dist/core/snapshot/index.d.ts +23 -0
- package/dist/core/snapshot/index.d.ts.map +1 -0
- package/dist/core/snapshot/index.js +27 -0
- package/dist/core/snapshot/index.js.map +1 -0
- package/dist/core/snapshot/snapshot-differ.d.ts +54 -0
- package/dist/core/snapshot/snapshot-differ.d.ts.map +1 -0
- package/dist/core/snapshot/snapshot-differ.js +262 -0
- package/dist/core/snapshot/snapshot-differ.js.map +1 -0
- package/dist/core/snapshot/snapshot-engine.d.ts +94 -0
- package/dist/core/snapshot/snapshot-engine.d.ts.map +1 -0
- package/dist/core/snapshot/snapshot-engine.js +492 -0
- package/dist/core/snapshot/snapshot-engine.js.map +1 -0
- package/dist/core/snapshot/types.d.ts +216 -0
- package/dist/core/snapshot/types.d.ts.map +1 -0
- package/dist/core/snapshot/types.js +79 -0
- package/dist/core/snapshot/types.js.map +1 -0
- package/dist/infrastructure/parser/analysis-types.d.ts +198 -0
- package/dist/infrastructure/parser/analysis-types.d.ts.map +1 -0
- package/dist/infrastructure/parser/analysis-types.js +6 -0
- package/dist/infrastructure/parser/analysis-types.js.map +1 -0
- package/dist/infrastructure/parser/base.d.ts +36 -0
- package/dist/infrastructure/parser/base.d.ts.map +1 -1
- package/dist/infrastructure/parser/base.js +72 -0
- package/dist/infrastructure/parser/base.js.map +1 -1
- package/dist/infrastructure/parser/index.d.ts +1 -0
- package/dist/infrastructure/parser/index.d.ts.map +1 -1
- package/dist/infrastructure/parser/index.js.map +1 -1
- package/dist/infrastructure/parser/interface.d.ts +63 -0
- package/dist/infrastructure/parser/interface.d.ts.map +1 -1
- package/dist/infrastructure/parser/interface.js +11 -1
- package/dist/infrastructure/parser/interface.js.map +1 -1
- package/dist/interfaces/cli/cli.d.ts +24 -0
- package/dist/interfaces/cli/cli.d.ts.map +1 -1
- package/dist/interfaces/cli/cli.js +1483 -157
- package/dist/interfaces/cli/cli.js.map +1 -1
- package/dist/plugins/javascript/parser.d.ts +41 -0
- package/dist/plugins/javascript/parser.d.ts.map +1 -1
- package/dist/plugins/javascript/parser.js +284 -0
- package/dist/plugins/javascript/parser.js.map +1 -1
- package/dist/plugins/swift/analyzers/complexity-analyzer.d.ts +41 -0
- package/dist/plugins/swift/analyzers/complexity-analyzer.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/complexity-analyzer.js +206 -0
- package/dist/plugins/swift/analyzers/complexity-analyzer.js.map +1 -0
- package/dist/plugins/swift/analyzers/duplication-detector.d.ts +89 -0
- package/dist/plugins/swift/analyzers/duplication-detector.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/duplication-detector.js +271 -0
- package/dist/plugins/swift/analyzers/duplication-detector.js.map +1 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.d.ts +34 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.js +135 -0
- package/dist/plugins/swift/analyzers/error-handling-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/naming-checker.d.ts +47 -0
- package/dist/plugins/swift/analyzers/naming-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/naming-checker.js +161 -0
- package/dist/plugins/swift/analyzers/naming-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/pattern-detector.d.ts +78 -0
- package/dist/plugins/swift/analyzers/pattern-detector.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/pattern-detector.js +247 -0
- package/dist/plugins/swift/analyzers/pattern-detector.js.map +1 -0
- package/dist/plugins/swift/analyzers/security-checker.d.ts +38 -0
- package/dist/plugins/swift/analyzers/security-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/security-checker.js +135 -0
- package/dist/plugins/swift/analyzers/security-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.d.ts +26 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.js +63 -0
- package/dist/plugins/swift/analyzers/test-coverage-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.d.ts +41 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.js +121 -0
- package/dist/plugins/swift/analyzers/type-safety-checker.js.map +1 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.d.ts +38 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.d.ts.map +1 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.js +211 -0
- package/dist/plugins/swift/analyzers/unused-symbol-detector.js.map +1 -0
- package/dist/plugins/swift/dependency-analyzer.d.ts +33 -0
- package/dist/plugins/swift/dependency-analyzer.d.ts.map +1 -0
- package/dist/plugins/swift/dependency-analyzer.js +95 -0
- package/dist/plugins/swift/dependency-analyzer.js.map +1 -0
- package/dist/plugins/swift/index.d.ts +14 -0
- package/dist/plugins/swift/index.d.ts.map +1 -0
- package/dist/plugins/swift/index.js +19 -0
- package/dist/plugins/swift/index.js.map +1 -0
- package/dist/plugins/swift/parser.d.ts +160 -0
- package/dist/plugins/swift/parser.d.ts.map +1 -0
- package/dist/plugins/swift/parser.js +670 -0
- package/dist/plugins/swift/parser.js.map +1 -0
- package/dist/plugins/swift/swift-bridge/swift-parser +0 -0
- package/dist/plugins/swift/symbol-extractor.d.ts +46 -0
- package/dist/plugins/swift/symbol-extractor.d.ts.map +1 -0
- package/dist/plugins/swift/symbol-extractor.js +187 -0
- package/dist/plugins/swift/symbol-extractor.js.map +1 -0
- package/dist/plugins/swift/types.d.ts +137 -0
- package/dist/plugins/swift/types.d.ts.map +1 -0
- package/dist/plugins/swift/types.js +212 -0
- package/dist/plugins/swift/types.js.map +1 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.d.ts +39 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.js +196 -0
- package/dist/plugins/typescript/analyzers/complexity-analyzer.js.map +1 -0
- package/dist/{core/analysis → plugins/typescript/analyzers}/duplication-detector.d.ts +34 -3
- package/dist/plugins/typescript/analyzers/duplication-detector.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/duplication-detector.js +695 -0
- package/dist/plugins/typescript/analyzers/duplication-detector.js.map +1 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.d.ts +26 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.js +84 -0
- package/dist/plugins/typescript/analyzers/error-handling-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/naming-checker.d.ts +30 -0
- package/dist/plugins/typescript/analyzers/naming-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/naming-checker.js +116 -0
- package/dist/plugins/typescript/analyzers/naming-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.d.ts +80 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.js +267 -0
- package/dist/plugins/typescript/analyzers/pattern-detector.js.map +1 -0
- package/dist/plugins/typescript/analyzers/security-checker.d.ts +34 -0
- package/dist/plugins/typescript/analyzers/security-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/security-checker.js +126 -0
- package/dist/plugins/typescript/analyzers/security-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.d.ts +22 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.js +62 -0
- package/dist/plugins/typescript/analyzers/test-coverage-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.d.ts +32 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.js +86 -0
- package/dist/plugins/typescript/analyzers/type-safety-checker.js.map +1 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.d.ts +47 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.d.ts.map +1 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.js +152 -0
- package/dist/plugins/typescript/analyzers/unused-symbol-detector.js.map +1 -0
- package/dist/plugins/typescript/parser.d.ts +41 -0
- package/dist/plugins/typescript/parser.d.ts.map +1 -1
- package/dist/plugins/typescript/parser.js +336 -0
- package/dist/plugins/typescript/parser.js.map +1 -1
- package/dist/shared/types/symbol.d.ts +7 -1
- package/dist/shared/types/symbol.d.ts.map +1 -1
- package/dist/shared/types/symbol.js +8 -2
- package/dist/shared/types/symbol.js.map +1 -1
- package/package.json +17 -7
- package/bin/mcp-server.js +0 -20
- package/dist/core/analysis/complexity-analyzer.d.ts +0 -81
- package/dist/core/analysis/complexity-analyzer.d.ts.map +0 -1
- package/dist/core/analysis/complexity-analyzer.js +0 -255
- package/dist/core/analysis/complexity-analyzer.js.map +0 -1
- package/dist/core/analysis/dead-code-detector.d.ts +0 -152
- package/dist/core/analysis/dead-code-detector.d.ts.map +0 -1
- package/dist/core/analysis/dead-code-detector.js +0 -351
- package/dist/core/analysis/dead-code-detector.js.map +0 -1
- package/dist/core/analysis/duplication-detector.d.ts.map +0 -1
- package/dist/core/analysis/duplication-detector.js +0 -433
- package/dist/core/analysis/duplication-detector.js.map +0 -1
- package/dist/interfaces/mcp/index.d.ts +0 -7
- package/dist/interfaces/mcp/index.d.ts.map +0 -1
- package/dist/interfaces/mcp/index.js +0 -6
- package/dist/interfaces/mcp/index.js.map +0 -1
- package/dist/interfaces/mcp/mcp-server.d.ts +0 -34
- package/dist/interfaces/mcp/mcp-server.d.ts.map +0 -1
- package/dist/interfaces/mcp/mcp-server.js +0 -162
- package/dist/interfaces/mcp/mcp-server.js.map +0 -1
- package/dist/interfaces/mcp/mcp.d.ts +0 -52
- package/dist/interfaces/mcp/mcp.d.ts.map +0 -1
- package/dist/interfaces/mcp/mcp.js +0 -843
- package/dist/interfaces/mcp/mcp.js.map +0 -1
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 重複程式碼檢測器
|
|
3
|
+
* 檢測程式碼克隆,包括 Type-1、Type-2 和 Type-3 克隆
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
/**
|
|
7
|
+
* 計算程式碼片段的平均行數
|
|
8
|
+
*/
|
|
9
|
+
function calculateAverageLines(fragments) {
|
|
10
|
+
const totalLines = fragments.reduce((sum, f) => sum + (f.location.endLine - f.location.startLine + 1), 0);
|
|
11
|
+
return Math.round(totalLines / fragments.length);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Type-1 克隆檢測器(完全相同的代碼,除了空白和註釋)
|
|
15
|
+
*/
|
|
16
|
+
export class Type1CloneDetector {
|
|
17
|
+
/**
|
|
18
|
+
* 檢測 Type-1 克隆
|
|
19
|
+
* @param fragments 程式碼片段列表
|
|
20
|
+
* @param config 檢測配置
|
|
21
|
+
* @returns Type-1 克隆列表
|
|
22
|
+
*/
|
|
23
|
+
detect(fragments, config) {
|
|
24
|
+
const clones = [];
|
|
25
|
+
const hashGroups = new Map();
|
|
26
|
+
// 按 hash 分組
|
|
27
|
+
for (const fragment of fragments) {
|
|
28
|
+
if (fragment.tokens.length < config.minTokens) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const hash = this.computeHash(fragment, config);
|
|
32
|
+
if (!hashGroups.has(hash)) {
|
|
33
|
+
hashGroups.set(hash, []);
|
|
34
|
+
}
|
|
35
|
+
const group = hashGroups.get(hash);
|
|
36
|
+
if (group) {
|
|
37
|
+
group.push(fragment);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 找出重複的組
|
|
41
|
+
for (const group of hashGroups.values()) {
|
|
42
|
+
if (group.length > 1) {
|
|
43
|
+
clones.push({
|
|
44
|
+
type: 'type-1',
|
|
45
|
+
instances: group,
|
|
46
|
+
lines: calculateAverageLines(group),
|
|
47
|
+
similarity: 1.0,
|
|
48
|
+
severity: this.calculateSeverity(group.length, calculateAverageLines(group))
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return clones;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 計算程式碼片段的 hash
|
|
56
|
+
*/
|
|
57
|
+
computeHash(fragment, config) {
|
|
58
|
+
let tokens = fragment.tokens.slice();
|
|
59
|
+
if (config.ignoreWhitespace) {
|
|
60
|
+
tokens = tokens.filter(token => !/^\s+$/.test(token));
|
|
61
|
+
}
|
|
62
|
+
if (config.ignoreComments) {
|
|
63
|
+
tokens = tokens.filter(token => !token.startsWith('//') && !token.startsWith('/*'));
|
|
64
|
+
}
|
|
65
|
+
const normalized = tokens.join('');
|
|
66
|
+
return createHash('md5').update(normalized).digest('hex');
|
|
67
|
+
}
|
|
68
|
+
calculateSeverity(instanceCount, lines) {
|
|
69
|
+
if (instanceCount >= 5 || lines >= 50) {
|
|
70
|
+
return 'high';
|
|
71
|
+
}
|
|
72
|
+
if (instanceCount >= 3 || lines >= 20) {
|
|
73
|
+
return 'medium';
|
|
74
|
+
}
|
|
75
|
+
return 'low';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Type-2 克隆檢測器(結構相同但變數名、類型、字面值不同)
|
|
80
|
+
*/
|
|
81
|
+
export class Type2CloneDetector {
|
|
82
|
+
/**
|
|
83
|
+
* 檢測 Type-2 克隆
|
|
84
|
+
* @param fragments 程式碼片段列表
|
|
85
|
+
* @param config 檢測配置
|
|
86
|
+
* @returns Type-2 克隆列表(僅包含跨檔案重複)
|
|
87
|
+
*/
|
|
88
|
+
detect(fragments, config) {
|
|
89
|
+
const clones = [];
|
|
90
|
+
const normalizedGroups = new Map();
|
|
91
|
+
for (const fragment of fragments) {
|
|
92
|
+
if (fragment.tokens.length < config.minTokens) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const normalized = this.normalizeAST(fragment.ast);
|
|
96
|
+
const hash = this.computeNormalizedHash(normalized);
|
|
97
|
+
if (!normalizedGroups.has(hash)) {
|
|
98
|
+
normalizedGroups.set(hash, []);
|
|
99
|
+
}
|
|
100
|
+
const group = normalizedGroups.get(hash);
|
|
101
|
+
if (group) {
|
|
102
|
+
group.push(fragment);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const group of normalizedGroups.values()) {
|
|
106
|
+
if (group.length > 1) {
|
|
107
|
+
// 過濾掉同一檔案內的重複(同一個類的不同方法不應視為重複)
|
|
108
|
+
const crossFileInstances = this.filterCrossFileInstances(group);
|
|
109
|
+
if (crossFileInstances.length > 1) {
|
|
110
|
+
clones.push({
|
|
111
|
+
type: 'type-2',
|
|
112
|
+
instances: crossFileInstances,
|
|
113
|
+
lines: calculateAverageLines(crossFileInstances),
|
|
114
|
+
similarity: this.calculateSimilarity(crossFileInstances),
|
|
115
|
+
severity: this.calculateSeverity(crossFileInstances.length, calculateAverageLines(crossFileInstances))
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return clones;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 過濾掉同一檔案內的實例,只保留跨檔案的重複
|
|
124
|
+
*/
|
|
125
|
+
filterCrossFileInstances(instances) {
|
|
126
|
+
const fileGroups = new Map();
|
|
127
|
+
for (const instance of instances) {
|
|
128
|
+
const file = instance.location.file;
|
|
129
|
+
if (!fileGroups.has(file)) {
|
|
130
|
+
fileGroups.set(file, []);
|
|
131
|
+
}
|
|
132
|
+
fileGroups.get(file).push(instance);
|
|
133
|
+
}
|
|
134
|
+
// 如果所有實例都在同一個檔案,返回空陣列
|
|
135
|
+
if (fileGroups.size === 1) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
// 每個檔案只保留一個代表
|
|
139
|
+
return Array.from(fileGroups.values()).map(group => group[0]);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 正規化 AST(移除變數名、字面值等)
|
|
143
|
+
*/
|
|
144
|
+
normalizeAST(ast) {
|
|
145
|
+
const normalized = {
|
|
146
|
+
type: ast.type
|
|
147
|
+
};
|
|
148
|
+
// 對於變數和字面值,統一為佔位符
|
|
149
|
+
if (ast.type === 'Identifier') {
|
|
150
|
+
normalized.value = '$VAR$';
|
|
151
|
+
}
|
|
152
|
+
else if (ast.type === 'Literal') {
|
|
153
|
+
normalized.value = '$LITERAL$';
|
|
154
|
+
}
|
|
155
|
+
else if (ast.value !== undefined) {
|
|
156
|
+
normalized.value = ast.value;
|
|
157
|
+
}
|
|
158
|
+
// 遞歸處理子節點
|
|
159
|
+
if (ast.children) {
|
|
160
|
+
normalized.children = ast.children.map(child => this.normalizeAST(child));
|
|
161
|
+
}
|
|
162
|
+
return normalized;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 計算正規化 AST 的 hash
|
|
166
|
+
*/
|
|
167
|
+
computeNormalizedHash(ast) {
|
|
168
|
+
const json = JSON.stringify(ast, Object.keys(ast).sort());
|
|
169
|
+
return createHash('md5').update(json).digest('hex');
|
|
170
|
+
}
|
|
171
|
+
calculateSimilarity(fragments) {
|
|
172
|
+
// Type-2 克隆的相似度計算
|
|
173
|
+
return 0.85; // 簡化實作
|
|
174
|
+
}
|
|
175
|
+
calculateSeverity(instanceCount, lines) {
|
|
176
|
+
if (instanceCount >= 4 || lines >= 40) {
|
|
177
|
+
return 'high';
|
|
178
|
+
}
|
|
179
|
+
if (instanceCount >= 3 || lines >= 15) {
|
|
180
|
+
return 'medium';
|
|
181
|
+
}
|
|
182
|
+
return 'low';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Type-3 克隆檢測器(結構相似但有些語句被修改、新增或刪除)
|
|
187
|
+
*/
|
|
188
|
+
export class Type3CloneDetector {
|
|
189
|
+
threshold = 0.7; // 預設相似度閾值
|
|
190
|
+
/**
|
|
191
|
+
* 設定相似度閾值
|
|
192
|
+
* @param threshold 相似度閾值(0-1 之間)
|
|
193
|
+
*/
|
|
194
|
+
setThreshold(threshold) {
|
|
195
|
+
this.threshold = Math.max(0, Math.min(1, threshold));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 檢測 Type-3 克隆
|
|
199
|
+
* @param fragments 程式碼片段列表
|
|
200
|
+
* @param config 檢測配置
|
|
201
|
+
* @returns Type-3 克隆列表(僅包含跨檔案重複)
|
|
202
|
+
*/
|
|
203
|
+
detect(fragments, config) {
|
|
204
|
+
const clones = [];
|
|
205
|
+
const effectiveThreshold = Math.min(config.similarityThreshold, this.threshold);
|
|
206
|
+
// Type-3 檢測需要比較每對片段的相似度
|
|
207
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
208
|
+
for (let j = i + 1; j < fragments.length; j++) {
|
|
209
|
+
// 跳過同一檔案內的片段
|
|
210
|
+
if (fragments[i].location.file === fragments[j].location.file) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const similarity = this.calculateSimilarity(fragments[i], fragments[j]);
|
|
214
|
+
if (similarity >= effectiveThreshold && similarity < 0.95) { // Type-3 不是完全相同
|
|
215
|
+
clones.push({
|
|
216
|
+
type: 'type-3',
|
|
217
|
+
instances: [fragments[i], fragments[j]],
|
|
218
|
+
lines: Math.max(fragments[i].location.endLine - fragments[i].location.startLine + 1, fragments[j].location.endLine - fragments[j].location.startLine + 1),
|
|
219
|
+
similarity,
|
|
220
|
+
severity: this.calculateSeverity(similarity, Math.max(fragments[i].location.endLine - fragments[i].location.startLine + 1, fragments[j].location.endLine - fragments[j].location.startLine + 1))
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return clones;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 計算兩個程式碼片段的相似度
|
|
229
|
+
*/
|
|
230
|
+
calculateSimilarity(fragment1, fragment2) {
|
|
231
|
+
// 使用改進的相似度計算
|
|
232
|
+
return this.calculateAdvancedSimilarity(fragment1, fragment2);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 計算編輯距離(Levenshtein Distance)
|
|
236
|
+
*/
|
|
237
|
+
calculateEditDistance(tokens1, tokens2) {
|
|
238
|
+
const m = tokens1.length;
|
|
239
|
+
const n = tokens2.length;
|
|
240
|
+
// 處理空陣列情況
|
|
241
|
+
if (m === 0) {
|
|
242
|
+
return n;
|
|
243
|
+
}
|
|
244
|
+
if (n === 0) {
|
|
245
|
+
return m;
|
|
246
|
+
}
|
|
247
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
248
|
+
// 初始化邊界條件
|
|
249
|
+
for (let i = 0; i <= m; i++) {
|
|
250
|
+
dp[i][0] = i;
|
|
251
|
+
}
|
|
252
|
+
for (let j = 0; j <= n; j++) {
|
|
253
|
+
dp[0][j] = j;
|
|
254
|
+
}
|
|
255
|
+
// 動態規劃計算編輯距離
|
|
256
|
+
for (let i = 1; i <= m; i++) {
|
|
257
|
+
for (let j = 1; j <= n; j++) {
|
|
258
|
+
if (tokens1[i - 1] === tokens2[j - 1]) {
|
|
259
|
+
dp[i][j] = dp[i - 1][j - 1]; // 無需編輯
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, // 刪除
|
|
263
|
+
dp[i][j - 1] + 1, // 插入
|
|
264
|
+
dp[i - 1][j - 1] + 1 // 替換
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return dp[m][n];
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 改進的相似度計算(結合多種指標)
|
|
273
|
+
*/
|
|
274
|
+
calculateAdvancedSimilarity(fragment1, fragment2) {
|
|
275
|
+
const tokens1 = fragment1.tokens;
|
|
276
|
+
const tokens2 = fragment2.tokens;
|
|
277
|
+
if (tokens1.length === 0 && tokens2.length === 0) {
|
|
278
|
+
return 1.0;
|
|
279
|
+
}
|
|
280
|
+
if (tokens1.length === 0 || tokens2.length === 0) {
|
|
281
|
+
return 0.0;
|
|
282
|
+
}
|
|
283
|
+
// 1. 編輯距離相似度
|
|
284
|
+
const editDistance = this.calculateEditDistance(tokens1, tokens2);
|
|
285
|
+
const maxLength = Math.max(tokens1.length, tokens2.length);
|
|
286
|
+
const editSimilarity = 1 - (editDistance / maxLength);
|
|
287
|
+
// 2. Jaccard 相似度(基於 token 集合)
|
|
288
|
+
const set1 = new Set(tokens1);
|
|
289
|
+
const set2 = new Set(tokens2);
|
|
290
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
291
|
+
const union = new Set([...set1, ...set2]);
|
|
292
|
+
const jaccardSimilarity = intersection.size / union.size;
|
|
293
|
+
// 3. 長度相似度
|
|
294
|
+
const lengthSimilarity = Math.min(tokens1.length, tokens2.length) /
|
|
295
|
+
Math.max(tokens1.length, tokens2.length);
|
|
296
|
+
// 綜合相似度(加權平均)
|
|
297
|
+
return (editSimilarity * 0.5) + (jaccardSimilarity * 0.3) + (lengthSimilarity * 0.2);
|
|
298
|
+
}
|
|
299
|
+
calculateSeverity(similarity, lines) {
|
|
300
|
+
if (similarity >= 0.8 && lines >= 30) {
|
|
301
|
+
return 'high';
|
|
302
|
+
}
|
|
303
|
+
if (similarity >= 0.7 && lines >= 15) {
|
|
304
|
+
return 'medium';
|
|
305
|
+
}
|
|
306
|
+
return 'low';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* 重複程式碼檢測器主類
|
|
311
|
+
*/
|
|
312
|
+
export class DuplicationDetector {
|
|
313
|
+
type1Detector = new Type1CloneDetector();
|
|
314
|
+
type2Detector = new Type2CloneDetector();
|
|
315
|
+
type3Detector = new Type3CloneDetector();
|
|
316
|
+
/**
|
|
317
|
+
* 設定 Type-3 檢測的相似度閾值
|
|
318
|
+
*/
|
|
319
|
+
setThreshold(threshold) {
|
|
320
|
+
this.type3Detector.setThreshold(threshold);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 檢測程式碼克隆 (兼容舊介面)
|
|
324
|
+
*/
|
|
325
|
+
detectClones(fragments) {
|
|
326
|
+
const fullConfig = {
|
|
327
|
+
minLines: 3,
|
|
328
|
+
minTokens: 5,
|
|
329
|
+
similarityThreshold: 0.7,
|
|
330
|
+
ignoreWhitespace: true,
|
|
331
|
+
ignoreComments: true
|
|
332
|
+
};
|
|
333
|
+
// 直接檢測不同類型的克隆
|
|
334
|
+
const type1Clones = this.type1Detector.detect(fragments, fullConfig);
|
|
335
|
+
const type2Clones = this.type2Detector.detect(fragments, fullConfig);
|
|
336
|
+
const type3Clones = this.type3Detector.detect(fragments, fullConfig);
|
|
337
|
+
return [...type1Clones, ...type2Clones, ...type3Clones];
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 檢測檔案中的重複程式碼
|
|
341
|
+
* @param files 檔案路徑列表
|
|
342
|
+
* @param config 檢測配置
|
|
343
|
+
* @returns 重複程式碼檢測結果
|
|
344
|
+
*/
|
|
345
|
+
async detect(files, config) {
|
|
346
|
+
// 輸入驗證
|
|
347
|
+
if (!Array.isArray(files)) {
|
|
348
|
+
throw new Error('檔案列表必須是陣列');
|
|
349
|
+
}
|
|
350
|
+
const fullConfig = {
|
|
351
|
+
minLines: 3,
|
|
352
|
+
minTokens: 5,
|
|
353
|
+
similarityThreshold: 0.7,
|
|
354
|
+
ignoreWhitespace: true,
|
|
355
|
+
ignoreComments: true,
|
|
356
|
+
...config
|
|
357
|
+
};
|
|
358
|
+
// 解析所有檔案並提取程式碼片段
|
|
359
|
+
const fragments = await this.extractFragments(files);
|
|
360
|
+
// 檢測不同類型的克隆
|
|
361
|
+
const type1Clones = this.type1Detector.detect(fragments, fullConfig);
|
|
362
|
+
const type2Clones = this.type2Detector.detect(fragments, fullConfig);
|
|
363
|
+
const type3Clones = this.type3Detector.detect(fragments, fullConfig);
|
|
364
|
+
return [...type1Clones, ...type2Clones, ...type3Clones];
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* 從檔案中提取程式碼片段
|
|
368
|
+
*/
|
|
369
|
+
async extractFragments(files) {
|
|
370
|
+
const { readFile } = await import('fs/promises');
|
|
371
|
+
const fragments = [];
|
|
372
|
+
for (const file of files) {
|
|
373
|
+
try {
|
|
374
|
+
const content = await readFile(file, 'utf-8');
|
|
375
|
+
const fileFragments = this.parseFileToFragments(file, content);
|
|
376
|
+
fragments.push(...fileFragments);
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return fragments;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 將檔案內容解析為程式碼片段(提取多種類型的代碼結構)
|
|
386
|
+
*/
|
|
387
|
+
parseFileToFragments(filePath, content) {
|
|
388
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
389
|
+
throw new Error('檔案路徑必須是非空字串');
|
|
390
|
+
}
|
|
391
|
+
if (typeof content !== 'string') {
|
|
392
|
+
throw new Error('檔案內容必須是字串');
|
|
393
|
+
}
|
|
394
|
+
const fragments = [];
|
|
395
|
+
// 1. 提取頂層註解(版權宣告等)
|
|
396
|
+
fragments.push(...this.extractTopLevelComments(filePath, content));
|
|
397
|
+
// 2. 提取常數定義
|
|
398
|
+
fragments.push(...this.extractConstantDefinitions(filePath, content));
|
|
399
|
+
// 3. 提取配置物件
|
|
400
|
+
fragments.push(...this.extractConfigObjects(filePath, content));
|
|
401
|
+
// 4. 提取方法和函式
|
|
402
|
+
fragments.push(...this.extractMethods(filePath, content));
|
|
403
|
+
// 如果仍然沒有找到任何片段,回退到按行分割
|
|
404
|
+
if (fragments.length === 0) {
|
|
405
|
+
const lines = content.split('\n');
|
|
406
|
+
for (let i = 0; i < lines.length; i += 10) {
|
|
407
|
+
const endLine = Math.min(i + 9, lines.length - 1);
|
|
408
|
+
const fragmentContent = lines.slice(i, endLine + 1).join('\n');
|
|
409
|
+
if (fragmentContent.trim().length > 0) {
|
|
410
|
+
fragments.push({
|
|
411
|
+
id: `${filePath}:${i + 1}-${endLine + 1}`,
|
|
412
|
+
ast: this.parseToAST(fragmentContent),
|
|
413
|
+
tokens: this.tokenize(fragmentContent),
|
|
414
|
+
location: {
|
|
415
|
+
file: filePath,
|
|
416
|
+
startLine: i + 1,
|
|
417
|
+
endLine: endLine + 1
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return fragments;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* 提取檔案開頭的頂層註解(如版權宣告)
|
|
427
|
+
*/
|
|
428
|
+
extractTopLevelComments(filePath, content) {
|
|
429
|
+
const fragments = [];
|
|
430
|
+
const lines = content.split('\n');
|
|
431
|
+
// 只檢查檔案開頭的註解(前 50 行內)
|
|
432
|
+
const headerLines = lines.slice(0, Math.min(50, lines.length));
|
|
433
|
+
let commentStart = -1;
|
|
434
|
+
let commentEnd = -1;
|
|
435
|
+
let inBlockComment = false;
|
|
436
|
+
for (let i = 0; i < headerLines.length; i++) {
|
|
437
|
+
const line = headerLines[i].trim();
|
|
438
|
+
// 檢測多行註解開始
|
|
439
|
+
if (line.startsWith('/**') || line.startsWith('/*')) {
|
|
440
|
+
if (commentStart === -1) {
|
|
441
|
+
commentStart = i;
|
|
442
|
+
}
|
|
443
|
+
inBlockComment = true;
|
|
444
|
+
}
|
|
445
|
+
// 檢測多行註解結束
|
|
446
|
+
if (inBlockComment && line.includes('*/')) {
|
|
447
|
+
commentEnd = i;
|
|
448
|
+
inBlockComment = false;
|
|
449
|
+
// 提取完整的註解區塊
|
|
450
|
+
const commentContent = lines.slice(commentStart, commentEnd + 1).join('\n');
|
|
451
|
+
const lineCount = commentEnd - commentStart + 1;
|
|
452
|
+
// 只處理 >= 3 行的註解區塊
|
|
453
|
+
if (lineCount >= 3) {
|
|
454
|
+
fragments.push({
|
|
455
|
+
id: `${filePath}:comment:${commentStart + 1}-${commentEnd + 1}`,
|
|
456
|
+
ast: this.parseToAST(commentContent),
|
|
457
|
+
tokens: this.tokenize(commentContent, true), // 保留註解
|
|
458
|
+
location: {
|
|
459
|
+
file: filePath,
|
|
460
|
+
startLine: commentStart + 1,
|
|
461
|
+
endLine: commentEnd + 1
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
commentStart = -1;
|
|
466
|
+
commentEnd = -1;
|
|
467
|
+
}
|
|
468
|
+
// 遇到非空白、非註解的行就停止(不再是頂層註解)
|
|
469
|
+
if (!line.startsWith('//') &&
|
|
470
|
+
!line.startsWith('/*') &&
|
|
471
|
+
!line.startsWith('*') &&
|
|
472
|
+
!line.includes('*/') &&
|
|
473
|
+
line.length > 0) {
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return fragments;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 提取常數定義(export const、const 等)
|
|
481
|
+
*/
|
|
482
|
+
extractConstantDefinitions(filePath, content) {
|
|
483
|
+
const fragments = [];
|
|
484
|
+
// 匹配常數定義(包括物件定義)
|
|
485
|
+
// 例如:export const XXX = { ... }; 或 const XXX = { ... };
|
|
486
|
+
const constRegex = /(export\s+)?const\s+([A-Z_][A-Z0-9_]*)\s*=\s*\{/gm;
|
|
487
|
+
let match;
|
|
488
|
+
while ((match = constRegex.exec(content)) !== null) {
|
|
489
|
+
const startPos = match.index;
|
|
490
|
+
const startLine = content.substring(0, startPos).split('\n').length;
|
|
491
|
+
const constName = match[2];
|
|
492
|
+
// 找到物件定義的開始 { 位置
|
|
493
|
+
const bracePos = match.index + match[0].length - 1;
|
|
494
|
+
// 找到配對的 }
|
|
495
|
+
const endPos = this.findMatchingBrace(content, bracePos);
|
|
496
|
+
if (endPos === -1) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const endLine = content.substring(0, endPos + 1).split('\n').length;
|
|
500
|
+
const fragmentContent = content.substring(startPos, endPos + 1);
|
|
501
|
+
const lineCount = endLine - startLine + 1;
|
|
502
|
+
// 只處理 >= 3 行的常數定義
|
|
503
|
+
if (lineCount >= 3) {
|
|
504
|
+
fragments.push({
|
|
505
|
+
id: `${filePath}:const:${startLine}:${constName}`,
|
|
506
|
+
ast: this.parseToAST(fragmentContent),
|
|
507
|
+
tokens: this.tokenize(fragmentContent),
|
|
508
|
+
location: {
|
|
509
|
+
file: filePath,
|
|
510
|
+
startLine,
|
|
511
|
+
endLine
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return fragments;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* 提取配置物件(識別常見的配置模式)
|
|
520
|
+
*/
|
|
521
|
+
extractConfigObjects(filePath, content) {
|
|
522
|
+
const fragments = [];
|
|
523
|
+
// 匹配常見的配置物件模式
|
|
524
|
+
// 例如:{ host: ..., port: ..., database: ... }
|
|
525
|
+
const configKeywords = ['host', 'port', 'database', 'uri', 'url', 'connection', 'options', 'config'];
|
|
526
|
+
const lines = content.split('\n');
|
|
527
|
+
for (let i = 0; i < lines.length; i++) {
|
|
528
|
+
const line = lines[i];
|
|
529
|
+
// 檢查是否包含配置相關關鍵字
|
|
530
|
+
const hasConfigKeyword = configKeywords.some(keyword => line.includes(`${keyword}:`) || line.includes(`${keyword} =`));
|
|
531
|
+
if (hasConfigKeyword && line.includes('{')) {
|
|
532
|
+
// 找到配置物件的開始
|
|
533
|
+
const startPos = content.substring(0, content.indexOf(lines.slice(0, i + 1).join('\n'))).length;
|
|
534
|
+
const braceIndex = line.indexOf('{');
|
|
535
|
+
const bracePos = startPos + braceIndex;
|
|
536
|
+
const endPos = this.findMatchingBrace(content, bracePos);
|
|
537
|
+
if (endPos === -1) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const startLine = i + 1;
|
|
541
|
+
const endLine = content.substring(0, endPos + 1).split('\n').length;
|
|
542
|
+
const fragmentContent = content.substring(bracePos, endPos + 1);
|
|
543
|
+
const lineCount = endLine - startLine + 1;
|
|
544
|
+
// 只處理 >= 3 行的配置物件
|
|
545
|
+
if (lineCount >= 3) {
|
|
546
|
+
fragments.push({
|
|
547
|
+
id: `${filePath}:config:${startLine}-${endLine}`,
|
|
548
|
+
ast: this.parseToAST(fragmentContent),
|
|
549
|
+
tokens: this.tokenize(fragmentContent),
|
|
550
|
+
location: {
|
|
551
|
+
file: filePath,
|
|
552
|
+
startLine,
|
|
553
|
+
endLine
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
// 跳過已處理的行
|
|
557
|
+
i = endLine - 1;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return fragments;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* 提取方法和函式(原有邏輯)
|
|
565
|
+
*/
|
|
566
|
+
extractMethods(filePath, content) {
|
|
567
|
+
const fragments = [];
|
|
568
|
+
// 提取方法和函式的正則表達式(更寬鬆的匹配)
|
|
569
|
+
const methodRegex = /(\s*)(async\s+)?(\w+)\s*\([^)]*\)\s*\{/gm;
|
|
570
|
+
let match;
|
|
571
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
572
|
+
const startPos = match.index;
|
|
573
|
+
const startLine = content.substring(0, startPos).split('\n').length;
|
|
574
|
+
const methodName = match[3];
|
|
575
|
+
// 找到方法開始的 { 位置
|
|
576
|
+
const bracePos = startPos + match[0].length - 1;
|
|
577
|
+
// 找到方法的結束位置(配對大括號)
|
|
578
|
+
const endPos = this.findMatchingBrace(content, bracePos);
|
|
579
|
+
if (endPos === -1) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const endLine = content.substring(0, endPos + 1).split('\n').length;
|
|
583
|
+
const fragmentContent = content.substring(startPos, endPos + 1);
|
|
584
|
+
const lineCount = endLine - startLine + 1;
|
|
585
|
+
// 只處理行數 >= 3 的方法
|
|
586
|
+
if (lineCount >= 3) {
|
|
587
|
+
const tokens = this.tokenize(fragmentContent);
|
|
588
|
+
fragments.push({
|
|
589
|
+
id: `${filePath}:${startLine}:${methodName}`,
|
|
590
|
+
ast: this.parseToAST(fragmentContent),
|
|
591
|
+
tokens,
|
|
592
|
+
location: {
|
|
593
|
+
file: filePath,
|
|
594
|
+
startLine,
|
|
595
|
+
endLine
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return fragments;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* 找到配對的右大括號
|
|
604
|
+
*/
|
|
605
|
+
findMatchingBrace(content, startPos) {
|
|
606
|
+
let braceCount = 1;
|
|
607
|
+
let inString = false;
|
|
608
|
+
let stringChar = '';
|
|
609
|
+
for (let i = startPos + 1; i < content.length; i++) {
|
|
610
|
+
const char = content[i];
|
|
611
|
+
const prevChar = content[i - 1];
|
|
612
|
+
// 處理字串
|
|
613
|
+
if ((char === '"' || char === '\'' || char === '`') && prevChar !== '\\') {
|
|
614
|
+
if (!inString) {
|
|
615
|
+
inString = true;
|
|
616
|
+
stringChar = char;
|
|
617
|
+
}
|
|
618
|
+
else if (char === stringChar) {
|
|
619
|
+
inString = false;
|
|
620
|
+
}
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (!inString) {
|
|
624
|
+
if (char === '{') {
|
|
625
|
+
braceCount++;
|
|
626
|
+
}
|
|
627
|
+
else if (char === '}') {
|
|
628
|
+
braceCount--;
|
|
629
|
+
if (braceCount === 0) {
|
|
630
|
+
return i;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return -1;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 簡化的 AST 解析
|
|
639
|
+
*/
|
|
640
|
+
parseToAST(content) {
|
|
641
|
+
// 簡化實作:建立基本的 AST 結構
|
|
642
|
+
return {
|
|
643
|
+
type: 'Program',
|
|
644
|
+
children: content.split('\n').map(line => ({
|
|
645
|
+
type: 'Statement',
|
|
646
|
+
value: line.trim()
|
|
647
|
+
}))
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* 程式碼分詞
|
|
652
|
+
* @param content 程式碼內容
|
|
653
|
+
* @param preserveComments 是否保留註解(預設false會過濾註解)
|
|
654
|
+
*/
|
|
655
|
+
tokenize(content, preserveComments = false) {
|
|
656
|
+
// 按空白和操作符分割
|
|
657
|
+
const tokens = content
|
|
658
|
+
.split(/(\s+|[(){}[\];,.]|\+|\-|\*|\/|=|!|<|>|&|\|)/)
|
|
659
|
+
.filter(token => token.trim().length > 0);
|
|
660
|
+
// 如果不保留註解,過濾掉註解 tokens
|
|
661
|
+
if (!preserveComments) {
|
|
662
|
+
return tokens.filter(token => {
|
|
663
|
+
const trimmed = token.trim();
|
|
664
|
+
return !trimmed.startsWith('//') &&
|
|
665
|
+
!trimmed.startsWith('/*') &&
|
|
666
|
+
!trimmed.startsWith('*') &&
|
|
667
|
+
!trimmed.includes('*/');
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return tokens;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* 獲取重複檢測統計
|
|
674
|
+
*/
|
|
675
|
+
async getStatistics(clones) {
|
|
676
|
+
const byType = { 'type-1': 0, 'type-2': 0, 'type-3': 0 };
|
|
677
|
+
const bySeverity = { low: 0, medium: 0, high: 0 };
|
|
678
|
+
let totalSimilarity = 0;
|
|
679
|
+
let totalLines = 0;
|
|
680
|
+
for (const clone of clones) {
|
|
681
|
+
byType[clone.type]++;
|
|
682
|
+
bySeverity[clone.severity]++;
|
|
683
|
+
totalSimilarity += clone.similarity;
|
|
684
|
+
totalLines += clone.lines * clone.instances.length;
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
totalClones: clones.length,
|
|
688
|
+
byType,
|
|
689
|
+
bySeverity,
|
|
690
|
+
averageSimilarity: clones.length > 0 ? totalSimilarity / clones.length : 0,
|
|
691
|
+
totalDuplicatedLines: totalLines
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
//# sourceMappingURL=duplication-detector.js.map
|