Skip to content

Commit 552bd1c

Browse files
author
Andy
authored
Support import fix/completions for export = (#25708)
1 parent b94061c commit 552bd1c

12 files changed

+132
-25
lines changed

src/compiler/checker.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,8 +2286,6 @@ namespace ts {
22862286
return getPackagesSet().has(getTypesPackageName(packageName));
22872287
}
22882288

2289-
// An external module with an 'export =' declaration resolves to the target of the 'export =' declaration,
2290-
// and an external module with no 'export =' declaration resolves to the module itself.
22912289
function resolveExternalModuleSymbol(moduleSymbol: Symbol, dontResolveAlias?: boolean): Symbol;
22922290
function resolveExternalModuleSymbol(moduleSymbol: Symbol | undefined, dontResolveAlias?: boolean): Symbol | undefined;
22932291
function resolveExternalModuleSymbol(moduleSymbol: Symbol, dontResolveAlias?: boolean): Symbol {

src/compiler/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3097,6 +3097,10 @@ namespace ts {
30973097
*/
30983098
/* @internal */ getAccessibleSymbolChain(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean): Symbol[] | undefined;
30993099
/* @internal */ getTypePredicateOfSignature(signature: Signature): TypePredicate;
3100+
/**
3101+
* An external module with an 'export =' declaration resolves to the target of the 'export =' declaration,
3102+
* and an external module with no 'export =' declaration resolves to the module itself.
3103+
*/
31003104
/* @internal */ resolveExternalModuleSymbol(symbol: Symbol): Symbol;
31013105
/** @param node A location where we might consider accessing `this`. Not necessarily a ThisExpression. */
31023106
/* @internal */ tryGetThisTypeAt(node: Node): Type | undefined;

src/harness/fourslash.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,8 +908,8 @@ namespace FourSlash {
908908
}
909909

910910
private verifyCompletionEntry(actual: ts.CompletionEntry, expected: FourSlashInterface.ExpectedCompletionEntry) {
911-
const { insertText, replacementSpan, hasAction, isRecommended, kind, text, documentation, sourceDisplay } = typeof expected === "string"
912-
? { insertText: undefined, replacementSpan: undefined, hasAction: undefined, isRecommended: undefined, kind: undefined, text: undefined, documentation: undefined, sourceDisplay: undefined }
911+
const { insertText, replacementSpan, hasAction, isRecommended, kind, text, documentation, source, sourceDisplay } = typeof expected === "string"
912+
? { insertText: undefined, replacementSpan: undefined, hasAction: undefined, isRecommended: undefined, kind: undefined, text: undefined, documentation: undefined, source: undefined, sourceDisplay: undefined }
913913
: expected;
914914

915915
if (actual.insertText !== insertText) {
@@ -927,6 +927,7 @@ namespace FourSlash {
927927

928928
assert.equal(actual.hasAction, hasAction);
929929
assert.equal(actual.isRecommended, isRecommended);
930+
assert.equal(actual.source, source);
930931

931932
if (text) {
932933
const actualDetails = this.getCompletionEntryDetails(actual.name, actual.source)!;
@@ -4789,6 +4790,7 @@ namespace FourSlashInterface {
47894790

47904791
export type ExpectedCompletionEntry = string | {
47914792
readonly name: string,
4793+
readonly source?: string,
47924794
readonly insertText?: string,
47934795
readonly replacementSpan?: FourSlash.Range,
47944796
readonly hasAction?: boolean, // If not specified, will assert that this is false.

src/services/codefixes/importFixes.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ namespace ts.codefix {
163163
position: number,
164164
preferences: UserPreferences,
165165
): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } {
166-
const exportInfos = getAllReExportingModules(exportedSymbol, moduleSymbol, symbolName, sourceFile, program.getTypeChecker(), program.getSourceFiles());
166+
const exportInfos = getAllReExportingModules(exportedSymbol, moduleSymbol, symbolName, sourceFile, program.getCompilerOptions(), program.getTypeChecker(), program.getSourceFiles());
167167
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol));
168168
// We sort the best codefixes first, so taking `first` is best for completions.
169169
const moduleSpecifier = first(getNewImportInfos(program, sourceFile, position, exportInfos, host, preferences)).moduleSpecifier;
@@ -175,18 +175,22 @@ namespace ts.codefix {
175175
return { description, changes, commands };
176176
}
177177

178-
function getAllReExportingModules(exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, sourceFile: SourceFile, checker: TypeChecker, allSourceFiles: ReadonlyArray<SourceFile>): ReadonlyArray<SymbolExportInfo> {
178+
function getAllReExportingModules(exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, sourceFile: SourceFile, compilerOptions: CompilerOptions, checker: TypeChecker, allSourceFiles: ReadonlyArray<SourceFile>): ReadonlyArray<SymbolExportInfo> {
179179
const result: SymbolExportInfo[] = [];
180180
forEachExternalModule(checker, allSourceFiles, (moduleSymbol, moduleFile) => {
181181
// Don't import from a re-export when looking "up" like to `./index` or `../index`.
182182
if (moduleFile && moduleSymbol !== exportingModuleSymbol && startsWith(sourceFile.fileName, getDirectoryPath(moduleFile.fileName))) {
183183
return;
184184
}
185185

186+
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
187+
if (defaultInfo && defaultInfo.name === symbolName && skipAlias(defaultInfo.symbol, checker) === exportedSymbol) {
188+
result.push({ moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol) });
189+
}
190+
186191
for (const exported of checker.getExportsOfModule(moduleSymbol)) {
187-
if ((exported.escapedName === InternalSymbolName.Default || exported.name === symbolName) && skipAlias(exported, checker) === exportedSymbol) {
188-
const isDefaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol) === exported;
189-
result.push({ moduleSymbol, importKind: isDefaultExport ? ImportKind.Default : ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported) });
192+
if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) {
193+
result.push({ moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported) });
190194
}
191195
}
192196
});
@@ -400,13 +404,9 @@ namespace ts.codefix {
400404
forEachExternalModuleToImportFrom(checker, sourceFile, program.getSourceFiles(), moduleSymbol => {
401405
cancellationToken.throwIfCancellationRequested();
402406

403-
// check the default export
404-
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
405-
if (defaultExport) {
406-
const info = getDefaultExportInfo(defaultExport, moduleSymbol, program);
407-
if (info && info.name === symbolName && symbolHasMeaning(info.symbolForMeaning, currentTokenMeaning)) {
408-
addSymbol(moduleSymbol, defaultExport, ImportKind.Default);
409-
}
407+
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, program.getCompilerOptions());
408+
if (defaultInfo && defaultInfo.name === symbolName && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) {
409+
addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.kind);
410410
}
411411

412412
// check exports with the same name
@@ -418,19 +418,36 @@ namespace ts.codefix {
418418
return originalSymbolToExportInfos;
419419
}
420420

421-
function getDefaultExportInfo(defaultExport: Symbol, moduleSymbol: Symbol, program: Program): { readonly symbolForMeaning: Symbol, readonly name: string } | undefined {
421+
function getDefaultLikeExportInfo(
422+
moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions,
423+
): { readonly symbol: Symbol, readonly symbolForMeaning: Symbol, readonly name: string, readonly kind: ImportKind.Default | ImportKind.Equals } | undefined {
424+
const exported = getDefaultLikeExportWorker(moduleSymbol, checker);
425+
if (!exported) return undefined;
426+
const { symbol, kind } = exported;
427+
const info = getDefaultExportInfoWorker(symbol, moduleSymbol, checker, compilerOptions);
428+
return info && { symbol, symbolForMeaning: info.symbolForMeaning, name: info.name, kind };
429+
}
430+
431+
function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly kind: ImportKind.Default | ImportKind.Equals } | undefined {
432+
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
433+
if (defaultExport) return { symbol: defaultExport, kind: ImportKind.Default };
434+
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
435+
return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, kind: ImportKind.Equals };
436+
}
437+
438+
function getDefaultExportInfoWorker(defaultExport: Symbol, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbolForMeaning: Symbol, readonly name: string } | undefined {
422439
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
423440
if (localSymbol) return { symbolForMeaning: localSymbol, name: localSymbol.name };
424441

425442
const name = getNameForExportDefault(defaultExport);
426443
if (name !== undefined) return { symbolForMeaning: defaultExport, name };
427444

428445
if (defaultExport.flags & SymbolFlags.Alias) {
429-
const aliased = program.getTypeChecker().getImmediateAliasedSymbol(defaultExport);
430-
return aliased && getDefaultExportInfo(aliased, Debug.assertDefined(aliased.parent), program);
446+
const aliased = checker.getImmediateAliasedSymbol(defaultExport);
447+
return aliased && getDefaultExportInfoWorker(aliased, Debug.assertDefined(aliased.parent), checker, compilerOptions);
431448
}
432449
else {
433-
return { symbolForMeaning: defaultExport, name: moduleSymbolToValidIdentifier(moduleSymbol, program.getCompilerOptions().target!) };
450+
return { symbolForMeaning: defaultExport, name: moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target!) };
434451
}
435452
}
436453

src/services/completions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,14 @@ namespace ts.Completions {
13781378
return;
13791379
}
13801380

1381+
if (resolvedModuleSymbol !== moduleSymbol &&
1382+
// Don't add another completion for `export =` of a symbol that's already global.
1383+
// So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`.
1384+
resolvedModuleSymbol.declarations.some(d => !!d.getSourceFile().externalModuleIndicator)) {
1385+
symbols.push(resolvedModuleSymbol);
1386+
symbolToOriginInfoMap[getSymbolId(resolvedModuleSymbol)] = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport: false };
1387+
}
1388+
13811389
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
13821390
// Don't add a completion for a re-export, only for the original.
13831391
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.

tests/cases/fourslash/completionsImport_default_anonymous.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
////fooB/*1*/
1212

1313
goTo.marker("0");
14-
const preferences = { includeCompletionsForModuleExports: true };
14+
const preferences: FourSlashInterface.UserPreferences = { includeCompletionsForModuleExports: true };
1515
verify.completions(
1616
{ marker: "0", excludes: { name: "default", source: "/src/foo-bar" }, preferences },
1717
{ marker: "1", includes: { name: "fooBar", source: "/src/foo-bar", sourceDisplay: "./foo-bar", text: "(property) default: 0", kind: "property", hasAction: true }, preferences }
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: commonjs
4+
5+
// @Filename: /a.d.ts
6+
////declare function a(): void;
7+
////declare namespace a {
8+
//// export interface b {}
9+
////}
10+
////export = a;
11+
12+
// @Filename: /b.ts
13+
////a/*0*/;
14+
////let x: b/*1*/;
15+
16+
const preferences: FourSlashInterface.UserPreferences = { includeCompletionsForModuleExports: true };
17+
verify.completions(
18+
{
19+
marker: "0",
20+
includes: { name: "a", source: "/a", hasAction: true, },
21+
preferences,
22+
},
23+
{
24+
marker: "1",
25+
includes: { name: "b", source: "/a", hasAction: true },
26+
preferences,
27+
}
28+
);
29+
30+
// Import { b } first, or it will just add a qualified name from 'a' (which isn't what we're trying to test)
31+
verify.applyCodeActionFromCompletion("1", {
32+
name: "b",
33+
source: "/a",
34+
description: `Import 'b' from module "./a"`,
35+
newFileContent:
36+
`import { b } from "./a";
37+
38+
a;
39+
let x: b;`,
40+
});
41+
42+
verify.applyCodeActionFromCompletion("0", {
43+
name: "a",
44+
source: "/a",
45+
description: `Import 'a' from module "./a"`,
46+
newFileContent:
47+
`import { b } from "./a";
48+
import a = require("./a");
49+
50+
a;
51+
let x: b;`,
52+
});
53+

tests/cases/fourslash/completionsImport_exportEqualsNamespace_noDuplicate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
verify.completions({
1919
marker: "",
2020
// Tester will assert that it is only included once
21-
includes: [{ name: "foo", hasAction: true }],
21+
includes: [{ name: "foo", source: "a", hasAction: true }],
2222
preferences: {
2323
includeCompletionsForModuleExports: true,
2424
}

tests/cases/fourslash/completionsImport_tsx.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
verify.completions({
1414
marker: "",
15-
includes: { name: "Foo", source: "/a.tsx", hasAction: true },
15+
includes: { name: "Foo", source: "/a", hasAction: true },
1616
excludes: "Bar",
1717
preferences: {
1818
includeCompletionsForModuleExports: true,

tests/cases/fourslash/completionsUniqueSymbol_import.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ verify.completions({
2424
marker: "",
2525
exact: [
2626
"n",
27-
{ name: "publicSym", insertText: "[publicSym]", replacementSpan: test.ranges()[0], hasAction: true },
27+
{ name: "publicSym", source: "/a", insertText: "[publicSym]", replacementSpan: test.ranges()[0], hasAction: true },
2828
],
2929
preferences: {
3030
includeInsertTextCompletions: true,

tests/cases/fourslash/importNameCodeFix_all.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ verify.codeFixAll({
3737
fixId: "fixMissingImport",
3838
fixAllDescription: "Add all missing imports",
3939
newFileContent:
40-
// TODO: GH#25135 (should import 'e')
4140
`import bd, * as b from "./b";
4241
import cd, { c0 } from "./c";
4342
import dd, { d0, d1 } from "./d";
4443
import ad, { a0 } from "./a";
44+
import e = require("./e");
4545
4646
ad; ad; a0; a0;
4747
bd; bd; b.b0; b.b0;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /a.d.ts
4+
////declare function a(): void;
5+
////declare namespace a {
6+
//// export interface b {}
7+
////}
8+
////export = a;
9+
10+
// @Filename: /b.ts
11+
////a;
12+
////let x: b;
13+
14+
goTo.file("/b.ts");
15+
verify.codeFixAll({
16+
fixId: "fixMissingImport",
17+
fixAllDescription: "Add all missing imports",
18+
newFileContent:
19+
`import { b } from "./a";
20+
21+
import a = require("./a");
22+
23+
a;
24+
let x: b;`,
25+
});

0 commit comments

Comments
 (0)