Skip to content

Commit c709443

Browse files
authored
Merge pull request #111 from FredericEspiau/fix/handle-deeply-nested-whole-exports
fix: handle deeply nested whole exports
2 parents 7c1120b + 8544c1b commit c709443

File tree

5 files changed

+303
-153
lines changed

5 files changed

+303
-153
lines changed

lib/util/edit.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,26 @@ export const b = 'b';`,
11301130
assert.equal(fileService.exists('/app/a_reexport.ts'), false);
11311131
assert.equal(fileService.exists('/app/a.ts'), false);
11321132
});
1133+
1134+
it('should look for deeply nested whole re-export without removing files', () => {
1135+
const fileService = new MemoryFileService();
1136+
fileService.set('/app/main.ts', `import { c } from './a';`);
1137+
fileService.set('/app/a.ts', `export * from './b';`);
1138+
fileService.set('/app/b.ts', `export * from './c';`);
1139+
fileService.set('/app/c.ts', `export const c = 'c';`);
1140+
1141+
edit({
1142+
fileService,
1143+
recursive,
1144+
deleteUnusedFile: true,
1145+
entrypoints: ['/app/main.ts'],
1146+
});
1147+
1148+
assert.equal(fileService.get('/app/main.ts'), `import { c } from './a';`);
1149+
assert.equal(fileService.get('/app/a.ts'), `export * from './b';`);
1150+
assert.equal(fileService.get('/app/b.ts'), `export * from './c';`);
1151+
assert.equal(fileService.get('/app/c.ts'), `export const c = 'c';`);
1152+
});
11331153
});
11341154

11351155
describe('namespace export declaration', () => {

lib/util/edit.ts

+84-14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import { MemoryFileService } from './MemoryFileService.js';
1212
import { findFileUsage } from './findFileUsage.js';
1313
import { parseFile } from './parseFile.js';
1414
import { Output } from './Output.js';
15+
import {
16+
WholeExportDeclarationWithFile,
17+
isWholeExportDeclarationWithFile,
18+
isNamedExport,
19+
isWholeExportDeclaration,
20+
} from './export.js';
1521

1622
const transform = (
1723
source: string,
@@ -148,7 +154,7 @@ const updateExportDeclaration = (code: string, unused: string[]) => {
148154

149155
const printer = ts.createPrinter();
150156
const printed = result ? printer.printFile(result).replace(/\n$/, '') : '';
151-
const leading = code.match(/^([\s]+)/)?.[0] || '';
157+
const leading = code.match(/^(\s+)/)?.[0] || '';
152158

153159
return `${leading}${printed}`;
154160
};
@@ -181,6 +187,76 @@ const getSpecifierPosition = (exportDeclaration: string) => {
181187
return result;
182188
};
183189

190+
/**
191+
* Retrieves the names of the exports from a whole export declaration.
192+
* For each whole export declaration, it will recursively get the names of the exports from the file it points to.
193+
*/
194+
const deeplyGetExportNames = ({
195+
item,
196+
files,
197+
fileNames,
198+
options,
199+
}: {
200+
item: WholeExportDeclarationWithFile;
201+
files: Map<string, string>;
202+
fileNames: Set<string>;
203+
options: ts.CompilerOptions;
204+
}): string[] => {
205+
const filesAlreadyVisited = new Set<string>();
206+
207+
return innerDeeplyGetExportNames({
208+
item,
209+
files,
210+
fileNames,
211+
options,
212+
filesAlreadyVisited,
213+
});
214+
};
215+
216+
const innerDeeplyGetExportNames = ({
217+
item,
218+
files,
219+
fileNames,
220+
options,
221+
filesAlreadyVisited,
222+
}: {
223+
item: WholeExportDeclarationWithFile;
224+
files: Map<string, string>;
225+
fileNames: Set<string>;
226+
options: ts.CompilerOptions;
227+
filesAlreadyVisited: Set<string>;
228+
}): string[] => {
229+
if (filesAlreadyVisited.has(item.file)) {
230+
return [];
231+
}
232+
233+
const parsed = parseFile({
234+
file: item.file,
235+
content: files.get(item.file) || '',
236+
options,
237+
destFiles: fileNames,
238+
});
239+
240+
const deepExportNames = parsed.exports
241+
.filter(
242+
(v) => isWholeExportDeclaration(v) && isWholeExportDeclarationWithFile(v),
243+
)
244+
.flatMap((v) =>
245+
innerDeeplyGetExportNames({
246+
item: v,
247+
files,
248+
fileNames,
249+
options,
250+
filesAlreadyVisited: filesAlreadyVisited.add(item.file),
251+
}),
252+
);
253+
254+
return parsed.exports
255+
.filter(isNamedExport)
256+
.flatMap((v) => v.name)
257+
.concat(deepExportNames);
258+
};
259+
184260
const processFile = ({
185261
targetFile,
186262
files,
@@ -424,23 +500,19 @@ const processFile = ({
424500
break;
425501
}
426502
case 'whole': {
427-
if (!item.file) {
503+
if (!isWholeExportDeclarationWithFile(item)) {
428504
// whole export is directed towards a file that is not in the project
429505
break;
430506
}
431507

432-
const parsed = parseFile({
433-
file: item.file,
434-
content: files.get(item.file) || '',
508+
const exportNames = deeplyGetExportNames({
509+
item,
510+
files,
511+
fileNames,
435512
options,
436-
destFiles: fileNames,
437513
});
438514

439-
const exported = parsed.exports.flatMap((v) =>
440-
'name' in v ? v.name : [],
441-
);
442-
443-
if (exported.some((v) => usage.has(v))) {
515+
if (exportNames.some((v) => usage.has(v))) {
444516
break;
445517
}
446518

@@ -582,13 +654,11 @@ export {};\n`,
582654

