Skip to content

Commit 91acb13

Browse files
committed
fix: ensure regex import groups have matching precedence
CRITICAL BUG FIXED - Regex Group Precedence: - Problem: Regex groups didn't have priority over keyword groups - Example: Config [Modules, /angular/] → /angular/ never matched (Modules captured everything first) - Root cause: Groups processed in config order without precedence sorting - Solution: Added importGroupSortForPrecedence utility IMPLEMENTATION: - Added importGroupSortForPrecedence() to import-utilities.ts - Sorts regex groups first, then keyword groups - Preserves original order within each category - Updated ImportManager to use precedence sorting - Ensures regex groups match before keyword groups - Matches original TypeScript Hero behavior NEW TESTS - Import Utilities (12 tests): - importGroupSortForPrecedence (4 tests): - Prioritize regex groups, preserve order - Handle lists with no regex groups - Handle lists with only regex groups - Handle empty lists - importSortByFirstSpecifier (8 tests): - Sort by specifier/alias/namespace - String imports by basename - Fallback to library name - Case-insensitive locale-aware sorting - Ascending/descending order TEST COVERAGE: - Previous: 54 tests - Current: 66 tests (all passing ✅) COMPATIBILITY: ✅ Ported from original TypeScript Hero utility tests ✅ Regex group precedence now works correctly ✅ All sorting utilities fully tested
1 parent 083a555 commit 91acb13

3 files changed

Lines changed: 159 additions & 2 deletions

File tree

mini-typescript-hero/src/imports/import-manager.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
StringImport,
1111
SymbolSpecifier
1212
} from './import-types';
13-
import { importSort, importSortByFirstSpecifier, specifierSort } from './import-utilities';
13+
import { importSort, importSortByFirstSpecifier, specifierSort, importGroupSortForPrecedence } from './import-utilities';
1414
import { ImportGroup } from './import-grouping';
1515

