Skip to content

Commit 2bcfed0

Browse files
authored
feat(37440): Provide a quick-fix for non-exported types (#51038)
* feat(37440): add QF to handle missing exports * change diagnostic message * add type modifier only if isolatedModules is set or if the export declaration already uses type modifiers
1 parent a24201c commit 2bcfed0

27 files changed

+725
-35
lines changed

src/compiler/checker.ts

-31
Original file line numberDiff line numberDiff line change
@@ -7235,16 +7235,6 @@ namespace ts {
72357235
return statements;
72367236
}
72377237

7238-
function canHaveExportModifier(node: Statement): node is Extract<HasModifiers, Statement> {
7239-
return isEnumDeclaration(node) ||
7240-
isVariableStatement(node) ||
7241-
isFunctionDeclaration(node) ||
7242-
isClassDeclaration(node) ||
7243-
(isModuleDeclaration(node) && !isExternalModuleAugmentation(node) && !isGlobalScopeAugmentation(node)) ||
7244-
isInterfaceDeclaration(node) ||
7245-
isTypeDeclaration(node);
7246-
}
7247-
72487238
function addExportModifier(node: Extract<HasModifiers, Statement>) {
72497239
const flags = (getEffectiveModifierFlags(node) | ModifierFlags.Export) & ~ModifierFlags.Ambient;
72507240
return factory.updateModifiers(node, flags);
@@ -42691,27 +42681,6 @@ namespace ts {
4269142681
getNameOfDeclaration(name.parent) === name;
4269242682
}
4269342683

42694-
function isTypeDeclaration(node: Node): node is TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag | EnumDeclaration | ImportClause | ImportSpecifier | ExportSpecifier {
42695-
switch (node.kind) {
42696-
case SyntaxKind.TypeParameter:
42697-
case SyntaxKind.ClassDeclaration:
42698-
case SyntaxKind.InterfaceDeclaration:
42699-
case SyntaxKind.TypeAliasDeclaration:
42700-
case SyntaxKind.EnumDeclaration:
42701-
case SyntaxKind.JSDocTypedefTag:
42702-
case SyntaxKind.JSDocCallbackTag:
42703-
case SyntaxKind.JSDocEnumTag:
42704-
return true;
42705-
case SyntaxKind.ImportClause:
42706-
return (node as ImportClause).isTypeOnly;
42707-
case SyntaxKind.ImportSpecifier:
42708-
case SyntaxKind.ExportSpecifier:
42709-
return (node as ImportSpecifier | ExportSpecifier).parent.parent.isTypeOnly;
42710-
default:
42711-
return false;
42712-
}
42713-
}
42714-
4271542684
// True if the given identifier is part of a type reference
4271642685
function isTypeReferenceIdentifier(node: EntityName): boolean {
4271742686
while (node.parent.kind === SyntaxKind.QualifiedName) {

src/compiler/diagnosticMessages.json

+8
Original file line numberDiff line numberDiff line change
@@ -6687,6 +6687,14 @@
66876687
"category": "Message",
66886688
"code": 90058
66896689
},
6690+
"Export '{0}' from module '{1}'": {
6691+
"category": "Message",
6692+
"code": 90059
6693+
},
6694+
"Export all referenced locals": {
6695+
"category": "Message",
6696+
"code": 90060
6697+
},
66906698

66916699
"Convert function to an ES2015 class": {
66926700
"category": "Message",

src/compiler/utilities.ts

+26
Original file line numberDiff line numberDiff line change
@@ -7752,4 +7752,30 @@ namespace ts {
77527752
export function getParameterTypeNode(parameter: ParameterDeclaration | JSDocParameterTag) {
77537753
return parameter.kind === SyntaxKind.JSDocParameterTag ? parameter.typeExpression?.type : parameter.type;
77547754
}
7755+
7756+
export function isTypeDeclaration(node: Node): node is TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag | EnumDeclaration | ImportClause | ImportSpecifier | ExportSpecifier {
7757+
switch (node.kind) {
7758+
case SyntaxKind.TypeParameter:
7759+
case SyntaxKind.ClassDeclaration:
7760+
case SyntaxKind.InterfaceDeclaration:
7761+
case SyntaxKind.TypeAliasDeclaration:
7762+
case SyntaxKind.EnumDeclaration:
7763+
case SyntaxKind.JSDocTypedefTag:
7764+
case SyntaxKind.JSDocCallbackTag:
7765+
case SyntaxKind.JSDocEnumTag:
7766+
return true;
7767+
case SyntaxKind.ImportClause:
7768+
return (node as ImportClause).isTypeOnly;
7769+
case SyntaxKind.ImportSpecifier:
7770+
case SyntaxKind.ExportSpecifier:
7771+
return (node as ImportSpecifier | ExportSpecifier).parent.parent.isTypeOnly;
7772+
default:
7773+
return false;
7774+
}
7775+
}
7776+
7777+
export function canHaveExportModifier(node: Node): node is Extract<HasModifiers, Statement> {
7778+
return isEnumDeclaration(node) || isVariableStatement(node) || isFunctionDeclaration(node) || isClassDeclaration(node)
7779+
|| isInterfaceDeclaration(node) || isTypeDeclaration(node) || (isModuleDeclaration(node) && !isExternalModuleAugmentation(node) && !isGlobalScopeAugmentation(node));
7780+
}
77557781
}

src/services/codefixes/fixAddMissingMember.ts

-4
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,6 @@ namespace ts.codefix {
264264
return undefined;
265265
}
266266

267-
function isSourceFileFromLibrary(program: Program, node: SourceFile) {
268-
return program.isSourceFileFromExternalLibrary(node) || program.isSourceFileDefaultLibrary(node);
269-
}
270-
271267
function getActionsForMissingMemberDeclaration(context: CodeFixContext, info: TypeLikeDeclarationInfo): CodeFixAction[] | undefined {
272268
return info.isJSFile ? singleElementArray(createActionForAddMissingMemberInJavascriptFile(context, info)) :
273269
createActionsForAddMissingMemberInTypeScriptFile(context, info);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/* @internal */
2+
namespace 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+
}

src/services/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"codefixes/fixOverrideModifier.ts",
7272
"codefixes/fixNoPropertyAccessFromIndexSignature.ts",
7373
"codefixes/fixImplicitThis.ts",
74+
"codefixes/fixImportNonExportedMember.ts",
7475
"codefixes/fixIncorrectNamedTupleSyntax.ts",
7576
"codefixes/fixSpelling.ts",
7677
"codefixes/returnValueCorrect.ts",

src/services/utilities.ts

+4
Original file line numberDiff line numberDiff line change
@@ -3423,5 +3423,9 @@ namespace ts {
34233423
return jsx === JsxEmit.React || jsx === JsxEmit.ReactNative;
34243424
}
34253425

3426+
export function isSourceFileFromLibrary(program: Program, node: SourceFile) {
3427+
return program.isSourceFileFromExternalLibrary(node) || program.isSourceFileDefaultLibrary(node);
3428+
}
3429+
34263430
// #endregion
34273431
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: esnext
4+
// @filename: /a.ts
5+
////declare function foo(): any
6+
////declare function bar(): any;
7+
////export { foo };
8+
9+
// @filename: /b.ts
10+
////import { bar } from "./a";
11+
12+
goTo.file("/b.ts");
13+
verify.codeFix({
14+
description: [ts.Diagnostics.Export_0_from_module_1.message, "bar", "./a"],
15+
index: 0,
16+
newFileContent: {
17+
"/a.ts":
18+
`declare function foo(): any
19+
declare function bar(): any;
20+
export { foo, bar };`,
21+
}
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: esnext
4+
// @filename: /a.ts
5+
/////**
6+
//// * foo
7+
//// */
8+
////function foo() {}
9+
////export const bar = 1;
10+
11+
// @filename: /b.ts
12+
////import { foo } from "./a";
13+
14+
goTo.file("/b.ts");
15+
verify.codeFix({
16+
description: [ts.Diagnostics.Export_0_from_module_1.message, "foo", "./a"],
17+
index: 0,
18+
newFileContent: {
19+
"/a.ts":
20+
`/**
21+
* foo
22+
*/
23+
export function foo() {}
24+
export const bar = 1;`,
25+
}
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: esnext
4+
// @isolatedModules: true
5+
// @filename: /a.ts
6+
////type T = {};
7+
////export {};
8+
9+
// @filename: /b.ts
10+
////import { T } from "./a";
11+
12+
goTo.file("/b.ts");
13+
verify.codeFix({
14+
description: [ts.Diagnostics.Export_0_from_module_1.message, "T", "./a"],
15+
index: 0,
16+
newFileContent: {
17+
"/a.ts":
18+
`type T = {};
19+
export { type T };`,
20+
}
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: esnext
4+
// @filename: /a.ts
5+
////type T1 = {};
6+
////type T2 = {};
7+
////export { type T1 };
8+
9+
// @filename: /b.ts
10+
////import { T2 } from "./a";
11+
12+
goTo.file("/b.ts");
13+
verify.codeFix({
14+
description: [ts.Diagnostics.Export_0_from_module_1.message, "T2", "./a"],
15+
index: 0,
16+
newFileContent: {
17+
"/a.ts":
18+
`type T1 = {};
19+
type T2 = {};
20+
export { type T1, type T2 };`,
21+
}
22+
});

0 commit comments

Comments
 (0)