Skip to content

Commit

Permalink
Merge pull request #111 from FredericEspiau/fix/handle-deeply-nested-…
Browse files Browse the repository at this point in the history
…whole-exports

fix: handle deeply nested whole exports
  • Loading branch information
kazushisan authored Jan 8, 2025
2 parents 7c1120b + 8544c1b commit c709443
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 153 deletions.
20 changes: 20 additions & 0 deletions lib/util/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,26 @@ export const b = 'b';`,
assert.equal(fileService.exists('/app/a_reexport.ts'), false);
assert.equal(fileService.exists('/app/a.ts'), false);
});

it('should look for deeply nested whole re-export without removing files', () => {
const fileService = new MemoryFileService();
fileService.set('/app/main.ts', `import { c } from './a';`);
fileService.set('/app/a.ts', `export * from './b';`);
fileService.set('/app/b.ts', `export * from './c';`);
fileService.set('/app/c.ts', `export const c = 'c';`);

edit({
fileService,
recursive,
deleteUnusedFile: true,
entrypoints: ['/app/main.ts'],
});

assert.equal(fileService.get('/app/main.ts'), `import { c } from './a';`);
assert.equal(fileService.get('/app/a.ts'), `export * from './b';`);
assert.equal(fileService.get('/app/b.ts'), `export * from './c';`);
assert.equal(fileService.get('/app/c.ts'), `export const c = 'c';`);
});
});

describe('namespace export declaration', () => {
Expand Down
98 changes: 84 additions & 14 deletions lib/util/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { MemoryFileService } from './MemoryFileService.js';
import { findFileUsage } from './findFileUsage.js';
import { parseFile } from './parseFile.js';
import { Output } from './Output.js';
import {
WholeExportDeclarationWithFile,
isWholeExportDeclarationWithFile,
isNamedExport,
isWholeExportDeclaration,
} from './export.js';

const transform = (
source: string,
Expand Down Expand Up @@ -148,7 +154,7 @@ const updateExportDeclaration = (code: string, unused: string[]) => {

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

return `${leading}${printed}`;
};
Expand Down Expand Up @@ -181,6 +187,76 @@ const getSpecifierPosition = (exportDeclaration: string) => {
return result;
};

/**
* Retrieves the names of the exports from a whole export declaration.
* For each whole export declaration, it will recursively get the names of the exports from the file it points to.
*/
const deeplyGetExportNames = ({
item,
files,
fileNames,
options,
}: {
item: WholeExportDeclarationWithFile;
files: Map<string, string>;
fileNames: Set<string>;
options: ts.CompilerOptions;
}): string[] => {
const filesAlreadyVisited = new Set<string>();

return innerDeeplyGetExportNames({
item,
files,
fileNames,
options,
filesAlreadyVisited,
});
};

const innerDeeplyGetExportNames = ({
item,
files,
fileNames,
options,
filesAlreadyVisited,
}: {
item: WholeExportDeclarationWithFile;
files: Map<string, string>;
fileNames: Set<string>;
options: ts.CompilerOptions;
filesAlreadyVisited: Set<string>;
}): string[] => {
if (filesAlreadyVisited.has(item.file)) {
return [];
}

const parsed = parseFile({
file: item.file,
content: files.get(item.file) || '',
options,
destFiles: fileNames,
});

const deepExportNames = parsed.exports
.filter(
(v) => isWholeExportDeclaration(v) && isWholeExportDeclarationWithFile(v),
)
.flatMap((v) =>
innerDeeplyGetExportNames({
item: v,
files,
fileNames,
options,
filesAlreadyVisited: filesAlreadyVisited.add(item.file),
}),
);

return parsed.exports
.filter(isNamedExport)
.flatMap((v) => v.name)
.concat(deepExportNames);
};

const processFile = ({
targetFile,
files,
Expand Down Expand Up @@ -424,23 +500,19 @@ const processFile = ({
break;
}
case 'whole': {
if (!item.file) {
if (!isWholeExportDeclarationWithFile(item)) {
// whole export is directed towards a file that is not in the project
break;
}

const parsed = parseFile({
file: item.file,
content: files.get(item.file) || '',
const exportNames = deeplyGetExportNames({
item,
files,
fileNames,
options,
destFiles: fileNames,
});

const exported = parsed.exports.flatMap((v) =>
'name' in v ? v.name : [],
);

if (exported.some((v) => usage.has(v))) {
if (exportNames.some((v) => usage.has(v))) {
break;
}

Expand Down Expand Up @@ -582,13 +654,11 @@ export {};\n`,

