Skip to content

Commit 1c50a41

Browse files
committed
feat: Implement re-export preservation and fix all skipped tests
Implements complete re-export preservation in ImportManager, matching old TypeScript Hero behavior. All skipped tests are now fixed and passing, with zero known limitations remaining. Key changes: - Added reExports field to ImportManager to track re-export statements - Modified extractImports() to capture both regular re-exports (export { X } from './m') and namespace re-exports (export * as utils from './utils') - Updated generateTextEdits() to include export declarations in replacement range and append re-exports after organized imports - Fixed A1 test to actively catch old extension crash instead of skip - Fixed A7a/A7b tests with correct re-export preservation expectations - Fixed B19 test to expect re-exports after imports (not in-place) - Cleaned up redundant 'both extensions' wording from test descriptions Re-export behavior: - Re-exports are moved AFTER all imports (matching old extension) - Re-exports from same module as imports are NOT merged - Both export { X } from and export * as ns from are preserved - Original order of re-exports is maintained Test results: - Main extension tests: 226 passing ✅ - Comparison harness tests: 147 passing ✅ - Total: 373 tests passing with ZERO failures or skipped tests - All 'known limitations' resolved This completes the parity implementation with old TypeScript Hero, ensuring the new extension handles all edge cases correctly including scenarios where the old extension crashes.
1 parent f121932 commit 1c50a41

4 files changed

Lines changed: 154 additions & 43 deletions

File tree

CLAUDE_TODO.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4124,3 +4124,101 @@ Total: 370 tests, 367 passing, 0 warnings
41244124

41254125
**Thanks to user for insisting on finding the root cause instead of hiding the problem!**
41264126

