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.
Files changed (41) hide show
  1. package/dist/core/change-signature/change-signature-service.d.ts.map +1 -1
  2. package/dist/core/change-signature/change-signature-service.js +16 -5
  3. package/dist/core/change-signature/change-signature-service.js.map +1 -1
  4. package/dist/core/change-signature/signature-parser.d.ts.map +1 -1
  5. package/dist/core/change-signature/signature-parser.js +28 -2
  6. package/dist/core/change-signature/signature-parser.js.map +1 -1
  7. package/dist/core/change-signature/types.d.ts +2 -0
  8. package/dist/core/change-signature/types.d.ts.map +1 -1
  9. package/dist/core/change-signature/types.js +2 -0
  10. package/dist/core/change-signature/types.js.map +1 -1
  11. package/dist/core/dead-code/dead-code-remover.d.ts +133 -0
  12. package/dist/core/dead-code/dead-code-remover.d.ts.map +1 -0
  13. package/dist/core/dead-code/dead-code-remover.js +803 -0
  14. package/dist/core/dead-code/dead-code-remover.js.map +1 -0
  15. package/dist/core/dead-code/index.d.ts +4 -3
  16. package/dist/core/dead-code/index.d.ts.map +1 -1
  17. package/dist/core/dead-code/index.js +3 -2
  18. package/dist/core/dead-code/index.js.map +1 -1
  19. package/dist/core/dead-code/types.d.ts +118 -1
  20. package/dist/core/dead-code/types.d.ts.map +1 -1
  21. package/dist/core/dead-code/types.js +34 -1
  22. package/dist/core/dead-code/types.js.map +1 -1
  23. package/dist/core/shared/symbol-finder.d.ts +4 -0
  24. package/dist/core/shared/symbol-finder.d.ts.map +1 -1
  25. package/dist/core/shared/symbol-finder.js +41 -0
  26. package/dist/core/shared/symbol-finder.js.map +1 -1
  27. package/dist/infrastructure/formatters/preview-converter.d.ts +62 -0
  28. package/dist/infrastructure/formatters/preview-converter.d.ts.map +1 -1
  29. package/dist/infrastructure/formatters/preview-converter.js +82 -0
  30. package/dist/infrastructure/formatters/preview-converter.js.map +1 -1
  31. package/dist/infrastructure/formatters/types.d.ts +2 -1
  32. package/dist/infrastructure/formatters/types.d.ts.map +1 -1
  33. package/dist/infrastructure/formatters/types.js +1 -0
  34. package/dist/infrastructure/formatters/types.js.map +1 -1
  35. package/dist/interfaces/cli/commands/change-signature.command.js +3 -3
  36. package/dist/interfaces/cli/commands/change-signature.command.js.map +1 -1
  37. package/dist/interfaces/cli/commands/deadcode.command.d.ts +1 -1
  38. package/dist/interfaces/cli/commands/deadcode.command.d.ts.map +1 -1
  39. package/dist/interfaces/cli/commands/deadcode.command.js +102 -41
  40. package/dist/interfaces/cli/commands/deadcode.command.js.map +1 -1
  41. 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