diff --git a/lib/util/removeUnusedExport.test.ts b/lib/util/removeUnusedExport.test.ts index b235ba3..94189fc 100644 --- a/lib/util/removeUnusedExport.test.ts +++ b/lib/util/removeUnusedExport.test.ts @@ -549,7 +549,11 @@ export { B };`, it('should remove export specifier for an identifier if its not used in some other file', () => { const { languageService, fileService } = setup(); - fileService.set('/app/main.ts', `import { remain } from './c';`); + fileService.set( + '/app/main.ts', + `import { remain } from './c'; +import { d } from './d';`, + ); fileService.set( '/app/a.ts', @@ -568,10 +572,18 @@ const remain = 'remain'; export { c, remain };`, ); + fileService.set( + '/app/d.ts', + `const d = 'd'; +const unused = 'unused'; +const unused2 = 'unused2'; +export { d, unused, unused2 };`, + ); + removeUnusedExport({ languageService, fileService, - targetFile: ['/app/a.ts', '/app/b.ts', '/app/c.ts'], + targetFile: ['/app/a.ts', '/app/b.ts', '/app/c.ts', '/app/d.ts'], }); assert.equal(fileService.get('/app/a.ts').trim(), `const a = 'a';`); @@ -582,6 +594,14 @@ export { c, remain };`, const remain = 'remain'; export { remain };`, ); + + assert.equal( + fileService.get('/app/d.ts').trim(), + `const d = 'd'; +const unused = 'unused'; +const unused2 = 'unused2'; +export { d };`, + ); }); it('should not remove export specifier for an identifier if it has a comment to ignore', () => { diff --git a/lib/util/removeUnusedExport.ts b/lib/util/removeUnusedExport.ts index b5da867..338b6f2 100644 --- a/lib/util/removeUnusedExport.ts +++ b/lib/util/removeUnusedExport.ts @@ -263,11 +263,25 @@ const getUpdatedExportDeclaration = ( const getTextChanges = ( languageService: ts.LanguageService, - sourceFile: ts.SourceFile, + file: string, editTracker: EditTracker, ) => { + const sourceFile = languageService.getProgram()?.getSourceFile(file); + + if (!sourceFile) { + throw new Error('source file not found'); + } + const changes: ts.TextChange[] = []; + // usually we want to remove all unused exports in one pass, but there are some cases where we need to do multiple passes + // for example, when we have multiple export specifiers in one export declaration, we want to remove them one by one because the text change range will conflict + let aborted = false; + for (const node of getUnusedExports(languageService, sourceFile)) { + if (aborted === true) { + break; + } + if (ts.isExportSpecifier(node)) { const specifierCount = Array.from(node.parent.elements).length; @@ -289,6 +303,7 @@ const getTextChanges = ( continue; } + aborted = true; changes.push({ newText: getUpdatedExportDeclaration(node.parent.parent, node), span: { @@ -381,7 +396,7 @@ const getTextChanges = ( }); } - return changes; + return { changes, done: !aborted }; }; const disabledEditTracker: EditTracker = { @@ -431,17 +446,31 @@ export const removeUnusedExport = ({ } } - const changes = getTextChanges(languageService, sourceFile, editTracker); + let content = fileService.get(file); - editTracker.end(file); + do { + const { changes, done } = getTextChanges( + languageService, + file, + editTracker, + ); + + content = applyTextChanges(content, changes); - const oldContent = fileService.get(file); - let newContent = applyTextChanges(oldContent, changes); + fileService.set(file, content); + + if (done) { + break; + } + // eslint-disable-next-line no-constant-condition + } while (true); + + editTracker.end(file); if (enableCodeFix) { // eslint-disable-next-line no-constant-condition while (true) { - fileService.set(file, newContent); + fileService.set(file, content); const result = applyCodeFix({ fixId: fixIdDelete, @@ -449,22 +478,22 @@ export const removeUnusedExport = ({ languageService, }); - if (result === newContent) { + if (result === content) { break; } - newContent = result; + content = result; } - fileService.set(file, newContent); + fileService.set(file, content); - newContent = applyCodeFix({ + content = applyCodeFix({ fixId: fixIdDeleteImports, fileName: file, languageService, }); } - fileService.set(file, newContent); + fileService.set(file, content); } };