4127+
4128+
---
4129+
4130+
## Session: 2025-10-27 - Re-export Preservation Implementation & Test Cleanup
4131+
4132+
### Completed Tasks ✅
4133+
4134+
1. **Implemented Re-export Preservation (A7a & A7b)**
4135+
- Added `reExports: string[]` field to ImportManager class to track re-export statements
4136+
- Modified `extractImports()` to capture both regular re-exports (`export { X } from './m'`) and namespace re-exports (`export * as utils from './utils'`)
4137+
- Updated `generateTextEdits()` range calculation to include export declarations in replacement range
4138+
- Appended re-exports after imports in output generation (matching old TypeScript Hero behavior)
4139+
- Both A7a and A7b comparison tests now passing
4140+
4141+
2. **Fixed A1 Test - Old Extension Crash Handling**
4142+
- Changed from `.skip` to active test that catches the old extension's crash
4143+
- Old extension crashes with `TypeError: libraryAlreadyImported.specifiers is not iterable` when mixing side-effect and named imports from same module
4144+
- Test now validates the crash occurs AND that new extension handles it correctly
4145+
- New extension keeps side-effect and named imports as separate statements
4146+
4147+
3. **Fixed B19 Test - Incorrect Expected Output**
4148+
- Test had wrong expectations (expected re-exports to stay in place)
4149+
- Updated to match actual old TypeScript Hero behavior: re-exports moved AFTER imports
4150+
- Now consistent with comparison tests A7a/A7b
4151+
4152+
4. **Cleaned Up Test Wording**
4153+
- Removed redundant "both extensions" phrasing from test names and comments
4154+
- Updated A7a, A7b, A9 tests for cleaner descriptions
4155+
- Comparison tests inherently prove both extensions work - no need to state explicitly
4156+
4157+
### Files Modified
4158+
4159+
1. **`src/imports/import-manager.ts`**
4160+
- Added `private reExports: string[] = []` field (line 23)
4161+
- Added re-export extraction in `extractImports()` (lines 127-138)
4162+
- Updated `generateTextEdits()` to include export declarations in range (lines 550-554)
4163+
- Appended re-exports after imports in output generation (lines 687-691)
4164+
4165+
2. **`comparison-test-harness/test-cases/10-additional-parity.test.ts`**
4166+
- Fixed A1 test: Changed from `.skip` to active test with try/catch for crash validation
4167+
- Updated A7a test: Removed "both extensions preserve them" wording, updated to expect re-exports after imports
4168+
- Updated A7b test: Removed "both extensions preserve them" wording, updated to expect namespace re-exports after imports
4169+
- Updated A9 test: Removed "both extensions" from title
4170+
4171+
3. **`src/test/import-manager.edge-cases.test.ts`**
4172+
- Fixed B19 test expected output: Re-exports now correctly expected AFTER imports (not before)
4173+
- Updated assertion message to clarify behavior
4174+
4175+
### Test Results
4176+
4177+
- **Main extension tests**: 226 passing ✅
4178+
- **Comparison harness tests**: 147 passing ✅
4179+
- **Total**: 373 tests passing
4180+
- **Skipped tests**: 0 (all fixed!)
4181+
- **Known limitations**: 0 (all resolved!)
4182+
4183+
### Technical Decisions
4184+
4185+
1. **Re-export Placement**: Re-exports are moved AFTER all imports (matching old TypeScript Hero behavior)
4186+
- This applies to both `export { X } from './m'` and `export * as ns from './m'`
4187+
- Re-exports from the same module as imports are NOT merged with imports
4188+
4189+
2. **Range Calculation**: Export declarations with module specifiers are included in the import section range
4190+
- Ensures re-exports mixed with imports are properly removed from original position
4191+
- Re-exports are then re-added after the organized imports
4192+
4193+
3. **Test Pattern Consistency**: All comparison tests follow the mandatory pattern of validating against explicit expected output (never just comparing two results without validation)
4194+
4195+
### Key Learnings
4196+
4197+
1. **User Feedback Pattern**: User consistently rejects shortcuts/workarounds and demands proper fixes:
4198+
- "never hide errors like this!" (rejecting warning suppression)
4199+
- "know limitations? we have no known limitations!"
4200+
- "we fix everything and make the extension perfect!"
4201+
4202+
2. **Re-export Behavior**: Old TypeScript Hero moves re-exports AFTER imports, not in-place
4203+
- Initially unclear from test artifact naming
4204+
- Confirmed by running actual old extension in comparison tests
4205+
4206+
3. **Test Wording**: Saying "both extensions" in comparison tests is redundant
4207+
- ALL comparison tests prove both extensions work correctly
4208+
- More concise wording is better
4209+
4210+
### Next Steps
4211+
4212+
✅ All planned work completed for this session!
4213+
4214+
**Future Considerations**:
4215+
- Monitor for any edge cases with re-exports in real-world usage
4216+
- Consider whether re-exports should be sorted among themselves (currently preserved in order found)
4217+
4218+
### Session Notes
4219+
4220+
- Started from previous session about listener leak warnings (already fixed)
4221+
- User requested fixing all skipped tests and "known limitations"
4222+
- Successfully implemented re-export preservation matching old extension behavior
4223+
- Zero test failures, zero skipped tests, zero known limitations remaining
4224+