fileService.set(targetFile, content);

const result = {
return {
operation: 'edit' as const,
content: fileService.get(targetFile),
removedExports: logs,
};

return result;
};

export const edit = ({
Expand Down
187 changes: 187 additions & 0 deletions lib/util/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import ts from 'typescript';

type ClassDeclaration = {
kind: ts.SyntaxKind.ClassDeclaration;
name: string;
change: {
code: string;
isUnnamedDefaultExport?: boolean;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

type EnumDeclaration = {
kind: ts.SyntaxKind.EnumDeclaration;
name: string;
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

type ExportAssignment = {
kind: ts.SyntaxKind.ExportAssignment;
name: 'default';
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

type FunctionDeclaration = {
kind: ts.SyntaxKind.FunctionDeclaration;
name: string;
change: {
code: string;
isUnnamedDefaultExport?: boolean;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

type InterfaceDeclaration = {
kind: ts.SyntaxKind.InterfaceDeclaration;
name: string;
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

type NameExportDeclaration = {
kind: ts.SyntaxKind.ExportDeclaration;
type: 'named';
name: string[];
skip: boolean;
change: {
code: string;
span: {
start: number;
length: number;
};
};
start: number;
};

type NamespaceExportDeclaration = {
kind: ts.SyntaxKind.ExportDeclaration;
type: 'namespace';
name: string;
start: number;
change: {
code: string;
span: {
start: number;
length: number;
};
};
};

type TypeAliasDeclaration = {
kind: ts.SyntaxKind.TypeAliasDeclaration;
name: string;
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

type VariableStatement = {
kind: ts.SyntaxKind.VariableStatement;
name: string[];
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

type NamedExport =
| ClassDeclaration
| EnumDeclaration
| ExportAssignment
| FunctionDeclaration
| InterfaceDeclaration
| NameExportDeclaration
| NamespaceExportDeclaration
| TypeAliasDeclaration
| VariableStatement;

type WholeExportDeclarationBase = {
kind: ts.SyntaxKind.ExportDeclaration;
type: 'whole';
specifier: string;
start: number;
change: {
code: string;
span: {
start: number;
length: number;
};
};
};

/**
* Whole export when the file is found within the destFiles
*/
export type WholeExportDeclarationWithFile = WholeExportDeclarationBase & {
file: string;
};

/**
* Whole export when the file is not found within the destFiles, i.e. the file is not part of the project
*/
type WholeExportDeclarationWithoutFile = WholeExportDeclarationBase & {
file: null;
};

type WholeExportDeclaration =
| WholeExportDeclarationWithFile
| WholeExportDeclarationWithoutFile;

export const isWholeExportDeclarationWithFile = (
exportDeclaration: WholeExportDeclaration,
): exportDeclaration is WholeExportDeclarationWithFile =>
exportDeclaration.file !== null;

export type Export = NamedExport | WholeExportDeclaration;

export const isNamedExport = (v: Export): v is NamedExport => 'name' in v;

export const isWholeExportDeclaration = (
v: Export,
): v is WholeExportDeclaration =>
v.kind === ts.SyntaxKind.ExportDeclaration && v.type === 'whole';
13 changes: 11 additions & 2 deletions lib/util/findFileUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import ts from 'typescript';
import { Vertexes } from './DependencyGraph.js';
import { parseFile } from './parseFile.js';

const ALL_EXPORTS_OF_UNKNOWN_FILE = '__all_exports_of_unknown_file__';
const ALL_EXPORTS_OF_UNKNOWN_FILE = '#all_exports_of_unknown_file#';
const CIRCULAR_DEPENDENCY = '#circular_dependency#';

const getExportsOfFile = ({
targetFile,
Expand All @@ -17,6 +18,7 @@ const getExportsOfFile = ({
}) => {
const result: string[] = [];

const alreadyVisited = new Set<string>();
const stack = [targetFile];

while (stack.length) {
Expand All @@ -26,6 +28,13 @@ const getExportsOfFile = ({
break;
}

if (alreadyVisited.has(item)) {
result.push(CIRCULAR_DEPENDENCY);
continue;
}

alreadyVisited.add(item);

const { exports } = parseFile({
file: item,
content: files.get(item) || '',
Expand Down Expand Up @@ -81,7 +90,7 @@ export const findFileUsage = ({
);

while (stack.length) {
const item = stack.pop()!;
const item = stack.pop();

if (!item) {
break;
Expand Down
Loading

0 comments on commit c709443

Please sign in to comment.