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
This commit is contained in:
Andy
2018-04-02 10:21:14 -07:00
committed by GitHub
parent 414bc49cc4
commit d5142a7f45
18 changed files with 125 additions and 70 deletions
+37 -43
View File
@@ -27,16 +27,9 @@ namespace ts.Completions {
const enum GlobalsSearch { Continue, Success, Fail }
export function getCompletionsAtPosition(
host: LanguageServiceHost,
typeChecker: TypeChecker,
log: Log,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
allSourceFiles: ReadonlyArray<SourceFile>,
preferences: UserPreferences,
): CompletionInfo | undefined {
export function getCompletionsAtPosition(host: LanguageServiceHost, program: Program, log: Log, sourceFile: SourceFile, position: number, preferences: UserPreferences): CompletionInfo | undefined {
const typeChecker = program.getTypeChecker();
const compilerOptions = program.getCompilerOptions();
if (isInReferenceComment(sourceFile, position)) {
const entries = PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host);
return entries && convertPathCompletions(entries);
@@ -55,7 +48,7 @@ namespace ts.Completions {
return getLabelCompletionAtPosition(contextToken.parent);
}
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, preferences, compilerOptions.target);
const completionData = getCompletionData(program, log, sourceFile, position, preferences);
if (!completionData) {
return undefined;
}
@@ -480,16 +473,9 @@ namespace ts.Completions {
previousToken: Node;
readonly isJsxInitializer: IsJsxInitializer;
}
function getSymbolCompletionFromEntryId(
typeChecker: TypeChecker,
log: (message: string) => void,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
{ name, source }: CompletionEntryIdentifier,
allSourceFiles: ReadonlyArray<SourceFile>,
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, { name, source }: CompletionEntryIdentifier,
): SymbolCompletion | { type: "request", request: Request } | { type: "none" } {
const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, compilerOptions.target);
const completionData = getCompletionData(program, log, sourceFile, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true });
if (!completionData) {
return { type: "none" };
}
@@ -505,7 +491,7 @@ namespace ts.Completions {
// completion entry.
return firstDefined<Symbol, SymbolCompletion>(symbols, (symbol): SymbolCompletion => { // TODO: Shouldn't need return type annotation (GH#12632)
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target, origin, completionKind);
const info = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, origin, completionKind);
return info && info.name === name && getSourceFromOrigin(origin) === source ? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer } : undefined;
}) || { type: "none" };
}
@@ -525,18 +511,17 @@ namespace ts.Completions {
export function getCompletionEntryDetails(
program: Program,
log: (message: string) => void,
compilerOptions: CompilerOptions,
log: Log,
sourceFile: SourceFile,
position: number,
entryId: CompletionEntryIdentifier,
allSourceFiles: ReadonlyArray<SourceFile>,
host: LanguageServiceHost,
formatContext: formatting.FormatContext,
getCanonicalFileName: GetCanonicalFileName,
preferences: UserPreferences,
): CompletionEntryDetails {
const typeChecker = program.getTypeChecker();
const compilerOptions = program.getCompilerOptions();
const { name } = entryId;
const contextToken = findPrecedingToken(position, sourceFile);
@@ -548,7 +533,7 @@ namespace ts.Completions {
}
// Compute all the completion symbols again.
const symbolCompletion = getSymbolCompletionFromEntryId(typeChecker, log, compilerOptions, sourceFile, position, entryId, allSourceFiles);
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
switch (symbolCompletion.type) {
case "request": {
const { request } = symbolCompletion;
@@ -565,7 +550,7 @@ namespace ts.Completions {
}
case "symbol": {
const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion;
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, allSourceFiles, preferences);
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences);
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, codeActions, sourceDisplay);
}
case "none":
@@ -642,16 +627,8 @@ namespace ts.Completions {
return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] };
}
export function getCompletionEntrySymbol(
typeChecker: TypeChecker,
log: (message: string) => void,
compilerOptions: CompilerOptions,
sourceFile: SourceFile,
position: number,
entryId: CompletionEntryIdentifier,
allSourceFiles: ReadonlyArray<SourceFile>,
): Symbol | undefined {
const completion = getSymbolCompletionFromEntryId(typeChecker, log, compilerOptions, sourceFile, position, entryId, allSourceFiles);
export function getCompletionEntrySymbol(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier): Symbol | undefined {
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
return completion.type === "symbol" ? completion.symbol : undefined;
}
@@ -760,14 +737,14 @@ namespace ts.Completions {
}
function getCompletionData(
typeChecker: TypeChecker,
program: Program,
log: (message: string) => void,
sourceFile: SourceFile,
position: number,
allSourceFiles: ReadonlyArray<SourceFile>,
preferences: Pick<UserPreferences, "includeCompletionsForModuleExports" | "includeCompletionsWithInsertText">,
target: ScriptTarget,
): CompletionData | Request | undefined {
const typeChecker = program.getTypeChecker();
let start = timestamp();
let currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853
// 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 {
}
}
// Don't suggest import completions for a commonjs-only module
if (preferences.includeCompletionsForModuleExports && !(sourceFile.commonJsModuleIndicator && !sourceFile.externalModuleIndicator)) {
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", target);
if (shouldOfferImportCompletions()) {
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", program.getCompilerOptions().target);
}
filterGlobalCompletion(symbols);
}
function shouldOfferImportCompletions(): boolean {
// If not already a module, must have modules enabled and not currently be in a commonjs module. (TODO: import completions for commonjs)
if (!preferences.includeCompletionsForModuleExports) return false;
// If already using ES6 modules, OK to continue using them.
if (sourceFile.externalModuleIndicator) return true;
// If already using commonjs, don't introduce ES6.
if (sourceFile.commonJsModuleIndicator) return false;
// If some file is using ES6 modules, assume that it's OK to add more.
if (program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator)) {
return true;
}
// For JS, stay on the safe side.
if (isSourceFileJavaScript(sourceFile)) return false;
// If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK.
const compilerOptions = program.getCompilerOptions();
return !!compilerOptions.module || compilerOptions.target >= ScriptTarget.ES2015 || !!compilerOptions.noEmit;
}
function isSnippetScope(scopeNode: Node): boolean {
switch (scopeNode.kind) {
case SyntaxKind.SourceFile:
@@ -1268,7 +1262,7 @@ namespace ts.Completions {
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
const tokenTextLowerCase = tokenText.toLowerCase();
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, allSourceFiles, moduleSymbol => {
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
// Don't add a completion for a re-export, only for the original.
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
+2 -13
View File
@@ -1404,12 +1404,10 @@ namespace ts {
synchronizeHostData();
return Completions.getCompletionsAtPosition(
host,
program.getTypeChecker(),
program,
log,
program.getCompilerOptions(),
getValidSourceFile(fileName),
position,
program.getSourceFiles(),
fullPreferences);
}
@@ -1418,11 +1416,9 @@ namespace ts {
return Completions.getCompletionEntryDetails(
program,
log,
program.getCompilerOptions(),
getValidSourceFile(fileName),
position,
{ name, source },
program.getSourceFiles(),
host,
formattingOptions && formatting.getFormatContext(formattingOptions),
getCanonicalFileName,
@@ -1431,14 +1427,7 @@ namespace ts {
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol {
synchronizeHostData();
return Completions.getCompletionEntrySymbol(
program.getTypeChecker(),
log,
program.getCompilerOptions(),
getValidSourceFile(fileName),
position,
{ name, source },
program.getSourceFiles());
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source });
}
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {
@@ -3,7 +3,8 @@
// @Filename: /tsconfig.json
////{
//// "compilerOptions": {
//// "baseUrl": "."
//// "baseUrl": ".",
//// "module": "esnext"
//// }
////}
@@ -16,6 +17,6 @@
// Test that it prefers a relative import (see sourceDisplay).
goTo.marker("");
verify.completionListContains({ name: "foo", source: "/src/a" }, "const foo: 0", "", "const", undefined, /*hasAction*/ true, {
includeExternalModuleExports: true,
includeCompletionsForModuleExports: true,
sourceDisplay: "./a",
});
@@ -0,0 +1,40 @@
/// <reference path="fourslash.ts" />
// @allowJs: true
// @module: commonjs
// @Filename: /node_modules/a/index.d.ts
////export const foo = 0;
// @Filename: /b.js
////const a = require("./a");
////fo/*b*/
// @Filename: /c.js
////const x = 0;/*c*/ // Off for JS files (unless a non-declaration external module exists in the project)
// @Filename: /c2.ts
////const x = 0;/*c2*/
// @Filename: /d.js
////const a = import("./a"); // Does not make this an external module
////fo/*d*/
// @Filename: /d2.ts
////const a = import("./a"); // Does not make this an external module
////fo/*d2*/
for (const marker of ["b", "c", "d"]) {
goTo.marker(marker);
verify.not.completionListContains({ name: "foo", source: "/node_modules/a/index" }, undefined, undefined, undefined, undefined, undefined, {
includeCompletionsForModuleExports: true
});
}
for (const marker of ["c2", "d2"]) {
goTo.marker(marker);
verify.completionListContains({ name: "foo", source: "/node_modules/a/index" }, "const foo: 0", "", "const", /*spanIndex*/ undefined, /*hasAction*/ true, {
includeCompletionsForModuleExports: true,
sourceDisplay: "a",
});
}
@@ -1,6 +1,7 @@
/// <reference path="fourslash.ts" />
// Use `/src` to test that directory names are not included in conversion from module path to identifier.
// @module: esnext
// @Filename: /src/foo-bar.ts
////export default 0;
@@ -1,6 +1,6 @@
/// <reference path="fourslash.ts" />
// @noLib: true
// @module: esnext
// @Filename: /a.ts
////export default function foo() {}
@@ -2,6 +2,8 @@
// Tests that we use the name "foo".
// @module: esnext
// @Filename: /a.ts
////const foo = 0;
////export default foo;
@@ -1,5 +1,7 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @Filename: /a.ts
////declare module "m" {
//// export const x: number;
@@ -1,5 +1,7 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @Filename: /a.ts
// Not included:
////export function abcde() {}
@@ -1,5 +1,7 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @Filename: /global.d.ts
// A local variable would prevent import completions (see `completionsImport_shadowedByLocal.ts`), but a global doesn't.
////declare var foo: number;
@@ -1,5 +1,6 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export function Test1() {}
////export function Test2() {}
@@ -1,5 +1,7 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @Filename: /a.d.ts
////declare namespace N {
//// export const foo = 0;
@@ -1,5 +1,7 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @Filename: /b.d.ts
////declare namespace N {
//// export const foo: number;
@@ -1,5 +1,7 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @Filename: /unrelated/node_modules/@types/foo/index.d.ts
////export function foo() {}
@@ -3,6 +3,8 @@
// Tests that we don't filter out a completion for an alias,
// so long as it's not an alias to a different module.
// @module: esnext
// @Filename: /a.ts
////const foo = 0;
////export { foo };
@@ -1,5 +1,7 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @Filename: /a.ts
////export const foo = 0;
@@ -1,5 +1,8 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @moduleResolution: node
// @Filename: /a/b/impl.ts
////export default function foo() {}
@@ -6,29 +6,37 @@
////export const foo = 0;
// @Filename: /b.js
////const a = require("./a");
////import * as s from "something";
////fo/*b*/
// @Filename: /c.js
////const a = import("./a");
////const a = require("./a");
////fo/*c*/
goTo.marker("b");
// Doesn't activate for commonjs-only module
verify.not.completionListContains({ name: "foo", source: "/a" });
// @Filename: /d.js
////const x = 0;/*d*/
goTo.marker("c");
// @Filename: /e.js
////const a = import("./a"); // Does not make this an external module
////fo/*e*/
for (const marker of ["c", "d", "e"]) {
// Doesn't activate for commonjs-only module, or non-module file unless 'module' is set see also completionsImport_compilerOptionsModule)
verify.not.completionListContains({ name: "foo", source: "/a" });
}
goTo.marker("b");
verify.completionListContains({ name: "foo", source: "/a" }, "const foo: 0", "", "const", /*spanIndex*/ undefined, /*hasAction*/ true, {
includeExternalModuleExports: true,
includeCompletionsForModuleExports: true,
sourceDisplay: "./a",
});
verify.applyCodeActionFromCompletion("c", {
verify.applyCodeActionFromCompletion("b", {
name: "foo",
source: "/a",
description: `Import 'foo' from module "./a"`,
newFileContent: `import { foo } from "./a";
const a = import("./a");
newFileContent:
`import * as s from "something";
import { foo } from "./a";
fo`,
});