Skip to content

Commit d5142a7

Browse files
author
Andy
authored
Don't offer import completions in non-module files unless "--module" is set (#22951)
* Don't offer import completions in non-module files unless "--module" is set * Even smarter shouldOfferImportCompletions
1 parent 414bc49 commit d5142a7

18 files changed

+125
-70
lines changed

src/services/completions.ts

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,9 @@ namespace ts.Completions {
2727

2828
const enum GlobalsSearch { Continue, Success, Fail }
2929

30-
export function getCompletionsAtPosition(
31-
host: LanguageServiceHost,
32-
typeChecker: TypeChecker,
33-
log: Log,
34-
compilerOptions: CompilerOptions,
35-
sourceFile: SourceFile,
36-
position: number,
37-
allSourceFiles: ReadonlyArray<SourceFile>,
38-
preferences: UserPreferences,
39-
): CompletionInfo | undefined {
30+
export function getCompletionsAtPosition(host: LanguageServiceHost, program: Program, log: Log, sourceFile: SourceFile, position: number, preferences: UserPreferences): CompletionInfo | undefined {
31+
const typeChecker = program.getTypeChecker();
32+
const compilerOptions = program.getCompilerOptions();
4033
if (isInReferenceComment(sourceFile, position)) {
4134
const entries = PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host);
4235
return entries && convertPathCompletions(entries);
@@ -55,7 +48,7 @@ namespace ts.Completions {
5548
return getLabelCompletionAtPosition(contextToken.parent);
5649
}
5750

58-
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, preferences, compilerOptions.target);
51+
const completionData = getCompletionData(program, log, sourceFile, position, preferences);
5952
if (!completionData) {
6053
return undefined;
6154
}
@@ -480,16 +473,9 @@ namespace ts.Completions {
480473
previousToken: Node;
481474
readonly isJsxInitializer: IsJsxInitializer;
482475
}
483-
function getSymbolCompletionFromEntryId(
484-
typeChecker: TypeChecker,
485-
log: (message: string) => void,
486-
compilerOptions: CompilerOptions,
487-
sourceFile: SourceFile,
488-
position: number,
489-
{ name, source }: CompletionEntryIdentifier,
490-
allSourceFiles: ReadonlyArray<SourceFile>,
476+
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, { name, source }: CompletionEntryIdentifier,
491477
): SymbolCompletion | { type: "request", request: Request } | { type: "none" } {
492-
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, compilerOptions.target);
478+
const completionData = getCompletionData(program, log, sourceFile, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true });
493479
if (!completionData) {
494480
return { type: "none" };
495481
}
@@ -505,7 +491,7 @@ namespace ts.Completions {
505491
// completion entry.
506492
return firstDefined<Symbol, SymbolCompletion>(symbols, (symbol): SymbolCompletion => { // TODO: Shouldn't need return type annotation (GH#12632)
507493
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
508-
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target, origin, completionKind);
494+
const info = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, origin, completionKind);
509495
return info && info.name === name && getSourceFromOrigin(origin) === source ? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer } : undefined;
510496
}) || { type: "none" };
511497
}
@@ -525,18 +511,17 @@ namespace ts.Completions {
525511

526512
export function getCompletionEntryDetails(
527513
program: Program,
528-
log: (message: string) => void,
529-
compilerOptions: CompilerOptions,
514+
log: Log,
530515
sourceFile: SourceFile,
531516
position: number,
532517
entryId: CompletionEntryIdentifier,
533-
allSourceFiles: ReadonlyArray<SourceFile>,
534518
host: LanguageServiceHost,
535519
formatContext: formatting.FormatContext,
536520
getCanonicalFileName: GetCanonicalFileName,
537521
preferences: UserPreferences,
538522
): CompletionEntryDetails {
539523
const typeChecker = program.getTypeChecker();
524+
const compilerOptions = program.getCompilerOptions();
540525
const { name } = entryId;
541526

542527
const contextToken = findPrecedingToken(position, sourceFile);
@@ -548,7 +533,7 @@ namespace ts.Completions {
548533
}
549534

550535
// Compute all the completion symbols again.
551-
const symbolCompletion = getSymbolCompletionFromEntryId(typeChecker, log, compilerOptions, sourceFile, position, entryId, allSourceFiles);
536+
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
552537
switch (symbolCompletion.type) {
553538
case "request": {
554539
const { request } = symbolCompletion;
@@ -565,7 +550,7 @@ namespace ts.Completions {
565550
}
566551
case "symbol": {
567552
const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion;
568-
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, allSourceFiles, preferences);
553+
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences);
569554
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, codeActions, sourceDisplay);
570555
}
571556
case "none":
@@ -642,16 +627,8 @@ namespace ts.Completions {
642627
return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] };
643628
}
644629

