Skip to content

Commit 72be715

Browse files
author
Andy
authored
Support completions for unique symbol exported from module (#25537)
1 parent 4bf42fd commit 72be715

File tree

4 files changed

+42
-17
lines changed

4 files changed

+42
-17
lines changed

src/services/completions.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22
namespace ts.Completions {
33
export type Log = (message: string) => void;
44

5-
type SymbolOriginInfo = { type: "this-type" } | { type: "symbol-member" } | SymbolOriginInfoExport;
5+
const enum SymbolOriginInfoKind { ThisType, SymbolMemberNoExport, SymbolMemberExport, Export }
6+
type SymbolOriginInfo = { kind: SymbolOriginInfoKind.ThisType } | { kind: SymbolOriginInfoKind.SymbolMemberNoExport } | SymbolOriginInfoExport;
67
interface SymbolOriginInfoExport {
7-
type: "export";
8+
kind: SymbolOriginInfoKind.SymbolMemberExport | SymbolOriginInfoKind.Export;
89
moduleSymbol: Symbol;
910
isDefaultExport: boolean;
1011
}
12+
function originIsSymbolMember(origin: SymbolOriginInfo): boolean {
13+
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.SymbolMemberNoExport;
14+
}
15+
function originIsExport(origin: SymbolOriginInfo): origin is SymbolOriginInfoExport {
16+
return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.Export;
17+
}
18+
1119
/**
1220
* Map from symbol id -> SymbolOriginInfo.
1321
* Only populated for symbols that come from other modules.
@@ -214,12 +222,12 @@ namespace ts.Completions {
214222

215223
let insertText: string | undefined;
216224
let replacementSpan: TextSpan | undefined;
217-
if (origin && origin.type === "this-type") {
225+
if (origin && origin.kind === SymbolOriginInfoKind.ThisType) {
218226
insertText = needsConvertPropertyAccess ? `this[${quote(name, preferences)}]` : `this.${name}`;
219227
}
220228
// We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790.
221229
// 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.
222-
else if ((origin && origin.type === "symbol-member" || needsConvertPropertyAccess) && propertyAccessToConvert) {
230+
else if ((origin && originIsSymbolMember(origin) || needsConvertPropertyAccess) && propertyAccessToConvert) {
223231
insertText = needsConvertPropertyAccess ? `[${quote(name, preferences)}]` : `[${name}]`;
224232
const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!;
225233
// If the text after the '.' starts with this name, write over it. Else, add new text.
@@ -253,7 +261,7 @@ namespace ts.Completions {
253261
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
254262
sortText: "0",
255263
source: getSourceFromOrigin(origin),
256-
hasAction: trueOrUndefined(!!origin && origin.type === "export"),
264+
hasAction: trueOrUndefined(!!origin && originIsExport(origin)),
257265
isRecommended: trueOrUndefined(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)),
258266
insertText,
259267
replacementSpan,
@@ -283,7 +291,7 @@ namespace ts.Completions {
283291
}
284292

285293
function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined {
286-
return origin && origin.type === "export" ? stripQuotes(origin.moduleSymbol.name) : undefined;
294+
return origin && originIsExport(origin) ? stripQuotes(origin.moduleSymbol.name) : undefined;
287295
}
288296

289297
function getCompletionEntriesFromSymbols(
@@ -529,7 +537,7 @@ namespace ts.Completions {
529537
}
530538

531539
function getSymbolName(symbol: Symbol, origin: SymbolOriginInfo | undefined, target: ScriptTarget): string {
532-
return origin && origin.type === "export" && origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default
540+
return origin && originIsExport(origin) && origin.isDefaultExport && symbol.escapedName === InternalSymbolName.Default
533541
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
534542
? firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined)
535543
|| codefix.moduleSymbolToValidIdentifier(origin.moduleSymbol, target)
@@ -648,7 +656,7 @@ namespace ts.Completions {
648656
preferences: UserPreferences,
649657
): CodeActionsAndSourceDisplay {
650658
const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)];
651-
if (!symbolOriginInfo || symbolOriginInfo.type !== "export") {
659+
if (!symbolOriginInfo || !originIsExport(symbolOriginInfo)) {
652660
return { codeActions: undefined, sourceDisplay: undefined };
653661
}
654662

@@ -1124,7 +1132,9 @@ namespace ts.Completions {
11241132
const firstAccessibleSymbol = nameSymbol && getFirstSymbolInChain(nameSymbol, contextToken, typeChecker);
11251133
if (firstAccessibleSymbol && !symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)]) {
11261134
symbols.push(firstAccessibleSymbol);
1127-
symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = { type: "symbol-member" };
1135+
const moduleSymbol = firstAccessibleSymbol.parent;
1136+
symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] =
1137+
!moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false };
11281138
}
11291139
}
11301140
else {
@@ -1222,7 +1232,7 @@ namespace ts.Completions {
12221232
const thisType = typeChecker.tryGetThisTypeAt(scopeNode);
12231233
if (thisType) {
12241234
for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) {
1225-
symbolToOriginInfoMap[getSymbolId(symbol)] = { type: "this-type" };
1235+
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType };
12261236
symbols.push(symbol);
12271237
}
12281238
}
@@ -1374,7 +1384,7 @@ namespace ts.Completions {
13741384
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
13751385
}
13761386

1377-
const origin: SymbolOriginInfo = { type: "export", moduleSymbol, isDefaultExport };
1387+
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport };
13781388
if (detailsEntryId || stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
13791389
symbols.push(symbol);
13801390
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;

src/services/findAllReferences.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ namespace ts.FindAllReferences.Core {
704704
- But if the parent has `export as namespace`, the symbol is globally visible through that namespace.
705705
*/
706706
const exposedByParent = parent && !(symbol.flags & SymbolFlags.TypeParameter);
707-
if (exposedByParent && !((parent!.flags & SymbolFlags.Module) && isExternalModuleSymbol(parent!) && !parent!.globalExports)) {
707+
if (exposedByParent && !(isExternalModuleSymbol(parent!) && !parent!.globalExports)) {
708708
return undefined;
709709
}
710710

src/services/utilities.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,8 +1184,7 @@ namespace ts {
11841184

11851185
/** True if the symbol is for an external module, as opposed to a namespace. */
11861186
export function isExternalModuleSymbol(moduleSymbol: Symbol): boolean {
1187-
Debug.assert(!!(moduleSymbol.flags & SymbolFlags.Module));
1188-
return moduleSymbol.name.charCodeAt(0) === CharacterCodes.doubleQuote;
1187+
return !!(moduleSymbol.flags & SymbolFlags.Module) && moduleSymbol.name.charCodeAt(0) === CharacterCodes.doubleQuote;
11891188
}
11901189

11911190
/** Returns `true` the first time it encounters a node and `false` afterwards. */
Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/// <reference path="fourslash.ts" />
22

3-
// @Filename: /a.ts
3+
// @noLib: true
4+
5+
// @Filename: /globals.d.ts
46
////declare const Symbol: () => symbol;
7+
8+
// @Filename: /a.ts
59
////const privateSym = Symbol();
610
////export const publicSym = Symbol();
711
////export interface I {
812
//// [privateSym]: number;
913
//// [publicSym]: number;
14+
//// [defaultPublicSym]: number;
1015
//// n: number;
1116
////}
1217
////export const i: I;
@@ -17,10 +22,21 @@
1722

1823
verify.completions({
1924
marker: "",
20-
// TODO: GH#25095 Should include `publicSym`
21-
exact: "n",
25+
exact: [
26+
"n",
27+
{ name: "publicSym", insertText: "[publicSym]", replacementSpan: test.ranges()[0], hasAction: true },
28+
],
2229
preferences: {
2330
includeInsertTextCompletions: true,
2431
includeCompletionsForModuleExports: true,
2532
},
2633
});
34+
35+
verify.applyCodeActionFromCompletion("", {
36+
name: "publicSym",
37+
source: "/a",
38+
description: `Add 'publicSym' to existing import declaration from "./a"`,
39+
newFileContent:
40+
`import { i, publicSym } from "./a";
41+
i.;`
42+
});

0 commit comments

Comments
 (0)