583655
fileService.set(targetFile, content);
584656

585-
const result = {
657+
return {
586658
operation: 'edit' as const,
587659
content: fileService.get(targetFile),
588660
removedExports: logs,
589661
};
590-
591-
return result;
592662
};
593663

594664
export const edit = ({

lib/util/export.ts

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import ts from 'typescript';
2+
3+
type ClassDeclaration = {
4+
kind: ts.SyntaxKind.ClassDeclaration;
5+
name: string;
6+
change: {
7+
code: string;
8+
isUnnamedDefaultExport?: boolean;
9+
span: {
10+
start: number;
11+
length: number;
12+
};
13+
};
14+
skip: boolean;
15+
start: number;
16+
};
17+
18+
type EnumDeclaration = {
19+
kind: ts.SyntaxKind.EnumDeclaration;
20+
name: string;
21+
change: {
22+
code: string;
23+
span: {
24+
start: number;
25+
length: number;
26+
};
27+
};
28+
skip: boolean;
29+
start: number;
30+
};
31+
32+
type ExportAssignment = {
33+
kind: ts.SyntaxKind.ExportAssignment;
34+
name: 'default';
35+
change: {
36+
code: string;
37+
span: {
38+
start: number;
39+
length: number;
40+
};
41+
};
42+
skip: boolean;
43+
start: number;
44+
};
45+
46+
type FunctionDeclaration = {
47+
kind: ts.SyntaxKind.FunctionDeclaration;
48+
name: string;
49+
change: {
50+
code: string;
51+
isUnnamedDefaultExport?: boolean;
52+
span: {
53+
start: number;
54+
length: number;
55+
};
56+
};
57+
skip: boolean;
58+
start: number;
59+
};
60+
61+
type InterfaceDeclaration = {
62+
kind: ts.SyntaxKind.InterfaceDeclaration;
63+
name: string;
64+
change: {
65+
code: string;
66+
span: {
67+
start: number;
68+
length: number;
69+
};
70+
};
71+
skip: boolean;
72+
start: number;
73+
};
74+
75+
type NameExportDeclaration = {
76+
kind: ts.SyntaxKind.ExportDeclaration;
77+
type: 'named';
78+
name: string[];
79+
skip: boolean;
80+
change: {
81+
code: string;
82+
span: {
83+
start: number;
84+
length: number;
85+
};
86+
};
87+
start: number;
88+
};
89+
90+
type NamespaceExportDeclaration = {
91+
kind: ts.SyntaxKind.ExportDeclaration;
92+
type: 'namespace';
93+
name: string;
94+
start: number;
95+
change: {
96+
code: string;
97+
span: {
98+
start: number;
99+
length: number;
100+
};
101+
};
102+
};
103+
104+
type TypeAliasDeclaration = {
105+
kind: ts.SyntaxKind.TypeAliasDeclaration;
106+
name: string;
107+
change: {
108+
code: string;
109+
span: {
110+
start: number;
111+
length: number;
112+
};
113+
};
114+
skip: boolean;
115+
start: number;
116+
};
117+
118+
type VariableStatement = {
119+
kind: ts.SyntaxKind.VariableStatement;
120+
name: string[];
121+
change: {
122+
code: string;
123+
span: {
124+
start: number;
125+
length: number;
126+
};
127+
};
128+
skip: boolean;
129+
start: number;
130+
};
131+
132+
type NamedExport =
133+
| ClassDeclaration
134+
| EnumDeclaration
135+
| ExportAssignment
136+
| FunctionDeclaration
137+
| InterfaceDeclaration
138+
| NameExportDeclaration
139+
| NamespaceExportDeclaration
140+
| TypeAliasDeclaration
141+
| VariableStatement;
142+
143+
type WholeExportDeclarationBase = {
144+
kind: ts.SyntaxKind.ExportDeclaration;
145+
type: 'whole';
146+
specifier: string;
147+
start: number;
148+
change: {
149+
code: string;
150+
span: {
151+
start: number;
152+
length: number;
153+
};
154+
};
155+
};
156+
157+
/**
158+
* Whole export when the file is found within the destFiles
159+
*/
160+
export type WholeExportDeclarationWithFile = WholeExportDeclarationBase & {
161+
file: string;
162+
};
163+
164+
/**
165+
* Whole export when the file is not found within the destFiles, i.e. the file is not part of the project
166+
*/
167+
type WholeExportDeclarationWithoutFile = WholeExportDeclarationBase & {
168+
file: null;
169+
};
170+
171+
type WholeExportDeclaration =
172+
| WholeExportDeclarationWithFile
173+
| WholeExportDeclarationWithoutFile;
174+
175+
export const isWholeExportDeclarationWithFile = (
176+
exportDeclaration: WholeExportDeclaration,
177+
): exportDeclaration is WholeExportDeclarationWithFile =>
178+
exportDeclaration.file !== null;
179+
180+
export type Export = NamedExport | WholeExportDeclaration;
181+
182+
export const isNamedExport = (v: Export): v is NamedExport => 'name' in v;
183+
184+
export const isWholeExportDeclaration = (
185+
v: Export,
186+
): v is WholeExportDeclaration =>
187+
v.kind === ts.SyntaxKind.ExportDeclaration && v.type === 'whole';

lib/util/findFileUsage.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import ts from 'typescript';
22
import { Vertexes } from './DependencyGraph.js';
33
import { parseFile } from './parseFile.js';
44

5-
const ALL_EXPORTS_OF_UNKNOWN_FILE = '__all_exports_of_unknown_file__';
5+
const ALL_EXPORTS_OF_UNKNOWN_FILE = '#all_exports_of_unknown_file#';
6+
const CIRCULAR_DEPENDENCY = '#circular_dependency#';
67

78
const getExportsOfFile = ({
89
targetFile,
@@ -17,6 +18,7 @@ const getExportsOfFile = ({
1718
}) => {
1819
const result: string[] = [];
1920

21+
const alreadyVisited = new Set<string>();
2022
const stack = [targetFile];
2123

2224
while (stack.length) {
@@ -26,6 +28,13 @@ const getExportsOfFile = ({
2628
break;
2729
}
2830

31+
if (alreadyVisited.has(item)) {
32+
result.push(CIRCULAR_DEPENDENCY);
33+
continue;
34+
}
35+
36+
alreadyVisited.add(item);
37+
2938
const { exports } = parseFile({
3039
file: item,
3140
content: files.get(item) || '',
@@ -81,7 +90,7 @@ export const findFileUsage = ({
8190
);
8291

8392
while (stack.length) {
84-
const item = stack.pop()!;
93+
const item = stack.pop();
8594

8695
if (!item) {
8796
break;

0 commit comments

Comments
 (0)