Skip to content

Commit e99be8c

Browse files
author
Andy
authored
Avoid duplicate import completions when a namespace is exported by two different modules (#24473)
1 parent aa0cddd commit e99be8c

File tree

3 files changed

+35
-2
lines changed

3 files changed

+35
-2
lines changed

src/harness/fourslash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ namespace FourSlash {
868868
for (const entry of actualCompletions.entries) {
869869
if (actualByName.has(entry.name)) {
870870
// TODO: GH#23587
871-
if (entry.name !== "undefined" && entry.name !== "require") this.raiseError(`Duplicate completions for ${entry.name}`);
871+
if (entry.name !== "undefined" && entry.name !== "require") this.raiseError(`Duplicate (${actualCompletions.entries.filter(a => a.name === entry.name).length}) completions for ${entry.name}`);
872872
}
873873
else {
874874
actualByName.set(entry.name, entry);

src/services/completions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1320,20 +1320,28 @@ namespace ts.Completions {
13201320
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
13211321
const tokenTextLowerCase = tokenText.toLowerCase();
13221322

1323+
const seenResolvedModules = createMap<true>();
1324+
13231325
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
13241326
// Perf -- ignore other modules if this is a request for details
13251327
if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) {
13261328
return;
13271329
}
13281330

1331+
const resolvedModuleSymbol = typeChecker.resolveExternalModuleSymbol(moduleSymbol);
1332+
// resolvedModuleSymbol may be a namespace. A namespace may be `export =` by multiple module declarations, but only keep the first one.
1333+
if (!addToSeen(seenResolvedModules, getSymbolId(resolvedModuleSymbol))) {
1334+
return;
1335+
}
1336+
13291337
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
13301338
// Don't add a completion for a re-export, only for the original.
13311339
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
13321340
// This is just to avoid adding duplicate completion entries.
13331341
//
13341342
// If `symbol.parent !== ...`, this comes from an `export * from "foo"` re-export. Those don't create new symbols.
13351343
// If `some(...)`, this comes from an `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
1336-
if (typeChecker.getMergedSymbol(symbol.parent!) !== typeChecker.resolveExternalModuleSymbol(moduleSymbol)
1344+
if (typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol
13371345
|| some(symbol.declarations, d => isExportSpecifier(d) && !!d.parent.parent.moduleSpecifier)) {
13381346
continue;
13391347
}
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: /node_modules/a/index.d.ts
4+
////declare namespace core {
5+
//// const foo: number;
6+
////}
7+
////declare module "a" {
8+
//// export = core;
9+
////}
10+
////declare module "a/alias" {
11+
//// export = core;
12+
////}
13+
14+
// @Filename: /user.ts
15+
////import * as a from "a";
16+
/////**/foo;
17+
18+
verify.completions({
19+
marker: "",
20+
// Tester will assert that it is only included once
21+
includes: [{ name: "foo", hasAction: true }],
22+
preferences: {
23+
includeCompletionsForModuleExports: true,
24+
}
25+
});

0 commit comments

Comments
 (0)