645-
export function getCompletionEntrySymbol(
646-
typeChecker: TypeChecker,
647-
log: (message: string) => void,
648-
compilerOptions: CompilerOptions,
649-
sourceFile: SourceFile,
650-
position: number,
651-
entryId: CompletionEntryIdentifier,
652-
allSourceFiles: ReadonlyArray<SourceFile>,
653-
): Symbol | undefined {
654-
const completion = getSymbolCompletionFromEntryId(typeChecker, log, compilerOptions, sourceFile, position, entryId, allSourceFiles);
630+
export function getCompletionEntrySymbol(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier): Symbol | undefined {
631+
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
655632
return completion.type === "symbol" ? completion.symbol : undefined;
656633
}
657634

@@ -760,14 +737,14 @@ namespace ts.Completions {
760737
}
761738

762739
function getCompletionData(
763-
typeChecker: TypeChecker,
740+
program: Program,
764741
log: (message: string) => void,
765742
sourceFile: SourceFile,
766743
position: number,
767-
allSourceFiles: ReadonlyArray<SourceFile>,
768744
preferences: Pick<UserPreferences, "includeCompletionsForModuleExports" | "includeCompletionsWithInsertText">,
769-
target: ScriptTarget,
770745
): CompletionData | Request | undefined {
746+
const typeChecker = program.getTypeChecker();
747+
771748
let start = timestamp();
772749
let currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853
773750
// We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.)
@@ -1168,13 +1145,30 @@ namespace ts.Completions {
11681145
}
11691146
}
11701147

1171-
// Don't suggest import completions for a commonjs-only module
1172-
if (preferences.includeCompletionsForModuleExports && !(sourceFile.commonJsModuleIndicator && !sourceFile.externalModuleIndicator)) {
1173-
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", target);
1148+
if (shouldOfferImportCompletions()) {
1149+
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", program.getCompilerOptions().target);
11741150
}
11751151
filterGlobalCompletion(symbols);
11761152
}
11771153

1154+
function shouldOfferImportCompletions(): boolean {
1155+
// If not already a module, must have modules enabled and not currently be in a commonjs module. (TODO: import completions for commonjs)
1156+
if (!preferences.includeCompletionsForModuleExports) return false;
1157+
// If already using ES6 modules, OK to continue using them.
1158+
if (sourceFile.externalModuleIndicator) return true;
1159+
// If already using commonjs, don't introduce ES6.
1160+
if (sourceFile.commonJsModuleIndicator) return false;
1161+
// If some file is using ES6 modules, assume that it's OK to add more.
1162+
if (program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator)) {
1163+
return true;
1164+
}
1165+
// For JS, stay on the safe side.
1166+
if (isSourceFileJavaScript(sourceFile)) return false;
1167+
// If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK.
1168+
const compilerOptions = program.getCompilerOptions();
1169+
return !!compilerOptions.module || compilerOptions.target >= ScriptTarget.ES2015 || !!compilerOptions.noEmit;
1170+
}
1171+
11781172
function isSnippetScope(scopeNode: Node): boolean {
11791173
switch (scopeNode.kind) {
11801174
case SyntaxKind.SourceFile:
@@ -1268,7 +1262,7 @@ namespace ts.Completions {
12681262
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
12691263
const tokenTextLowerCase = tokenText.toLowerCase();
12701264

1271-
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, allSourceFiles, moduleSymbol => {
1265+
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
12721266
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
12731267
// Don't add a completion for a re-export, only for the original.
12741268
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.

src/services/services.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,12 +1404,10 @@ namespace ts {
14041404
synchronizeHostData();
14051405
return Completions.getCompletionsAtPosition(
14061406
host,
1407-
program.getTypeChecker(),
1407+
program,
14081408
log,
1409-
program.getCompilerOptions(),
14101409
getValidSourceFile(fileName),
14111410
position,
1412-
program.getSourceFiles(),
14131411
fullPreferences);
14141412
}
14151413

@@ -1418,11 +1416,9 @@ namespace ts {
14181416
return Completions.getCompletionEntryDetails(
14191417
program,
14201418
log,
1421-
program.getCompilerOptions(),
14221419
getValidSourceFile(fileName),
14231420
position,
14241421
{ name, source },
1425-
program.getSourceFiles(),
14261422
host,
14271423
formattingOptions && formatting.getFormatContext(formattingOptions),
14281424
getCanonicalFileName,
@@ -1431,14 +1427,7 @@ namespace ts {
14311427

14321428
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol {
14331429
synchronizeHostData();
1434-
return Completions.getCompletionEntrySymbol(
1435-
program.getTypeChecker(),
1436-
log,
1437-
program.getCompilerOptions(),
1438-
getValidSourceFile(fileName),
1439-
position,
1440-
{ name, source },
1441-
program.getSourceFiles());
1430+
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source });
14421431
}
14431432

14441433
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {

tests/cases/fourslash/completionsImportBaseUrl.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// @Filename: /tsconfig.json
44
////{
55
//// "compilerOptions": {
6-
//// "baseUrl": "."
6+
//// "baseUrl": ".",
7+
//// "module": "esnext"
78
//// }
89
////}
910

@@ -16,6 +17,6 @@
1617
// Test that it prefers a relative import (see sourceDisplay).
1718
goTo.marker("");
1819
verify.completionListContains({ name: "foo", source: "/src/a" }, "const foo: 0", "", "const", undefined, /*hasAction*/ true, {
19-
includeExternalModuleExports: true,
20+
includeCompletionsForModuleExports: true,
2021
sourceDisplay: "./a",
2122
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @allowJs: true
4+
// @module: commonjs
5+
6+
// @Filename: /node_modules/a/index.d.ts
7+
////export const foo = 0;
8+
9+
// @Filename: /b.js
10+
////const a = require("./a");
11+
////fo/*b*/
12+
13+
// @Filename: /c.js
14+
////const x = 0;/*c*/ // Off for JS files (unless a non-declaration external module exists in the project)
15+
16+
// @Filename: /c2.ts
17+
////const x = 0;/*c2*/
18+
19+
// @Filename: /d.js
20+
////const a = import("./a"); // Does not make this an external module
21+
////fo/*d*/
22+
23+
// @Filename: /d2.ts
24+
////const a = import("./a"); // Does not make this an external module
25+
////fo/*d2*/
26+
27+
for (const marker of ["b", "c", "d"]) {
28+
goTo.marker(marker);
29+
verify.not.completionListContains({ name: "foo", source: "/node_modules/a/index" }, undefined, undefined, undefined, undefined, undefined, {
30+
includeCompletionsForModuleExports: true
31+
});
32+
}
33+
34+
for (const marker of ["c2", "d2"]) {
35+
goTo.marker(marker);
36+
verify.completionListContains({ name: "foo", source: "/node_modules/a/index" }, "const foo: 0", "", "const", /*spanIndex*/ undefined, /*hasAction*/ true, {
37+
includeCompletionsForModuleExports: true,
38+
sourceDisplay: "a",
39+
});
40+
}

tests/cases/fourslash/completionsImport_default_anonymous.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference path="fourslash.ts" />
22

33
// Use `/src` to test that directory names are not included in conversion from module path to identifier.
4+
// @module: esnext
45

56
// @Filename: /src/foo-bar.ts
67
////export default 0;

tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference path="fourslash.ts" />
22

3-
// @noLib: true
3+
// @module: esnext
44

55
// @Filename: /a.ts
66
////export default function foo() {}

tests/cases/fourslash/completionsImport_default_exportDefaultIdentifier.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
// Tests that we use the name "foo".
44

5+
// @module: esnext
6+
57
// @Filename: /a.ts
68
////const foo = 0;
79
////export default foo;

tests/cases/fourslash/completionsImport_fromAmbientModule.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/// <reference path="fourslash.ts" />
22

3+
// @module: esnext
4+
35
// @Filename: /a.ts
46
////declare module "m" {
57
//// export const x: number;

tests/cases/fourslash/completionsImport_matching.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/// <reference path="fourslash.ts" />
22

3+
// @module: esnext
4+
35
// @Filename: /a.ts
46
// Not included:
57
////export function abcde() {}

tests/cases/fourslash/completionsImport_multipleWithSameName.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/// <reference path="fourslash.ts" />
22

3+
// @module: esnext
4+
35
// @Filename: /global.d.ts
46
// A local variable would prevent import completions (see `completionsImport_shadowedByLocal.ts`), but a global doesn't.
57
////declare var foo: number;

0 commit comments

Comments
 (0)