Skip to content

Commit f3ec897

Browse files
author
Richard Lynch
committed
Add fixImportNonExportedMember
Signed-off-by: Richard Lynch <[email protected]>
1 parent ffa9cf2 commit f3ec897

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const fixId = "importNonExportedMember";
4+
5+
const errorCodes = [
6+
Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported.code,
7+
];
8+
9+
registerCodeFix({
10+
errorCodes,
11+
12+
getCodeActions(context) {
13+
const { sourceFile } = context;
14+
15+
const info = getInfo(sourceFile, context, context.span.start);
16+
17+
if (!info || info.originSourceFile.isDeclarationFile) {
18+
return undefined;
19+
}
20+
21+
const changes = textChanges.ChangeTracker.with(context, (t) =>
22+
doChange(t, info.originSourceFile, info.node)
23+
);
24+
25+
return [
26+
createCodeFixAction(
27+
/*fixName*/ fixId,
28+
changes,
29+
/*description*/ [
30+
Diagnostics.Export_0_from_module_1,
31+
info.node.text,
32+
showModuleSpecifier(info.importDecl),
33+
],
34+
fixId,
35+
/*fixAllDescription*/ Diagnostics.Add_all_missing_exports
36+
),
37+
];
38+
},
39+
40+
fixIds: [fixId],
41+
42+
getAllCodeActions: (context) =>
43+
codeFixAll(context, errorCodes, (changes, diag) => {
44+
const info = getInfo(diag.file, context, diag.start);
45+
46+
if (info) {
47+
doChange(changes, info.originSourceFile, info.node);
48+
}
49+
}),
50+
});
51+
52+
interface Info {
53+
readonly node: Identifier;
54+
55+
readonly importDecl: ImportDeclaration;
56+
57+
readonly originSourceFile: SourceFile;
58+
}
59+
60+
function getInfo(
61+
sourceFile: SourceFile,
62+
context: CodeFixContext | CodeFixAllContext,
63+
pos: number
64+
): Info | undefined {
65+
const node = getTokenAtPosition(sourceFile, pos);
66+
67+
if (!isIdentifier(node)) {
68+
return;
69+
}
70+
71+
const importDecl = findAncestor(node, isImportDeclaration);
72+
73+
if (!importDecl || !isStringLiteralLike(importDecl.moduleSpecifier)) {
74+
return undefined;
75+
}
76+
77+
const resolvedModule = getResolvedModule(
78+
sourceFile,
79+
/*moduleName*/ importDecl.moduleSpecifier.text,
80+
/*mode*/ undefined
81+
);
82+
83+
if (!resolvedModule) {
84+
return undefined;
85+
}
86+
87+
const originSourceFile = context.program.getSourceFile(
88+
resolvedModule.resolvedFileName
89+
);
90+
91+
if (!originSourceFile) {
92+
return undefined;
93+
}
94+
95+
return { node, importDecl, originSourceFile };
96+
}
97+
98+
function getNamedExportDeclaration(
99+
sourceFile: SourceFile
100+
): ExportDeclaration | undefined {
101+
let namedExport;
102+
103+
for (const statement of sourceFile.statements) {
104+
if (
105+
isExportDeclaration(statement) &&
106+
statement.exportClause &&
107+
isNamedExports(statement.exportClause)
108+
) {
109+
namedExport = statement;
110+
}
111+
}
112+
113+
return namedExport;
114+
}
115+
116+
function compareIdentifiers(s1: Identifier, s2: Identifier) {
117+
return compareStringsCaseInsensitive(s1.text, s2.text);
118+
}
119+
120+
function sortSpecifiers(
121+
specifiers: ExportSpecifier[]
122+
): readonly ExportSpecifier[] {
123+
return stableSort(specifiers, (s1, s2) =>
124+
compareIdentifiers(
125+
s1.propertyName || s1.name,
126+
s2.propertyName || s2.name
127+
)
128+
);
129+
}
130+
131+
const isVariableDeclarationListWith1Element = (
132+
node: Node
133+
): node is VariableDeclarationList =>
134+
!(isVariableDeclarationList(node) && node.declarations.length !== 1);
135+
136+
function doChange(
137+
changes: textChanges.ChangeTracker,
138+
sourceFile: SourceFile,
139+
node: Identifier
140+
): void {
141+
const moduleSymbol = sourceFile.localSymbol ?? sourceFile.symbol;
142+
143+
const localSymbol = moduleSymbol.valueDeclaration?.locals?.get(
144+
node.escapedText
145+
);
146+
147+
if (localSymbol === undefined) {
148+
return;
149+
}
150+
151+
// Node we need to export is a function
152+
if (isFunctionSymbol(localSymbol)) {
153+
const node = localSymbol.valueDeclaration;
154+
155+
if (node === undefined) {
156+
return;
157+
}
158+
159+
if (!isDeclarationStatement(node) && !isVariableStatement(node)) {
160+
return;
161+
}
162+
163+
return changes.insertExportModifier(sourceFile, node);
164+
}
165+
// Node we need to export is a variable declaration
166+
else if (
167+
localSymbol.valueDeclaration &&
168+
isVariableDeclarationListWith1Element(
169+
localSymbol.valueDeclaration.parent
170+
)
171+
) {
172+
const node = localSymbol.valueDeclaration.parent;
173+
174+
return changes.insertExportModifier(sourceFile, node);
175+
}
176+
177+
// In all other cases => the node is a variable, and should be exported via `export {a}`
178+
const namedExportDeclaration = getNamedExportDeclaration(sourceFile);
179+
180+
// If there is an existing export
181+
if (
182+
namedExportDeclaration?.exportClause &&
183+
isNamedExports(namedExportDeclaration.exportClause)
184+
) {
185+
return changes.replaceNode(
186+
sourceFile,
187+
namedExportDeclaration,
188+
factory.updateExportDeclaration(
189+
/*node*/ namedExportDeclaration,
190+
/*modifiers*/ undefined,
191+
/*isTypeOnly*/ false,
192+
/*exportClause*/ factory.updateNamedExports(
193+
/*node*/ namedExportDeclaration.exportClause,
194+
/*elements*/ sortSpecifiers(
195+
namedExportDeclaration.exportClause.elements.concat(
196+
factory.createExportSpecifier(
197+
/*isTypeOnly*/ false,
198+
/*propertyName*/ undefined,
199+
node
200+
)
201+
)
202+
)
203+
),
204+
/*moduleSpecifier*/ undefined,
205+
/*assertClause*/ undefined
206+
)
207+
);
208+
}
209+
// There is no existing export
210+
else {
211+
return changes.insertNodeAtEndOfScope(
212+
sourceFile,
213+
sourceFile,
214+
factory.createExportDeclaration(
215+
/*modifiers*/ undefined,
216+
/*isTypeOnly*/ false,
217+
/*exportClause*/ factory.createNamedExports([
218+
factory.createExportSpecifier(
219+
/*isTypeOnly*/ false,
220+
/*propertyName*/ undefined,
221+
node
222+
),
223+
]),
224+
/*moduleSpecifier*/ undefined
225+
)
226+
);
227+
}
228+
}
229+
}

src/services/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"codefixes/convertConstToLet.ts",
116116
"codefixes/fixExpectedComma.ts",
117117
"codefixes/fixAddVoidToPromise.ts",
118+
"codefixes/fixImportNonExportedMember.ts",
118119
"refactors/convertExport.ts",
119120
"refactors/convertImport.ts",
120121
"refactors/convertToOptionalChainExpression.ts",
@@ -134,6 +135,6 @@
134135
"transform.ts",
135136
"shims.ts",
136137
"globalThisShim.ts",
137-
"exportAsModule.ts"
138+
"exportAsModule.ts",
138139
]
139140
}

0 commit comments

Comments
 (0)