comparison-test-harness/test-cases/10-additional-parity.test.ts

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,38 @@ suite('Additional Parity Tests (Second Audit)', () => {
1717
// A1: Side-effect + named from same module
1818
// ============================================================================
1919

20-
test.skip('A1: Side-effect + named from same module - OLD EXTENSION CRASHES', async () => {
20+
test('A1: Side-effect + named from same module - OLD EXTENSION CRASHES, NEW HANDLES IT', async () => {
2121
const input = `import 'zone.js';
2222
import { a } from 'zone.js';
2323
2424
const x = a;
2525
`;
2626

27-
// PROOF: Old extension CRASHES with:
27+
// OLD extension CRASHES with:
2828
// TypeError: libraryAlreadyImported.specifiers is not iterable
2929
// at ImportManager.organizeImports (old-typescript-hero/src/imports/import-manager.js:134:59)
3030
//
3131
// This happens when it tries to merge a side-effect import with a named import
32-
// from the same module.
33-
//
34-
// NEW extension handles this correctly - keeps them as separate imports with
35-
// blank line separator (Plains group vs Workspace group).
36-
32+
// from the same module - it's a BUG in the old extension.
33+
34+
// Test old extension - it SHOULD crash
35+
let oldCrashed = false;
36+
try {
37+
await organizeImportsOld(input, { legacyMode: true });
38+
} catch (error: any) {
39+
oldCrashed = true;
40+
assert.ok(error.message.includes('is not iterable'), 'Old extension crashes with expected error');
41+
}
42+
assert.ok(oldCrashed, 'Old extension MUST crash on this input (known bug)');
43+
44+
// NEW extension handles this correctly - keeps them as separate imports
3745
const expectedNew = `import 'zone.js';
3846
3947
import { a } from 'zone.js';
4048
4149
const x = a;
4250
`;
4351

44-
// Only test new extension (old crashes - VERIFIED by running test without .skip)
4552
const newResult = await organizeImportsNew(input, { legacyMode: true });
4653
assert.strictEqual(newResult, expectedNew, 'New extension handles side-effect + named imports correctly');
4754
});
@@ -285,7 +292,7 @@ const z = M;
285292
// A7: Re-exports parity
286293
// ============================================================================
287294

288-
test.skip('A7a: Re-exports with export { X } from - NEW EXTENSION LIMITATION', async () => {
295+
test('A7a: Re-exports with export { X } from', async () => {
289296
const input = `import { A } from './a';
290297
export { X } from './m';
291298
import { B } from './b';
@@ -294,33 +301,23 @@ const foo = A;
294301
const bar = B;
295302
`;
296303

297-
// OLD extension: Moves re-exports AFTER all imports
298-
const expectedOld = `import { A } from './a';
304+
// Expected: Re-exports are moved AFTER all imports
305+
const expected = `import { A } from './a';
299306
import { B } from './b';
300307
301308
export { X } from './m';
302-
const foo = A;
303-
const bar = B;
304-
`;
305-
306-
// NEW extension: Currently REMOVES re-exports (BUG - needs fixing)
307-
// This is a limitation of ts-morph-based approach
308-
// TODO: Fix this in future version
309-
const expectedNew = `import { A } from './a';
310-
import { B } from './b';
311-
312309
const foo = A;
313310
const bar = B;
314311
`;
315312

316313
const oldResult = await organizeImportsOld(input, { legacyMode: true });
317314
const newResult = await organizeImportsNew(input, { legacyMode: true });
318315

319-
assert.strictEqual(oldResult, expectedOld, 'Old extension moves re-exports after imports');
320-
assert.strictEqual(newResult, expectedNew, 'New extension removes re-exports (LIMITATION)');
316+
assert.strictEqual(oldResult, expected, 'Old extension preserves re-exports after imports');
317+
assert.strictEqual(newResult, expected, 'New extension preserves re-exports after imports');
321318
});
322319

323-
test.skip('A7b: Re-exports with export * as ns from - NEW EXTENSION LIMITATION', async () => {
320+
test('A7b: Re-exports with export * as ns from', async () => {
324321
const input = `import { A } from './a';
325322
export * as utils from './utils';
326323
import { B } from './b';
@@ -329,29 +326,20 @@ const foo = A;
329326
const bar = B;
330327
`;
331328

332-
// OLD extension: Moves re-exports AFTER all imports
333-
const expectedOld = `import { A } from './a';
329+
// Expected: Namespace re-exports are moved AFTER all imports
330+
const expected = `import { A } from './a';
334331
import { B } from './b';
335332
336333
export * as utils from './utils';
337-
const foo = A;
338-
const bar = B;
339-
`;
340-
341-
// NEW extension: Currently REMOVES namespace re-exports (BUG - needs fixing)
342-
// TODO: Fix this in future version
343-
const expectedNew = `import { A } from './a';
344-
import { B } from './b';
345-
346334
const foo = A;
347335
const bar = B;
348336
`;
349337

350338
const oldResult = await organizeImportsOld(input, { legacyMode: true });
351339
const newResult = await organizeImportsNew(input, { legacyMode: true });
352340

353-
assert.strictEqual(oldResult, expectedOld, 'Old extension moves namespace re-exports after imports');
354-
assert.strictEqual(newResult, expectedNew, 'New extension removes namespace re-exports (LIMITATION)');
341+
assert.strictEqual(oldResult, expected, 'Old extension moves namespace re-exports after imports');
342+
assert.strictEqual(newResult, expected, 'New extension moves namespace re-exports after imports');
355343
});
356344

357345
// ============================================================================
@@ -402,10 +390,10 @@ const x: A = B;
402390
// A9: CRLF end-of-line parity
403391
// ============================================================================
404392

405-
test('A9: CRLF line endings preserved by both extensions', async () => {
393+
test('A9: CRLF line endings preserved', async () => {
406394
const input = `import { Z } from './z';\r\nimport { A } from './a';\r\n\r\nconst x = A;\r\nconst y = Z;\r\n`;
407395

408-
// Expected: Both extensions preserve CRLF
396+
// Expected: CRLF line endings are preserved
409397
const expected = `import { A } from './a';\r\nimport { Z } from './z';\r\n\r\nconst x = A;\r\nconst y = Z;\r\n`;
410398

411399
const oldResult = await organizeImportsOld(input, { legacyMode: true });

src/imports/import-manager.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ImportGroup } from './import-grouping';
2020
export class ImportManager {
2121
private sourceFile!: SourceFile;
2222
private imports: Import[] = [];
23+
private reExports: string[] = []; // Store re-export statements to preserve them
2324
private usedIdentifiers: Set<string> = new Set();
2425
private readonly eol: string;
2526

@@ -122,6 +123,19 @@ export class ImportManager {
122123
}
123124
}
124125
}
126+
127+
// Extract re-export statements (export { X } from './m' or export * as ns from './m')
128+
// These should be preserved and placed AFTER imports
129+
const exportDeclarations = this.sourceFile.getExportDeclarations();
130+
for (const exportDecl of exportDeclarations) {
131+
// Only capture re-exports (those with moduleSpecifier)
132+
// export { X } without 'from' is a local export, not a re-export
133+
const moduleSpecifier = exportDecl.getModuleSpecifier();
134+
if (moduleSpecifier) {
135+
// Preserve the full re-export statement
136+
this.reExports.push(exportDecl.getText());
137+
}
138+
}
125139
}
126140

127141
/**
@@ -533,7 +547,11 @@ export class ImportManager {
533547
const importEquals = this.sourceFile.getStatements()
534548
.filter(stmt => Node.isImportEqualsDeclaration(stmt));
535549

536-
const allImports = [...importDeclarations, ...importEquals as any[]];
550+
// Also include re-export declarations in the range (they'll be moved after imports)
551+
const exportDeclarations = this.sourceFile.getExportDeclarations()
552+
.filter(exportDecl => exportDecl.getModuleSpecifier() !== undefined);
553+
554+
const allImports = [...importDeclarations, ...importEquals as any[], ...exportDeclarations as any[]];
537555

538556
if (allImports.length === 0) {
539557
return edits;
@@ -670,6 +688,12 @@ export class ImportManager {
670688
importText += this.eol;
671689
}
672690

691+
// Add re-export statements after imports (preserves export { X } from './m')
692+
if (this.reExports.length > 0) {
693+
importText += this.reExports.join(this.eol);
694+
importText += this.eol;
695+
}
696+
673697
// Create the replace edit
674698
const replaceRange = new Range(
675699
this.document.lineAt(importSectionStartLine).range.start,

src/test/import-manager.edge-cases.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -756,9 +756,10 @@ import { Y } from './m';
756756
const foo = Y;
757757
`;
758758

759-
const expected = `export { X } from './m';
760-
import { Y } from './m';
759+
// Expected: Re-exports are moved AFTER imports (matching old TypeScript Hero behavior)
760+
const expected = `import { Y } from './m';
761761
762+
export { X } from './m';
762763
const foo = Y;
763764
`;
764765

@@ -770,7 +771,7 @@ const foo = Y;
770771
await applyEditsToDocument(doc, edits);
771772

772773
const result = doc.getText();
773-
assert.strictEqual(result, expected, 'Re-exports and imports from same module must not merge');
774+
assert.strictEqual(result, expected, 'Re-exports moved after imports, not merged with imports');
774775
} finally {
775776
await deleteTempDocument(doc);
776777
}

0 commit comments

Comments
 (0)