1616
/**
@@ -299,8 +299,12 @@ export class ImportManager {
299299
group.reset();
300300
}
301301

302+
// Sort groups for precedence: regex groups first, then keyword groups
303+
// This ensures regex groups can match imports even if they appear later in the config
304+
const groupsWithPrecedence = importGroupSortForPrecedence(importGroups);
305+
302306
for (const imp of keep) {
303-
for (const group of importGroups) {
307+
for (const group of groupsWithPrecedence) {
304308
if (group.processImport(imp)) {
305309
break;
306310
}

mini-typescript-hero/src/imports/import-utilities.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { basename } from 'path';
22

33
import { ExternalModuleImport, Import, NamedImport, NamespaceImport, StringImport, SymbolSpecifier } from './import-types';
4+
import { ImportGroup, RegexImportGroup } from './import-grouping';
45

56
/**
67
* String-Sort function.
@@ -80,3 +81,19 @@ function getImportFirstSpecifier(imp: Import): string {
8081
export function specifierSort(i1: SymbolSpecifier, i2: SymbolSpecifier): number {
8182
return stringSort(i1.specifier, i2.specifier);
8283
}
84+
85+
/**
86+
* Orders import groups by matching precedence (regex first).
87+
* This ensures regex groups can capture imports before keyword groups,
88+
* even if they appear later in the configuration.
89+
*
90+
* Used internally by ImportManager when assigning imports to groups.
91+
*/
92+
export function importGroupSortForPrecedence(importGroups: ImportGroup[]): ImportGroup[] {
93+
const regexGroups: ImportGroup[] = [];
94+
const otherGroups: ImportGroup[] = [];
95+
for (const ig of importGroups) {
96+
(ig instanceof RegexImportGroup ? regexGroups : otherGroups).push(ig);
97+
}
98+
return regexGroups.concat(otherGroups);
99+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Unit Tests for Import Utilities
3+
*
4+
* Tests utility functions used for sorting and organizing imports.
5+
* Ported from original TypeScript Hero to ensure compatibility.
6+
*/
7+
8+
import * as assert from 'assert';
9+
import {
10+
ImportGroupKeyword,
11+
KeywordImportGroup,
12+
RegexImportGroup
13+
} from '../../imports/import-grouping';
14+
import { importGroupSortForPrecedence, importSortByFirstSpecifier } from '../../imports/import-utilities';
15+
import { NamedImport, NamespaceImport, StringImport } from '../../imports/import-types';
16+
17+
suite('Import Utilities Tests', () => {
18+
suite('importGroupSortForPrecedence', () => {
19+
test('should prioritize regex groups, leaving original order untouched besides that', () => {
20+
const initialList = [
21+
new KeywordImportGroup(ImportGroupKeyword.Modules),
22+
new KeywordImportGroup(ImportGroupKeyword.Plains),
23+
new RegexImportGroup('/cool-library/'),
24+
new RegexImportGroup('/cooler-library/'),
25+
new KeywordImportGroup(ImportGroupKeyword.Workspace),
26+
];
27+
28+
const result = importGroupSortForPrecedence(initialList);
29+
30+
// Expected: regex groups first, then keyword groups in original order
31+
assert.strictEqual(result[0], initialList[2], 'First regex group should be first');
32+
assert.strictEqual(result[1], initialList[3], 'Second regex group should be second');
33+
assert.strictEqual(result[2], initialList[0], 'First keyword group should be third');
34+
assert.strictEqual(result[3], initialList[1], 'Second keyword group should be fourth');
35+
assert.strictEqual(result[4], initialList[4], 'Third keyword group should be fifth');
36+
});
37+
38+
test('should handle list with no regex groups', () => {
39+
const initialList = [
40+
new KeywordImportGroup(ImportGroupKeyword.Modules),
41+
new KeywordImportGroup(ImportGroupKeyword.Plains),
42+
new KeywordImportGroup(ImportGroupKeyword.Workspace),
43+
];
44+
45+
const result = importGroupSortForPrecedence(initialList);
46+
47+
// Order should remain unchanged
48+
assert.deepStrictEqual(result, initialList);
49+
});
50+
51+
test('should handle list with only regex groups', () => {
52+
const initialList = [
53+
new RegexImportGroup('/lib-a/'),
54+
new RegexImportGroup('/lib-b/'),
55+
new RegexImportGroup('/lib-c/'),
56+
];
57+
58+
const result = importGroupSortForPrecedence(initialList);
59+
60+
// Order should remain unchanged
61+
assert.deepStrictEqual(result, initialList);
62+
});
63+
64+
test('should handle empty list', () => {
65+
const result = importGroupSortForPrecedence([]);
66+
assert.deepStrictEqual(result, []);
67+
});
68+
});
69+
70+
suite('importSortByFirstSpecifier', () => {
71+
test('should sort by first specifier (named import)', () => {
72+
const imp1 = new NamedImport('lib', [{ specifier: 'Zebra' }]);
73+
const imp2 = new NamedImport('lib', [{ specifier: 'Apple' }]);
74+
75+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2), 1);
76+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1), -1);
77+
});
78+
79+
test('should sort by alias when present (named import)', () => {
80+
const imp1 = new NamedImport('lib', [{ specifier: 'Foo', alias: 'Zebra' }]);
81+
const imp2 = new NamedImport('lib', [{ specifier: 'Bar', alias: 'Apple' }]);
82+
83+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2), 1);
84+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1), -1);
85+
});
86+
87+
test('should sort namespace imports by alias', () => {
88+
const imp1 = new NamespaceImport('lib', 'Zebra');
89+
const imp2 = new NamespaceImport('lib', 'Apple');
90+
91+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2), 1);
92+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1), -1);
93+
});
94+
95+
test('should sort string imports by library basename', () => {
96+
const imp1 = new StringImport('path/to/zebra');
97+
const imp2 = new StringImport('path/to/apple');
98+
99+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2), 1);
100+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1), -1);
101+
});
102+
103+
test('should fall back to library name when no specifiers', () => {
104+
const imp1 = new NamedImport('zebra-lib', []);
105+
const imp2 = new NamedImport('apple-lib', []);
106+
107+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2), 1);
108+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1), -1);
109+
});
110+
111+
test('should sort by default alias when present', () => {
112+
const imp1 = new NamedImport('lib', [], 'Zebra');
113+
const imp2 = new NamedImport('lib', [], 'Apple');
114+
115+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2), 1);
116+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1), -1);
117+
});
118+
119+
test('should be case-insensitive (locale-aware)', () => {
120+
const imp1 = new NamedImport('lib', [{ specifier: 'zebra' }]);
121+
const imp2 = new NamedImport('lib', [{ specifier: 'Apple' }]);
122+
123+
// Should sort 'Apple' before 'zebra' (case-insensitive)
124+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2), 1);
125+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1), -1);
126+
});
127+
128+
test('should handle descending order', () => {
129+
const imp1 = new NamedImport('lib', [{ specifier: 'Zebra' }]);
130+
const imp2 = new NamedImport('lib', [{ specifier: 'Apple' }]);
131+
132+
assert.strictEqual(importSortByFirstSpecifier(imp1, imp2, 'desc'), -1);
133+
assert.strictEqual(importSortByFirstSpecifier(imp2, imp1, 'desc'), 1);
134+
});
135+
});
136+
});

0 commit comments

Comments
 (0)