• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    const fixId = "fixImportNonExportedMember";
4    const errorCodes = [
5        Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported.code,
6    ];
7
8    registerCodeFix({
9        errorCodes,
10        fixIds: [fixId],
11        getCodeActions(context) {
12            const { sourceFile, span, program } = context;
13            const info = getInfo(sourceFile, span.start, program);
14            if (info === undefined) return undefined;
15
16            const changes = textChanges.ChangeTracker.with(context, t => doChange(t, program, info));
17            return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.exportName.node.text, info.moduleSpecifier], fixId, Diagnostics.Export_all_referenced_locals)];
18        },
19        getAllCodeActions(context) {
20            const { program } = context;
21            return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => {
22                const exports = new Map<SourceFile, ModuleExports>();
23
24                eachDiagnostic(context, errorCodes, diag => {
25                    const info = getInfo(diag.file, diag.start, program);
26                    if (info === undefined) return undefined;
27
28                    const { exportName, node, moduleSourceFile } = info;
29                    if (tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly) === undefined && canHaveExportModifier(node)) {
30                        changes.insertExportModifier(moduleSourceFile, node);
31                    }
32                    else {
33                        const moduleExports = exports.get(moduleSourceFile) || { typeOnlyExports: [], exports: [] };
34                        if (exportName.isTypeOnly) {
35                            moduleExports.typeOnlyExports.push(exportName);
36                        }
37                        else {
38                            moduleExports.exports.push(exportName);
39                        }
40                        exports.set(moduleSourceFile, moduleExports);
41                    }
42                });
43
44                exports.forEach((moduleExports, moduleSourceFile) => {
45                    const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ true);
46                    if (exportDeclaration && exportDeclaration.isTypeOnly) {
47                        doChanges(changes, program, moduleSourceFile, moduleExports.typeOnlyExports, exportDeclaration);
48                        doChanges(changes, program, moduleSourceFile, moduleExports.exports, tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ false));
49                    }
50                    else {
51                        doChanges(changes, program, moduleSourceFile, [...moduleExports.exports, ...moduleExports.typeOnlyExports], exportDeclaration);
52                    }
53                });
54            }));
55        }
56    });
57
58    interface ModuleExports {
59        typeOnlyExports: ExportName[];
60        exports: ExportName[];
61    }
62
63    interface ExportName {
64        node: Identifier;
65        isTypeOnly: boolean;
66    }
67
68    interface Info {
69        exportName: ExportName;
70        node: Declaration | VariableStatement;
71        moduleSourceFile: SourceFile;
72        moduleSpecifier: string;
73    }
74
75    function getInfo(sourceFile: SourceFile, pos: number, program: Program): Info | undefined {
76        const token = getTokenAtPosition(sourceFile, pos);
77        if (isIdentifier(token)) {
78            const importDeclaration = findAncestor(token, isImportDeclaration);
79            if (importDeclaration === undefined) return undefined;
80
81            const moduleSpecifier = isStringLiteral(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : undefined;
82            if (moduleSpecifier === undefined) return undefined;
83
84            const resolvedModule = getResolvedModule(sourceFile, moduleSpecifier, /*mode*/ undefined);
85            if (resolvedModule === undefined) return undefined;
86
87            const moduleSourceFile = program.getSourceFile(resolvedModule.resolvedFileName);
88            if (moduleSourceFile === undefined || isSourceFileFromLibrary(program, moduleSourceFile)) return undefined;
89
90            const moduleSymbol = moduleSourceFile.symbol;
91            const locals = moduleSymbol.valueDeclaration?.locals;
92            if (locals === undefined) return undefined;
93
94            const localSymbol = locals.get(token.escapedText);
95            if (localSymbol === undefined) return undefined;
96
97            const node = getNodeOfSymbol(localSymbol);
98            if (node === undefined) return undefined;
99
100            const exportName = { node: token, isTypeOnly: isTypeDeclaration(node) };
101            return { exportName, node, moduleSourceFile, moduleSpecifier };
102        }
103        return undefined;
104    }
105
106    function doChange(changes: textChanges.ChangeTracker, program: Program, { exportName, node, moduleSourceFile }: Info) {
107        const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly);
108        if (exportDeclaration) {
109            updateExport(changes, program, moduleSourceFile, exportDeclaration, [exportName]);
110        }
111        else if (canHaveExportModifier(node)) {
112            changes.insertExportModifier(moduleSourceFile, node);
113        }
114        else {
115            createExport(changes, program, moduleSourceFile, [exportName]);
116        }
117    }
118
119    function doChanges(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, moduleExports: ExportName[], node: ExportDeclaration | undefined) {
120        if (length(moduleExports)) {
121            if (node) {
122                updateExport(changes, program, sourceFile, node, moduleExports);
123            }
124            else {
125                createExport(changes, program, sourceFile, moduleExports);
126            }
127        }
128    }
129
130    function tryGetExportDeclaration(sourceFile: SourceFile, isTypeOnly: boolean) {
131        const predicate = (node: Node): node is ExportDeclaration =>
132            isExportDeclaration(node) && (isTypeOnly && node.isTypeOnly || !node.isTypeOnly);
133        return findLast(sourceFile.statements, predicate);
134    }
135
136    function updateExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, node: ExportDeclaration, names: ExportName[]) {
137        const namedExports = node.exportClause && isNamedExports(node.exportClause) ? node.exportClause.elements : factory.createNodeArray([]);
138        const allowTypeModifier = !node.isTypeOnly && !!(program.getCompilerOptions().isolatedModules || find(namedExports, e => e.isTypeOnly));
139        changes.replaceNode(sourceFile, node,
140            factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly,
141                factory.createNamedExports(
142                    factory.createNodeArray([...namedExports, ...createExportSpecifiers(names, allowTypeModifier)], /*hasTrailingComma*/ namedExports.hasTrailingComma)), node.moduleSpecifier, node.assertClause));
143    }
144
145    function createExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, names: ExportName[]) {
146        changes.insertNodeAtEndOfScope(sourceFile, sourceFile,
147            factory.createExportDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false,
148                factory.createNamedExports(createExportSpecifiers(names, /*allowTypeModifier*/ !!program.getCompilerOptions().isolatedModules)), /*moduleSpecifier*/ undefined, /*assertClause*/ undefined));
149    }
150
151    function createExportSpecifiers(names: ExportName[], allowTypeModifier: boolean) {
152        return factory.createNodeArray(map(names, n => factory.createExportSpecifier(allowTypeModifier && n.isTypeOnly, /*propertyName*/ undefined, n.node)));
153    }
154
155    function getNodeOfSymbol(symbol: Symbol) {
156        if (symbol.valueDeclaration === undefined) {
157            return firstOrUndefined(symbol.declarations);
158        }
159        const declaration = symbol.valueDeclaration;
160        const variableStatement = isVariableDeclaration(declaration) ? tryCast(declaration.parent.parent, isVariableStatement) : undefined;
161        return variableStatement && length(variableStatement.declarationList.declarations) === 1 ? variableStatement : declaration;
162    }
163}
164