Support completions for unique symbol exported from module (#25537)

This commit is contained in:
Andy 2018-07-10 10:47:43 -07:00 committed by GitHub
parent 4bf42fd1c4
commit 72be7156d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 42 additions and 17 deletions

View File

@ -2,12 +2,20 @@
namespace ts.Completions {
export type Log = (message: string) => void;
type SymbolOriginInfo = { type: "this-type" } | { type: "symbol-member" } | SymbolOriginInfoExport;
const enum SymbolOriginInfoKind { ThisType, SymbolMemberNoExport, SymbolMemberExport, Export }
type SymbolOriginInfo = { kind: SymbolOriginInfoKind.ThisType } | { kind: SymbolOriginInfoKind.SymbolMemberNoExport } | SymbolOriginInfoExport;
interface SymbolOriginInfoExport {
type: "export";
kind: SymbolOriginInfoKind.SymbolMemberExport | SymbolOriginInfoKind.Export;
moduleSymbol: Symbol;
isDefaultExport: boolean;
}
function originIsSymbolMember(origin: SymbolOriginInfo): boolean {
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.SymbolMemberNoExport;
}
function originIsExport(origin: SymbolOriginInfo): origin is SymbolOriginInfoExport {
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.Export;
}
/**
* Map from symbol id -> SymbolOriginInfo.
* Only populated for symbols that come from other modules.
@ -214,12 +222,12 @@ namespace ts.Completions {
let insertText: string | undefined;
let replacementSpan: TextSpan | undefined;
if (origin && origin.type === "this-type") {
if (origin && origin.kind === SymbolOriginInfoKind.ThisType) {
insertText = needsConvertPropertyAccess ? `this[${quote(name, preferences)}]` : `this.${name}`;
}
// We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790.
// Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro.
else if ((origin && origin.type === "symbol-member" || needsConvertPropertyAccess) && propertyAccessToConvert) {
else if ((origin && originIsSymbolMember(origin) || needsConvertPropertyAccess) && propertyAccessToConvert) {
insertText = needsConvertPropertyAccess ? `[${quote(name, preferences)}]` : `[${name}]`;
const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!;
// If the text after the '.' starts with this name, write over it. Else, add new text.
@ -253,7 +261,7 @@ namespace ts.Completions {
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
sortText: "0",
source: getSourceFromOrigin(origin),
hasAction: trueOrUndefined(!!origin && origin.type === "export"),
hasAction: trueOrUndefined(!!origin && originIsExport(origin)),
isRecommended: trueOrUndefined(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)),
insertText,
replacementSpan,
@ -283,7 +291,7 @@ namespace ts.Completions {
}
function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined {
return origin && origin.type === "export" ? stripQuotes(origin.moduleSymbol.name) : undefined;
return origin && originIsExport(origin) ? stripQuotes(origin.moduleSymbol.name) : undefined;
}
function getCompletionEntriesFromSymbols(
@ -529,7 +537,7 @@ namespace ts.Completions {
}
function getSymbolName(symbol: Symbol, origin: SymbolOriginInfo | undefined, target: ScriptTarget): string {
return origin && origin.type === "export" && origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default
return origin && originIsExport(origin) && origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
? firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined)
|| codefix.moduleSymbolToValidIdentifier(origin.moduleSymbol, target)
@ -648,7 +656,7 @@ namespace ts.Completions {
preferences: UserPreferences,
): CodeActionsAndSourceDisplay {
const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)];
if (!symbolOriginInfo || symbolOriginInfo.type !== "export") {
if (!symbolOriginInfo || !originIsExport(symbolOriginInfo)) {
return { codeActions: undefined, sourceDisplay: undefined };
}
@ -1124,7 +1132,9 @@ namespace ts.Completions {
const firstAccessibleSymbol = nameSymbol && getFirstSymbolInChain(nameSymbol, contextToken, typeChecker);
if (firstAccessibleSymbol && !symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)]) {
symbols.push(firstAccessibleSymbol);
symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = { type: "symbol-member" };
const moduleSymbol = firstAccessibleSymbol.parent;
symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] =
!moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false };
}
}
else {
@ -1222,7 +1232,7 @@ namespace ts.Completions {
const thisType = typeChecker.tryGetThisTypeAt(scopeNode);
if (thisType) {
for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) {
symbolToOriginInfoMap[getSymbolId(symbol)] = { type: "this-type" };
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType };
symbols.push(symbol);
}
}
@ -1374,7 +1384,7 @@ namespace ts.Completions {
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
}
const origin: SymbolOriginInfo = { type: "export", moduleSymbol, isDefaultExport };
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport };
if (detailsEntryId || stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
symbols.push(symbol);
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;

View File

@ -704,7 +704,7 @@ namespace ts.FindAllReferences.Core {
- But if the parent has `export as namespace`, the symbol is globally visible through that namespace.
*/
const exposedByParent = parent && !(symbol.flags & SymbolFlags.TypeParameter);
if (exposedByParent && !((parent!.flags & SymbolFlags.Module) && isExternalModuleSymbol(parent!) && !parent!.globalExports)) {
if (exposedByParent && !(isExternalModuleSymbol(parent!) && !parent!.globalExports)) {
return undefined;
}

View File

@ -1184,8 +1184,7 @@ namespace ts {
/** True if the symbol is for an external module, as opposed to a namespace. */
export function isExternalModuleSymbol(moduleSymbol: Symbol): boolean {
Debug.assert(!!(moduleSymbol.flags & SymbolFlags.Module));
return moduleSymbol.name.charCodeAt(0) === CharacterCodes.doubleQuote;
return !!(moduleSymbol.flags & SymbolFlags.Module) && moduleSymbol.name.charCodeAt(0) === CharacterCodes.doubleQuote;
}
/** Returns `true` the first time it encounters a node and `false` afterwards. */

View File

@ -1,12 +1,17 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
// @noLib: true
// @Filename: /globals.d.ts
////declare const Symbol: () => symbol;
// @Filename: /a.ts
////const privateSym = Symbol();
////export const publicSym = Symbol();
////export interface I {
//// [privateSym]: number;
//// [publicSym]: number;
//// [defaultPublicSym]: number;
//// n: number;
////}
////export const i: I;
@ -17,10 +22,21 @@
verify.completions({
marker: "",
// TODO: GH#25095 Should include `publicSym`
exact: "n",
exact: [
"n",
{ name: "publicSym", insertText: "[publicSym]", replacementSpan: test.ranges()[0], hasAction: true },
],
preferences: {
includeInsertTextCompletions: true,
includeCompletionsForModuleExports: true,
},
});
verify.applyCodeActionFromCompletion("", {
name: "publicSym",
source: "/a",
description: `Add 'publicSym' to existing import declaration from "./a"`,
newFileContent:
`import { i, publicSym } from "./a";
i.;`
});