diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json
index 82f3cb4ce579b..230150480ee25 100644
--- a/src/compiler/diagnosticMessages.json
+++ b/src/compiler/diagnosticMessages.json
@@ -5316,6 +5316,14 @@
"category": "Message",
"code": 90053
},
+ "Export '{0}' from module '{1}'": {
+ "category": "Message",
+ "code": 90054
+ },
+ "Add all missing exports": {
+ "category": "Message",
+ "code": 90055
+ },
"Convert function to an ES2015 class": {
"category": "Message",
"code": 95001
diff --git a/src/services/codefixes/fixImportNonExportedMember.ts b/src/services/codefixes/fixImportNonExportedMember.ts
new file mode 100644
index 0000000000000..10c3590273334
--- /dev/null
+++ b/src/services/codefixes/fixImportNonExportedMember.ts
@@ -0,0 +1,110 @@
+/* @internal */
+namespace ts.codefix {
+ const fixId = "importNonExportedMember";
+ const errorCodes = [
+ Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported.code
+ ];
+ registerCodeFix({
+ errorCodes,
+ getCodeActions(context) {
+ const { sourceFile } = context;
+ const info = getInfo(sourceFile, context, context.span.start);
+ if (!info || info.originSourceFile.isDeclarationFile) {
+ return undefined;
+ }
+ const changes = textChanges.ChangeTracker.with(context, t => doChange(t, info.originSourceFile, info.node));
+ return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.node.text, showModuleSpecifier(info.importDecl)], fixId, Diagnostics.Add_all_missing_exports)];
+ },
+ fixIds: [fixId],
+ getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
+ const info = getInfo(diag.file, context, diag.start);
+ if (info) doChange(changes, info.originSourceFile, info.node);
+ }),
+ });
+
+ interface Info {
+ readonly node: Identifier;
+ readonly importDecl: ImportDeclaration;
+ readonly originSourceFile: SourceFile
+ }
+
+ function getInfo(sourceFile: SourceFile, context: CodeFixContext | CodeFixAllContext, pos: number): Info | undefined {
+ const node = getTokenAtPosition(sourceFile, pos);
+ if (node && isIdentifier(node)) {
+ const importDecl = findAncestor(node, isImportDeclaration);
+ if (!importDecl || !isStringLiteralLike(importDecl.moduleSpecifier)) {
+ return undefined;
+ }
+ const resolvedModule = getResolvedModule(sourceFile, importDecl.moduleSpecifier.text);
+ const originSourceFile = resolvedModule && context.program.getSourceFile(resolvedModule.resolvedFileName);
+ if (!originSourceFile) {
+ return undefined;
+ }
+ return { node, importDecl, originSourceFile };
+ }
+ }
+
+ function getNamedExportDeclaration(sourceFile: SourceFile): ExportDeclaration | undefined {
+ let namedExport;
+ for (const statement of sourceFile.statements) {
+ if (isExportDeclaration(statement) && statement.exportClause &&
+ isNamedExports(statement.exportClause)) {
+ namedExport = statement;
+ }
+ }
+ return namedExport;
+ }
+
+ function compareIdentifiers(s1: Identifier, s2: Identifier) {
+ return compareStringsCaseInsensitive(s1.text, s2.text);
+ }
+
+ function sortSpecifiers(specifiers: ExportSpecifier[]): readonly ExportSpecifier[] {
+ return stableSort(specifiers, (s1, s2) => {
+ return compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name);
+ });
+ }
+
+ function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: Identifier): void {
+ const moduleSymbol = sourceFile.localSymbol || sourceFile.symbol;
+ const localSymbol = moduleSymbol.valueDeclaration.locals?.get(node.escapedText);
+ if (!localSymbol) {
+ return;
+ }
+ if (isFunctionSymbol(localSymbol)) {
+ const start = localSymbol.valueDeclaration.pos;
+ changes.insertExportModifierAt(sourceFile, start ? start + 1 : 0);
+ return;
+ }
+
+ const current: VariableDeclarationList | Node = localSymbol.valueDeclaration.parent;
+ if (isVariableDeclarationList(current) && current.declarations.length <= 1) {
+ const start = localSymbol.valueDeclaration.parent.pos;
+ changes.insertExportModifierAt(sourceFile, start ? start + 1 : 0);
+ return;
+ }
+
+ const namedExportDeclaration = getNamedExportDeclaration(sourceFile);
+ const exportSpecifier = factory.createExportSpecifier(/*propertyName*/ undefined, node);
+ if (namedExportDeclaration?.exportClause && isNamedExports(namedExportDeclaration.exportClause)) {
+ const sortedExportSpecifiers = sortSpecifiers(namedExportDeclaration.exportClause.elements.concat(exportSpecifier));
+ return changes.replaceNode(sourceFile, namedExportDeclaration, factory.updateExportDeclaration(
+ namedExportDeclaration,
+ /*decorators*/ undefined,
+ /*modifiers*/ undefined,
+ /*isTypeOnly*/ false,
+ factory.updateNamedExports(namedExportDeclaration.exportClause, sortedExportSpecifiers),
+ /*moduleSpecifier*/ undefined
+ ));
+ }
+ else {
+ return changes.insertNodeAtEndOfScope(sourceFile, sourceFile, factory.createExportDeclaration(
+ /*decorators*/ undefined,
+ /*modifiers*/ undefined,
+ /*isTypeOnly*/ false,
+ factory.createNamedExports([exportSpecifier]),
+ /*moduleSpecifier*/ undefined
+ ));
+ }
+ }
+}
diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts
index 023a34c05663f..f8f4ba2a1a4a2 100644
--- a/src/services/textChanges.ts
+++ b/src/services/textChanges.ts
@@ -686,7 +686,11 @@ namespace ts.textChanges {
}
public insertExportModifier(sourceFile: SourceFile, node: DeclarationStatement | VariableStatement): void {
- this.insertText(sourceFile, node.getStart(sourceFile), "export ");
+ this.insertExportModifierAt(sourceFile, node.getStart(sourceFile));
+ }
+
+ public insertExportModifierAt(sourceFile: SourceFile, position: number): void {
+ this.insertText(sourceFile, position, "export ");
}
/**
diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json
index cc24ac7f9c405..56fca5181a1ad 100644
--- a/src/services/tsconfig.json
+++ b/src/services/tsconfig.json
@@ -79,6 +79,7 @@
"codefixes/fixPropertyAssignment.ts",
"codefixes/fixExtendsInterfaceBecomesImplements.ts",
"codefixes/fixForgottenThisPropertyAccess.ts",
+ "codefixes/fixImportNonExportedMember.ts",
"codefixes/fixInvalidJsxCharacters.ts",
"codefixes/fixUnusedIdentifier.ts",
"codefixes/fixUnreachableCode.ts",
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember1.ts b/tests/cases/fourslash/codeFixImportNonExportedMember1.ts
new file mode 100644
index 0000000000000..ef8eb3365ef7d
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember1.ts
@@ -0,0 +1,29 @@
+///
+// @Filename: /a.ts
+////declare function zoo(): any;
+////export { zoo };
+
+// @Filename: /b.ts
+////declare function foo(): any;
+////function bar(): any;
+////export { foo };
+
+// @Filename: /c.ts
+////import { zoo } from "./a";
+////import { bar } from "./b";
+
+goTo.file("/c.ts");
+verify.codeFixAvailable([
+ { description: `Export 'bar' from module './b'` },
+ { description: `Remove import from './a'` },
+ { description: `Remove import from './b'` },
+]);
+verify.codeFix({
+ index: 0,
+ description: `Export 'bar' from module './b'`,
+ newFileContent: {
+ '/b.ts': `declare function foo(): any;
+export function bar(): any;
+export { foo };`
+ }
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember2.ts b/tests/cases/fourslash/codeFixImportNonExportedMember2.ts
new file mode 100644
index 0000000000000..d6cda9317e92b
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember2.ts
@@ -0,0 +1,45 @@
+///
+
+// @Filename: /a.ts
+////let a = 1, b = 2, c = 3;
+////export function whatever() {
+////}
+
+// @Filename: /b.ts
+////let d = 4;
+////export function whatever2() {
+////}
+
+// @Filename: /c.ts
+////import { a } from "./a"
+////import { d } from "./b"
+
+goTo.file("/c.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Export 'd' from module './b'` },
+ { description: `Remove import from './a'` },
+ { description: `Remove import from './b'` },
+]);
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ '/a.ts': `let a = 1, b = 2, c = 3;
+export function whatever() {
+}
+
+export { a };
+`
+ }
+});
+
+verify.codeFix({
+ index: 1,
+ description: `Export 'd' from module './b'`,
+ newFileContent: {
+ '/b.ts': `export let d = 4;
+export function whatever2() {
+}`
+ }
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember3.ts b/tests/cases/fourslash/codeFixImportNonExportedMember3.ts
new file mode 100644
index 0000000000000..2927ba48cf6d4
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember3.ts
@@ -0,0 +1,27 @@
+///
+// @Filename: /a.ts
+////let a = 1, b = 2, c = 3;
+////let d = 4;
+////export function whatever() {
+////}
+////export { d }
+
+// @Filename: /b.ts
+////import { a, d } from "./a"
+
+goTo.file("/b.ts");
+verify.codeFixAvailable([
+ { description: `Export 'a' from module './a'` },
+ { description: `Remove import from './a'` },
+]);
+verify.codeFix({
+ index: 0,
+ description: `Export 'a' from module './a'`,
+ newFileContent: {
+ '/a.ts': `let a = 1, b = 2, c = 3;
+let d = 4;
+export function whatever() {
+}
+export { a, d };`
+ }
+});
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember4.ts b/tests/cases/fourslash/codeFixImportNonExportedMember4.ts
new file mode 100644
index 0000000000000..381a0e0d380b7
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember4.ts
@@ -0,0 +1,9 @@
+///
+// @Filename: /node_modules/foo/index.d.ts
+////let a = 0
+////module.exports = 0;
+
+// @Filename: /a.ts
+////import { a } from "foo";
+
+verify.not.codeFixAvailable();
\ No newline at end of file
diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all.ts
new file mode 100644
index 0000000000000..8486a0680ae36
--- /dev/null
+++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all.ts
@@ -0,0 +1,22 @@
+///
+
+// @Filename: /a.ts
+////declare function foo(): any;
+////declare function bar(): any;
+////declare function zoo(): any;
+////export { zoo }
+
+// @Filename: /b.ts
+////import { foo, bar } from "./a";
+
+goTo.file("/b.ts");
+verify.codeFixAll({
+ fixId: "importNonExportedMember",
+ fixAllDescription: ts.Diagnostics.Add_all_missing_exports.message,
+ newFileContent: {
+ '/a.ts': `export declare function foo(): any;
+export declare function bar(): any;
+declare function zoo(): any;
+export { zoo }`
+ }
+});