agent-ide 0.6.0 → 0.7.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/dist/core/change-signature/change-signature-service.d.ts.map +1 -1
- package/dist/core/change-signature/change-signature-service.js +16 -5
- package/dist/core/change-signature/change-signature-service.js.map +1 -1
- package/dist/core/change-signature/signature-parser.d.ts.map +1 -1
- package/dist/core/change-signature/signature-parser.js +28 -2
- package/dist/core/change-signature/signature-parser.js.map +1 -1
- package/dist/core/change-signature/types.d.ts +2 -0
- package/dist/core/change-signature/types.d.ts.map +1 -1
- package/dist/core/change-signature/types.js +2 -0
- package/dist/core/change-signature/types.js.map +1 -1
- package/dist/core/dead-code/dead-code-remover.d.ts +133 -0
- package/dist/core/dead-code/dead-code-remover.d.ts.map +1 -0
- package/dist/core/dead-code/dead-code-remover.js +803 -0
- package/dist/core/dead-code/dead-code-remover.js.map +1 -0
- package/dist/core/dead-code/index.d.ts +4 -3
- package/dist/core/dead-code/index.d.ts.map +1 -1
- package/dist/core/dead-code/index.js +3 -2
- package/dist/core/dead-code/index.js.map +1 -1
- package/dist/core/dead-code/types.d.ts +118 -1
- package/dist/core/dead-code/types.d.ts.map +1 -1
- package/dist/core/dead-code/types.js +34 -1
- package/dist/core/dead-code/types.js.map +1 -1
- package/dist/core/shared/symbol-finder.d.ts +4 -0
- package/dist/core/shared/symbol-finder.d.ts.map +1 -1
- package/dist/core/shared/symbol-finder.js +41 -0
- package/dist/core/shared/symbol-finder.js.map +1 -1
- package/dist/infrastructure/formatters/preview-converter.d.ts +62 -0
- package/dist/infrastructure/formatters/preview-converter.d.ts.map +1 -1
- package/dist/infrastructure/formatters/preview-converter.js +82 -0
- package/dist/infrastructure/formatters/preview-converter.js.map +1 -1
- package/dist/infrastructure/formatters/types.d.ts +2 -1
- package/dist/infrastructure/formatters/types.d.ts.map +1 -1
- package/dist/infrastructure/formatters/types.js +1 -0
- package/dist/infrastructure/formatters/types.js.map +1 -1
- package/dist/interfaces/cli/commands/change-signature.command.js +3 -3
- package/dist/interfaces/cli/commands/change-signature.command.js.map +1 -1
- package/dist/interfaces/cli/commands/deadcode.command.d.ts +1 -1
- package/dist/interfaces/cli/commands/deadcode.command.d.ts.map +1 -1
- package/dist/interfaces/cli/commands/deadcode.command.js +102 -41
- package/dist/interfaces/cli/commands/deadcode.command.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead Code 刪除器
|
|
3
|
+
* 負責刪除未使用的程式碼並清理相關 import
|
|
4
|
+
*/
|
|
5
|
+
import { minimatch } from 'minimatch';
|
|
6
|
+
import { createSymbolFinder, SymbolReferenceType } from '../shared/symbol-finder.js';
|
|
7
|
+
import { DEFAULT_REMOVAL_OPTIONS } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Dead Code 刪除器
|
|
10
|
+
*/
|
|
11
|
+
export class DeadCodeRemover {
|
|
12
|
+
fileSystem;
|
|
13
|
+
parserRegistry;
|
|
14
|
+
options;
|
|
15
|
+
fileCache = new Map();
|
|
16
|
+
symbolFinder;
|
|
17
|
+
constructor(fileSystem, parserRegistry, options) {
|
|
18
|
+
this.fileSystem = fileSystem;
|
|
19
|
+
this.parserRegistry = parserRegistry;
|
|
20
|
+
this.options = { ...DEFAULT_REMOVAL_OPTIONS, ...options };
|
|
21
|
+
this.symbolFinder = createSymbolFinder(parserRegistry, fileSystem);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 預覽刪除操作
|
|
25
|
+
*/
|
|
26
|
+
async preview(deadCodeItems) {
|
|
27
|
+
try {
|
|
28
|
+
// 1. 過濾符合條件的項目
|
|
29
|
+
const { filteredItems, warnings } = this.filterItems(deadCodeItems);
|
|
30
|
+
if (filteredItems.length === 0) {
|
|
31
|
+
return this.createEmptyPreview(warnings);
|
|
32
|
+
}
|
|
33
|
+
// 2. 產生刪除操作
|
|
34
|
+
const { operations: removals, warnings: removalWarnings } = await this.generateRemovalOperations(filteredItems);
|
|
35
|
+
warnings.push(...removalWarnings);
|
|
36
|
+
// 3. 分析並產生 import 清理操作
|
|
37
|
+
let importCleanups = [];
|
|
38
|
+
if (this.options.cleanupImports) {
|
|
39
|
+
const importResult = await this.analyzeImportCleanups(removals);
|
|
40
|
+
importCleanups = importResult.cleanups;
|
|
41
|
+
warnings.push(...importResult.warnings);
|
|
42
|
+
}
|
|
43
|
+
// 4. 計算統計
|
|
44
|
+
const summary = this.calculateSummary(removals, importCleanups);
|
|
45
|
+
// 5. 收集影響的檔案
|
|
46
|
+
const affectedFiles = this.collectAffectedFiles(removals, importCleanups);
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
removals,
|
|
50
|
+
importCleanups,
|
|
51
|
+
affectedFiles,
|
|
52
|
+
summary,
|
|
53
|
+
warnings: warnings.length > 0 ? warnings : undefined
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
removals: [],
|
|
60
|
+
importCleanups: [],
|
|
61
|
+
affectedFiles: [],
|
|
62
|
+
summary: this.createEmptySummary(),
|
|
63
|
+
errors: [error instanceof Error ? error.message : String(error)]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 執行刪除(非 dry-run 時)
|
|
69
|
+
*/
|
|
70
|
+
async execute(preview) {
|
|
71
|
+
if (!preview.success) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
updatedFiles: [],
|
|
75
|
+
summary: preview.summary,
|
|
76
|
+
errors: preview.errors
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const errors = [];
|
|
80
|
+
const updatedFiles = [];
|
|
81
|
+
// 按檔案分組操作
|
|
82
|
+
const fileOperations = this.groupOperationsByFile(preview);
|
|
83
|
+
// 逐檔案套用變更
|
|
84
|
+
for (const [filePath, operations] of fileOperations) {
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.applyFileOperations(filePath, operations);
|
|
87
|
+
updatedFiles.push(result);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
errors.push(`檔案 ${filePath} 處理失敗: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
success: errors.length === 0,
|
|
95
|
+
updatedFiles,
|
|
96
|
+
summary: preview.summary,
|
|
97
|
+
errors: errors.length > 0 ? errors : undefined
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 過濾符合刪除條件的項目
|
|
102
|
+
*/
|
|
103
|
+
filterItems(items) {
|
|
104
|
+
const filteredItems = [];
|
|
105
|
+
const warnings = [];
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
// 信心度檢查
|
|
108
|
+
if (item.confidence < this.options.minConfidence) {
|
|
109
|
+
warnings.push(`跳過 ${item.name}:信心度 ${(item.confidence * 100).toFixed(0)}% 低於門檻 ${(this.options.minConfidence * 100).toFixed(0)}%`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// 排除檔案模式(支援 glob 匹配)
|
|
113
|
+
if (this.options.excludeFiles.some(pattern => this.matchesExcludePattern(item.location.filePath, pattern))) {
|
|
114
|
+
warnings.push(`跳過 ${item.name}:檔案被排除`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// 排除符號名稱
|
|
118
|
+
if (this.options.excludeSymbols.includes(item.name)) {
|
|
119
|
+
warnings.push(`跳過 ${item.name}:符號被排除`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
filteredItems.push(item);
|
|
123
|
+
}
|
|
124
|
+
return { filteredItems, warnings };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 產生刪除操作
|
|
128
|
+
*/
|
|
129
|
+
async generateRemovalOperations(items) {
|
|
130
|
+
const operations = [];
|
|
131
|
+
const warnings = [];
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
const content = await this.readFile(item.location.filePath);
|
|
134
|
+
if (!content) {
|
|
135
|
+
warnings.push(`跳過 ${item.name}:無法讀取檔案 ${item.location.filePath}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// 擴展範圍以包含完整宣告(含 JSDoc 註解)
|
|
139
|
+
const expandedRange = this.expandRangeToFullDeclaration(content, item.location.range, item.type);
|
|
140
|
+
const originalCode = this.extractCode(content, expandedRange);
|
|
141
|
+
operations.push({
|
|
142
|
+
filePath: item.location.filePath,
|
|
143
|
+
range: expandedRange,
|
|
144
|
+
originalCode,
|
|
145
|
+
symbolName: item.name,
|
|
146
|
+
symbolType: item.type,
|
|
147
|
+
confidence: item.confidence
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { operations, warnings };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 分析需要清理的 import
|
|
154
|
+
* 支援部分清理:當 import { A, B, C } 中只有部分符號未使用時,保留其他符號
|
|
155
|
+
*/
|
|
156
|
+
async analyzeImportCleanups(removals) {
|
|
157
|
+
const cleanups = [];
|
|
158
|
+
const warnings = [];
|
|
159
|
+
const affectedFiles = new Set(removals.map(r => r.filePath));
|
|
160
|
+
const removedSymbols = new Set(removals.map(r => r.symbolName));
|
|
161
|
+
for (const filePath of affectedFiles) {
|
|
162
|
+
const content = await this.readFile(filePath);
|
|
163
|
+
if (!content) {
|
|
164
|
+
warnings.push(`跳過 import 清理:無法讀取檔案 ${filePath}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// 解析 import 語句(以語句為單位)
|
|
168
|
+
const importStatements = this.parseImportStatements(content);
|
|
169
|
+
const fileRemovals = removals.filter(r => r.filePath === filePath);
|
|
170
|
+
for (const stmt of importStatements) {
|
|
171
|
+
// 找出此 import 中需要清理的符號
|
|
172
|
+
const unusedSymbols = [];
|
|
173
|
+
const usedSymbols = [];
|
|
174
|
+
for (const symbol of stmt.symbols) {
|
|
175
|
+
// 符號是否在被刪除的列表中,且刪除後不再使用
|
|
176
|
+
if (removedSymbols.has(symbol.name)) {
|
|
177
|
+
const stillUsed = await this.isImportStillUsed(filePath, symbol.name, fileRemovals);
|
|
178
|
+
if (!stillUsed) {
|
|
179
|
+
unusedSymbols.push(symbol.name);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
usedSymbols.push(symbol.name);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
usedSymbols.push(symbol.name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// 沒有需要清理的符號,跳過
|
|
190
|
+
if (unusedSymbols.length === 0) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// 判斷清理類型
|
|
194
|
+
if (usedSymbols.length === 0) {
|
|
195
|
+
// 所有符號都未使用,刪除整行
|
|
196
|
+
cleanups.push({
|
|
197
|
+
filePath,
|
|
198
|
+
range: stmt.range,
|
|
199
|
+
originalImport: stmt.statement,
|
|
200
|
+
unusedSymbols,
|
|
201
|
+
cleanupType: 'delete'
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// 部分符號仍在使用,產生新的 import 語句
|
|
206
|
+
const newImport = this.generatePartialImport(stmt, usedSymbols);
|
|
207
|
+
if (newImport) {
|
|
208
|
+
cleanups.push({
|
|
209
|
+
filePath,
|
|
210
|
+
range: stmt.range,
|
|
211
|
+
originalImport: stmt.statement,
|
|
212
|
+
unusedSymbols,
|
|
213
|
+
cleanupType: 'partial',
|
|
214
|
+
newImport
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { cleanups, warnings };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 產生部分清理後的 import 語句
|
|
224
|
+
* 支援:純 named import、混合 default + named import
|
|
225
|
+
*/
|
|
226
|
+
generatePartialImport(stmt, usedSymbols) {
|
|
227
|
+
// Namespace import 不支援部分清理(整體使用)
|
|
228
|
+
if (stmt.isNamespace) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
// 從原始語句中提取 from 路徑
|
|
232
|
+
const fromMatch = stmt.statement.match(/from\s+(['"])(.+?)\1/);
|
|
233
|
+
if (!fromMatch) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const fromPath = fromMatch[2];
|
|
237
|
+
const quote = fromMatch[1];
|
|
238
|
+
// 分離 default 和 named symbols
|
|
239
|
+
const defaultSymbol = stmt.symbols.find(s => s.isDefault);
|
|
240
|
+
const namedSymbols = stmt.symbols.filter(s => !s.isDefault);
|
|
241
|
+
// 檢查 default import 是否仍需保留
|
|
242
|
+
const keepDefault = defaultSymbol && usedSymbols.includes(defaultSymbol.name);
|
|
243
|
+
// 過濾出需要保留的 named symbols,並保留別名資訊
|
|
244
|
+
// 同時檢查 name 和 alias,因為 usedSymbols 可能包含別名
|
|
245
|
+
const keptNamedSymbols = namedSymbols
|
|
246
|
+
.filter(s => usedSymbols.includes(s.name) || (s.alias && usedSymbols.includes(s.alias)))
|
|
247
|
+
.map(s => s.alias ? `${s.name} as ${s.alias}` : s.name);
|
|
248
|
+
// 判斷是否需要 type 關鍵字(僅對純 named import)
|
|
249
|
+
const isTypeImport = stmt.statement.match(/import\s+type\s*\{/);
|
|
250
|
+
const typePrefix = isTypeImport ? 'type ' : '';
|
|
251
|
+
// 建構新的 import 語句
|
|
252
|
+
if (keepDefault && keptNamedSymbols.length > 0) {
|
|
253
|
+
// 混合格式:import X, { Y, Z } from '...'
|
|
254
|
+
return `import ${defaultSymbol.name}, { ${keptNamedSymbols.join(', ')} } from ${quote}${fromPath}${quote};`;
|
|
255
|
+
}
|
|
256
|
+
else if (keepDefault) {
|
|
257
|
+
// 只有 default:import X from '...'
|
|
258
|
+
return `import ${defaultSymbol.name} from ${quote}${fromPath}${quote};`;
|
|
259
|
+
}
|
|
260
|
+
else if (keptNamedSymbols.length > 0) {
|
|
261
|
+
// 只有 named:import { Y, Z } from '...'
|
|
262
|
+
return `import ${typePrefix}{ ${keptNamedSymbols.join(', ')} } from ${quote}${fromPath}${quote};`;
|
|
263
|
+
}
|
|
264
|
+
// 沒有任何符號需要保留
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 解析 import 語句(以語句為單位)
|
|
269
|
+
* 支援 named import, default import, namespace import, 多行 import
|
|
270
|
+
*/
|
|
271
|
+
parseImportStatements(content) {
|
|
272
|
+
const statements = [];
|
|
273
|
+
const lines = content.split('\n');
|
|
274
|
+
// 用於處理多行 import
|
|
275
|
+
let multiLineImport = '';
|
|
276
|
+
let multiLineStartLine = -1;
|
|
277
|
+
let multiLineCount = 0;
|
|
278
|
+
const MAX_MULTILINE_IMPORT = 20; // 安全限制:最多 20 行
|
|
279
|
+
for (let i = 0; i < lines.length; i++) {
|
|
280
|
+
const line = lines[i];
|
|
281
|
+
const lineNumber = i + 1;
|
|
282
|
+
// 處理多行 import
|
|
283
|
+
if (multiLineImport) {
|
|
284
|
+
multiLineImport += '\n' + line;
|
|
285
|
+
multiLineCount++;
|
|
286
|
+
// 檢測結束條件:有 from 和 引號,或超過安全限制
|
|
287
|
+
const cleanLine = line.replace(/\/\/.*/, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
288
|
+
const isComplete = cleanLine.includes('from') && /['"]/.test(cleanLine);
|
|
289
|
+
const isOverLimit = multiLineCount > MAX_MULTILINE_IMPORT;
|
|
290
|
+
if (isComplete || isOverLimit) {
|
|
291
|
+
// 多行 import 結束
|
|
292
|
+
const stmt = this.parseImportStatementLine(multiLineImport, multiLineStartLine, lineNumber, lines);
|
|
293
|
+
if (stmt) {
|
|
294
|
+
statements.push(stmt);
|
|
295
|
+
}
|
|
296
|
+
multiLineImport = '';
|
|
297
|
+
multiLineStartLine = -1;
|
|
298
|
+
multiLineCount = 0;
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
// 檢查是否為多行 import 開始(有 { 但沒有 } 或沒有 from)
|
|
303
|
+
if (line.match(/^\s*import\s+(?:type\s*)?\{/) && !line.includes('}')) {
|
|
304
|
+
multiLineImport = line;
|
|
305
|
+
multiLineStartLine = lineNumber;
|
|
306
|
+
multiLineCount = 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
// 單行處理
|
|
310
|
+
const stmt = this.parseImportStatementLine(line, lineNumber, lineNumber, lines);
|
|
311
|
+
if (stmt) {
|
|
312
|
+
statements.push(stmt);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return statements;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 解析單行或合併後的 import 語句
|
|
319
|
+
*/
|
|
320
|
+
parseImportStatementLine(line, startLine, endLine, lines) {
|
|
321
|
+
const trimmedLine = line.replace(/\s+/g, ' ').trim();
|
|
322
|
+
// 不是 import 語句
|
|
323
|
+
if (!trimmedLine.startsWith('import ')) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
// Side-effect import: import '...' (沒有符號)
|
|
327
|
+
if (trimmedLine.match(/^import\s+['"][^'"]+['"]/)) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const range = {
|
|
331
|
+
start: { line: startLine, column: 1, offset: undefined },
|
|
332
|
+
end: { line: endLine, column: (lines[endLine - 1] || '').length + 1, offset: undefined }
|
|
333
|
+
};
|
|
334
|
+
const symbols = [];
|
|
335
|
+
let hasDefault = false;
|
|
336
|
+
let isNamespace = false;
|
|
337
|
+
// 1. Namespace import: import * as X from '...'
|
|
338
|
+
const namespaceMatch = trimmedLine.match(/import\s+\*\s+as\s+(\w+)\s+from/);
|
|
339
|
+
if (namespaceMatch) {
|
|
340
|
+
symbols.push({ name: namespaceMatch[1], isNamespace: true });
|
|
341
|
+
isNamespace = true;
|
|
342
|
+
return { statement: trimmedLine, range, symbols, hasDefault, isNamespace };
|
|
343
|
+
}
|
|
344
|
+
// 2. Default import with named: import X, { Y, Z } from '...'
|
|
345
|
+
const defaultWithNamedMatch = trimmedLine.match(/import\s+(\w+)\s*,\s*\{([^}]+)\}\s*from/);
|
|
346
|
+
if (defaultWithNamedMatch) {
|
|
347
|
+
hasDefault = true;
|
|
348
|
+
symbols.push({ name: defaultWithNamedMatch[1], isDefault: true });
|
|
349
|
+
this.parseNamedSymbols(defaultWithNamedMatch[2], symbols);
|
|
350
|
+
return { statement: trimmedLine, range, symbols, hasDefault, isNamespace };
|
|
351
|
+
}
|
|
352
|
+
// 3. Default import only: import X from '...'
|
|
353
|
+
const defaultMatch = trimmedLine.match(/import\s+(\w+)\s+from\s+['"]/);
|
|
354
|
+
if (defaultMatch && !trimmedLine.includes('{')) {
|
|
355
|
+
hasDefault = true;
|
|
356
|
+
symbols.push({ name: defaultMatch[1], isDefault: true });
|
|
357
|
+
return { statement: trimmedLine, range, symbols, hasDefault, isNamespace };
|
|
358
|
+
}
|
|
359
|
+
// 4. Named import: import { X, Y } from '...' or import type { X } from '...'
|
|
360
|
+
const namedImportMatch = trimmedLine.match(/import\s+(?:type\s*)?\{([^}]+)\}\s*from/);
|
|
361
|
+
if (namedImportMatch) {
|
|
362
|
+
this.parseNamedSymbols(namedImportMatch[1], symbols);
|
|
363
|
+
if (symbols.length > 0) {
|
|
364
|
+
return { statement: trimmedLine, range, symbols, hasDefault, isNamespace };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* 解析 named import 中的符號
|
|
371
|
+
*/
|
|
372
|
+
parseNamedSymbols(symbolsStr, symbols) {
|
|
373
|
+
const parts = symbolsStr.split(',').map(s => s.trim());
|
|
374
|
+
for (const part of parts) {
|
|
375
|
+
// 跳過空字串和 type-only imports
|
|
376
|
+
if (!part || part.startsWith('type ')) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
// 處理 as 別名: X as Y
|
|
380
|
+
const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
381
|
+
if (asMatch) {
|
|
382
|
+
symbols.push({ name: asMatch[1], alias: asMatch[2] });
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const cleanSymbol = part.trim();
|
|
386
|
+
if (cleanSymbol) {
|
|
387
|
+
symbols.push({ name: cleanSymbol });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 檢查 import 是否仍被使用
|
|
394
|
+
* 使用 SymbolFinder.findReferencesInFile 進行語義分析
|
|
395
|
+
*/
|
|
396
|
+
async isImportStillUsed(filePath, symbolName, removalsInFile) {
|
|
397
|
+
// 使用 SymbolFinder 查找該檔案中的所有引用
|
|
398
|
+
const references = await this.symbolFinder.findReferencesInFile(filePath, symbolName);
|
|
399
|
+
// 過濾掉 import 類型的引用(import 語句本身)
|
|
400
|
+
const usageRefs = references.filter(ref => ref.type === SymbolReferenceType.Usage);
|
|
401
|
+
// 過濾掉被刪除程式碼區塊內的引用
|
|
402
|
+
const remainingRefs = usageRefs.filter(ref => {
|
|
403
|
+
const refLine = ref.location.range.start.line;
|
|
404
|
+
// 檢查引用是否在任一刪除範圍內
|
|
405
|
+
for (const removal of removalsInFile) {
|
|
406
|
+
if (refLine >= removal.range.start.line && refLine <= removal.range.end.line) {
|
|
407
|
+
return false; // 在刪除範圍內,過濾掉
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
});
|
|
412
|
+
// 如果還有剩餘的使用引用,表示 import 仍需要
|
|
413
|
+
return remainingRefs.length > 0;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 移除註解和字串,用於準確檢測符號使用
|
|
417
|
+
*/
|
|
418
|
+
removeCommentsAndStrings(content) {
|
|
419
|
+
let result = content;
|
|
420
|
+
// 移除多行註解 /* ... */
|
|
421
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
422
|
+
// 移除單行註解 // ...
|
|
423
|
+
result = result.replace(/\/\/[^\n]*/g, '');
|
|
424
|
+
// 移除模板字串 `...`(簡化處理,不處理嵌套)
|
|
425
|
+
result = result.replace(/`(?:[^`\\]|\\.)*`/g, '""');
|
|
426
|
+
// 移除雙引號字串 "..."
|
|
427
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
428
|
+
// 移除單引號字串 '...'
|
|
429
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, '\'\'');
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 擴展範圍至完整宣告(包含前導註解和空行)
|
|
434
|
+
* 使用清理後的內容進行括號匹配,避免字串/註解中的括號干擾
|
|
435
|
+
*/
|
|
436
|
+
expandRangeToFullDeclaration(content, range, symbolType) {
|
|
437
|
+
const lines = content.split('\n');
|
|
438
|
+
let startLine = range.start.line - 1; // 轉為 0-based
|
|
439
|
+
// 向上擴展:包含 JSDoc 註解和裝飾器
|
|
440
|
+
while (startLine > 0) {
|
|
441
|
+
const prevLine = lines[startLine - 1].trim();
|
|
442
|
+
if (prevLine.endsWith('*/') ||
|
|
443
|
+
prevLine.startsWith('*') ||
|
|
444
|
+
prevLine.startsWith('//') ||
|
|
445
|
+
prevLine.startsWith('@') ||
|
|
446
|
+
prevLine === '') {
|
|
447
|
+
startLine--;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// 向下擴展:確保包含完整的結尾
|
|
454
|
+
let endLine = range.end.line - 1;
|
|
455
|
+
// 對於 class/function,需要找到對應的結尾括號
|
|
456
|
+
if (symbolType === 'class' || symbolType === 'function') {
|
|
457
|
+
let braceCount = 0;
|
|
458
|
+
let foundOpenBrace = false;
|
|
459
|
+
for (let i = range.start.line - 1; i < lines.length; i++) {
|
|
460
|
+
// 清理該行的註解和字串,避免括號誤判
|
|
461
|
+
const cleanLine = this.removeCommentsAndStringsFromLine(lines[i]);
|
|
462
|
+
for (const char of cleanLine) {
|
|
463
|
+
if (char === '{') {
|
|
464
|
+
braceCount++;
|
|
465
|
+
foundOpenBrace = true;
|
|
466
|
+
}
|
|
467
|
+
if (char === '}') {
|
|
468
|
+
braceCount--;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
472
|
+
endLine = i;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// 對於 variable(可能是 arrow function),只有當包含 { 時才做括號匹配
|
|
478
|
+
if (symbolType === 'variable') {
|
|
479
|
+
const startLineContent = lines[range.start.line - 1] || '';
|
|
480
|
+
// 檢查是否包含 arrow function 的 block body
|
|
481
|
+
if (startLineContent.includes('=>') && startLineContent.includes('{')) {
|
|
482
|
+
let braceCount = 0;
|
|
483
|
+
let foundOpenBrace = false;
|
|
484
|
+
for (let i = range.start.line - 1; i < lines.length; i++) {
|
|
485
|
+
const cleanLine = this.removeCommentsAndStringsFromLine(lines[i]);
|
|
486
|
+
for (const char of cleanLine) {
|
|
487
|
+
if (char === '{') {
|
|
488
|
+
braceCount++;
|
|
489
|
+
foundOpenBrace = true;
|
|
490
|
+
}
|
|
491
|
+
if (char === '}') {
|
|
492
|
+
braceCount--;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
496
|
+
endLine = i;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// 包含後續空行(最多一行)
|
|
503
|
+
if (endLine < lines.length - 1 && lines[endLine + 1].trim() === '') {
|
|
504
|
+
endLine++;
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
start: { line: startLine + 1, column: 1, offset: undefined },
|
|
508
|
+
end: { line: endLine + 1, column: lines[endLine].length + 1, offset: undefined }
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* 移除單行中的註解和字串(用於括號匹配)
|
|
513
|
+
*/
|
|
514
|
+
removeCommentsAndStringsFromLine(line) {
|
|
515
|
+
let result = line;
|
|
516
|
+
// 移除單行註解 // ...
|
|
517
|
+
const commentIndex = result.indexOf('//');
|
|
518
|
+
if (commentIndex !== -1) {
|
|
519
|
+
// 確保 // 不在字串中
|
|
520
|
+
const beforeComment = result.substring(0, commentIndex);
|
|
521
|
+
const quoteCount = (beforeComment.match(/['"]/g) || []).length;
|
|
522
|
+
if (quoteCount % 2 === 0) {
|
|
523
|
+
result = beforeComment;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// 移除字串(簡化處理)
|
|
527
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
528
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, '\'\'');
|
|
529
|
+
result = result.replace(/`(?:[^`\\]|\\.)*`/g, '""');
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* 提取程式碼
|
|
534
|
+
*/
|
|
535
|
+
extractCode(content, range) {
|
|
536
|
+
const lines = content.split('\n');
|
|
537
|
+
// 邊界檢查:確保索引在有效範圍內
|
|
538
|
+
const startLine = Math.max(0, Math.min(range.start.line - 1, lines.length - 1));
|
|
539
|
+
const endLine = Math.max(0, Math.min(range.end.line - 1, lines.length - 1));
|
|
540
|
+
if (startLine === endLine) {
|
|
541
|
+
const line = lines[startLine] || '';
|
|
542
|
+
return line.substring(range.start.column - 1, range.end.column - 1);
|
|
543
|
+
}
|
|
544
|
+
const result = [];
|
|
545
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
546
|
+
const line = lines[i] || '';
|
|
547
|
+
if (i === startLine) {
|
|
548
|
+
result.push(line.substring(range.start.column - 1));
|
|
549
|
+
}
|
|
550
|
+
else if (i === endLine) {
|
|
551
|
+
result.push(line.substring(0, range.end.column - 1));
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
result.push(line);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return result.join('\n');
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 按檔案分組操作(去重相同 range)
|
|
561
|
+
*/
|
|
562
|
+
groupOperationsByFile(preview) {
|
|
563
|
+
const fileOperations = new Map();
|
|
564
|
+
// 用於檢查 range 是否重複
|
|
565
|
+
const rangeKey = (r) => `${r.start.line}:${r.start.column}-${r.end.line}:${r.end.column}`;
|
|
566
|
+
const seenRanges = new Map();
|
|
567
|
+
const addOperation = (filePath, op) => {
|
|
568
|
+
const existing = fileOperations.get(filePath) || [];
|
|
569
|
+
const seen = seenRanges.get(filePath) || new Set();
|
|
570
|
+
const key = rangeKey(op.range);
|
|
571
|
+
// 去重:相同 range 只加入一次
|
|
572
|
+
if (!seen.has(key)) {
|
|
573
|
+
existing.push(op);
|
|
574
|
+
seen.add(key);
|
|
575
|
+
fileOperations.set(filePath, existing);
|
|
576
|
+
seenRanges.set(filePath, seen);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
// 加入刪除操作
|
|
580
|
+
for (const removal of preview.removals) {
|
|
581
|
+
addOperation(removal.filePath, { range: removal.range, type: 'removal' });
|
|
582
|
+
}
|
|
583
|
+
// 加入 import 清理操作
|
|
584
|
+
for (const cleanup of preview.importCleanups) {
|
|
585
|
+
if (cleanup.cleanupType === 'partial' && cleanup.newImport) {
|
|
586
|
+
addOperation(cleanup.filePath, {
|
|
587
|
+
range: cleanup.range,
|
|
588
|
+
type: 'import-partial',
|
|
589
|
+
newContent: cleanup.newImport
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
addOperation(cleanup.filePath, { range: cleanup.range, type: 'import-delete' });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return fileOperations;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* 套用檔案操作
|
|
600
|
+
*/
|
|
601
|
+
async applyFileOperations(filePath, operations) {
|
|
602
|
+
const originalContent = await this.readFile(filePath);
|
|
603
|
+
if (!originalContent) {
|
|
604
|
+
throw new Error(`無法讀取檔案: ${filePath}`);
|
|
605
|
+
}
|
|
606
|
+
// 按位置從後往前排序(避免位置偏移)
|
|
607
|
+
// 第三層:type 排序確保穩定性(import 清理優先於符號刪除)
|
|
608
|
+
const typeOrder = {
|
|
609
|
+
'import-partial': 0,
|
|
610
|
+
'import-delete': 1,
|
|
611
|
+
'removal': 2
|
|
612
|
+
};
|
|
613
|
+
const sortedOps = [...operations].sort((a, b) => {
|
|
614
|
+
if (a.range.start.line !== b.range.start.line) {
|
|
615
|
+
return b.range.start.line - a.range.start.line;
|
|
616
|
+
}
|
|
617
|
+
if (a.range.start.column !== b.range.start.column) {
|
|
618
|
+
return b.range.start.column - a.range.start.column;
|
|
619
|
+
}
|
|
620
|
+
return typeOrder[a.type] - typeOrder[b.type];
|
|
621
|
+
});
|
|
622
|
+
let lines = originalContent.split('\n');
|
|
623
|
+
let removedSymbols = 0;
|
|
624
|
+
let cleanedImports = 0;
|
|
625
|
+
for (const op of sortedOps) {
|
|
626
|
+
// 邊界檢查:確保索引在有效範圍內
|
|
627
|
+
const startLine = Math.max(0, Math.min(op.range.start.line - 1, lines.length - 1));
|
|
628
|
+
const endLine = Math.max(startLine, Math.min(op.range.end.line - 1, lines.length - 1));
|
|
629
|
+
const deleteCount = endLine - startLine + 1;
|
|
630
|
+
if (op.type === 'import-partial' && op.newContent) {
|
|
631
|
+
// 部分清理:替換而非刪除
|
|
632
|
+
if (startLine < lines.length && deleteCount > 0) {
|
|
633
|
+
// 保留原始縮排
|
|
634
|
+
const originalIndent = lines[startLine].match(/^(\s*)/)?.[1] || '';
|
|
635
|
+
lines.splice(startLine, deleteCount, originalIndent + op.newContent);
|
|
636
|
+
}
|
|
637
|
+
cleanedImports++;
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// 完整刪除
|
|
641
|
+
if (startLine < lines.length && deleteCount > 0) {
|
|
642
|
+
lines.splice(startLine, deleteCount);
|
|
643
|
+
}
|
|
644
|
+
if (op.type === 'removal') {
|
|
645
|
+
removedSymbols++;
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
cleanedImports++;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// 清理連續空行(最多保留一行)
|
|
653
|
+
lines = this.cleanupEmptyLines(lines);
|
|
654
|
+
const newContent = lines.join('\n');
|
|
655
|
+
// 寫入檔案
|
|
656
|
+
await this.writeFile(filePath, newContent);
|
|
657
|
+
return {
|
|
658
|
+
filePath,
|
|
659
|
+
removedSymbols,
|
|
660
|
+
cleanedImports
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* 清理連續空行
|
|
665
|
+
*/
|
|
666
|
+
cleanupEmptyLines(lines) {
|
|
667
|
+
const result = [];
|
|
668
|
+
let prevEmpty = false;
|
|
669
|
+
for (const line of lines) {
|
|
670
|
+
const isEmpty = line.trim() === '';
|
|
671
|
+
if (isEmpty && prevEmpty) {
|
|
672
|
+
// 跳過連續的空行
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
result.push(line);
|
|
676
|
+
prevEmpty = isEmpty;
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* 計算統計摘要
|
|
682
|
+
*/
|
|
683
|
+
calculateSummary(removals, importCleanups) {
|
|
684
|
+
const byType = {};
|
|
685
|
+
for (const removal of removals) {
|
|
686
|
+
byType[removal.symbolType] = (byType[removal.symbolType] || 0) + 1;
|
|
687
|
+
}
|
|
688
|
+
const filesAffected = new Set([
|
|
689
|
+
...removals.map(r => r.filePath),
|
|
690
|
+
...importCleanups.map(c => c.filePath)
|
|
691
|
+
]).size;
|
|
692
|
+
// 計算刪除的行數
|
|
693
|
+
let linesRemoved = 0;
|
|
694
|
+
for (const removal of removals) {
|
|
695
|
+
linesRemoved += removal.range.end.line - removal.range.start.line + 1;
|
|
696
|
+
}
|
|
697
|
+
for (const cleanup of importCleanups) {
|
|
698
|
+
linesRemoved += cleanup.range.end.line - cleanup.range.start.line + 1;
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
totalRemovals: removals.length,
|
|
702
|
+
byType,
|
|
703
|
+
filesAffected,
|
|
704
|
+
linesRemoved,
|
|
705
|
+
importsCleanedUp: importCleanups.length
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* 收集影響的檔案
|
|
710
|
+
*/
|
|
711
|
+
collectAffectedFiles(removals, importCleanups) {
|
|
712
|
+
const files = new Set();
|
|
713
|
+
for (const removal of removals) {
|
|
714
|
+
files.add(removal.filePath);
|
|
715
|
+
}
|
|
716
|
+
for (const cleanup of importCleanups) {
|
|
717
|
+
files.add(cleanup.filePath);
|
|
718
|
+
}
|
|
719
|
+
return Array.from(files);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* 建立空的預覽結果
|
|
723
|
+
*/
|
|
724
|
+
createEmptyPreview(warnings) {
|
|
725
|
+
return {
|
|
726
|
+
success: true,
|
|
727
|
+
removals: [],
|
|
728
|
+
importCleanups: [],
|
|
729
|
+
affectedFiles: [],
|
|
730
|
+
summary: this.createEmptySummary(),
|
|
731
|
+
warnings: warnings.length > 0 ? warnings : undefined
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* 建立空的統計摘要
|
|
736
|
+
*/
|
|
737
|
+
createEmptySummary() {
|
|
738
|
+
return {
|
|
739
|
+
totalRemovals: 0,
|
|
740
|
+
byType: {},
|
|
741
|
+
filesAffected: 0,
|
|
742
|
+
linesRemoved: 0,
|
|
743
|
+
importsCleanedUp: 0
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* 讀取檔案
|
|
748
|
+
*/
|
|
749
|
+
async readFile(filePath) {
|
|
750
|
+
if (this.fileCache.has(filePath)) {
|
|
751
|
+
return this.fileCache.get(filePath);
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const content = await this.fileSystem.readFile(filePath, 'utf-8');
|
|
755
|
+
const contentStr = typeof content === 'string' ? content : content.toString('utf-8');
|
|
756
|
+
this.fileCache.set(filePath, contentStr);
|
|
757
|
+
return contentStr;
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
// 清除可能存在的失敗快取,避免重試時仍返回 null
|
|
761
|
+
this.fileCache.delete(filePath);
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* 寫入檔案
|
|
767
|
+
*/
|
|
768
|
+
async writeFile(filePath, content) {
|
|
769
|
+
await this.fileSystem.writeFile(filePath, content);
|
|
770
|
+
this.fileCache.set(filePath, content);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* 檢查檔案路徑是否匹配排除模式
|
|
774
|
+
* 支援 glob 模式(如 *.test.ts、**\/__tests__/**)和簡單字串匹配
|
|
775
|
+
*/
|
|
776
|
+
matchesExcludePattern(filePath, pattern) {
|
|
777
|
+
// 如果 pattern 包含 glob 特殊字符,使用 minimatch
|
|
778
|
+
if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
|
|
779
|
+
return minimatch(filePath, pattern, { dot: true, matchBase: true });
|
|
780
|
+
}
|
|
781
|
+
// 否則使用簡單字串包含匹配(向後相容)
|
|
782
|
+
return filePath.includes(pattern);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* 逸出正則表達式特殊字符
|
|
786
|
+
*/
|
|
787
|
+
escapeRegex(text) {
|
|
788
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* 清除快取
|
|
792
|
+
*/
|
|
793
|
+
clearCache() {
|
|
794
|
+
this.fileCache.clear();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* 建立 DeadCodeRemover 實例
|
|
799
|
+
*/
|
|
800
|
+
export function createDeadCodeRemover(fileSystem, parserRegistry, options) {
|
|
801
|
+
return new DeadCodeRemover(fileSystem, parserRegistry, options);
|
|
802
|
+
}
|
|
803
|
+
//# sourceMappingURL=dead-code-remover.js.map
|