Skip to content

Commit 36607e1

Browse files
author
Andy
authored
Allow quoted names in completions (#18162)
* Allow quoted names in completions * Don't allow string literal completions if not in an object literal; and use string literals for number keys * Add TODO
1 parent afdd9b5 commit 36607e1

9 files changed

+57
-76
lines changed

src/harness/fourslash.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,14 +762,18 @@ namespace FourSlash {
762762
}
763763
}
764764

765-
public verifyCompletionsAt(markerName: string, expected: string[]) {
765+
public verifyCompletionsAt(markerName: string, expected: string[], options?: FourSlashInterface.CompletionsAtOptions) {
766766
this.goToMarker(markerName);
767767

768768
const actualCompletions = this.getCompletionListAtCaret();
769769
if (!actualCompletions) {
770770
this.raiseError(`No completions at position '${this.currentCaretPosition}'.`);
771771
}
772772

773+
if (options && options.isNewIdentifierLocation !== undefined && actualCompletions.isNewIdentifierLocation !== options.isNewIdentifierLocation) {
774+
this.raiseError(`Expected 'isNewIdentifierLocation' to be ${options.isNewIdentifierLocation}, got ${actualCompletions.isNewIdentifierLocation}`);
775+
}
776+
773777
const actual = actualCompletions.entries;
774778

775779
if (actual.length !== expected.length) {
@@ -3705,8 +3709,8 @@ namespace FourSlashInterface {
37053709
super(state);
37063710
}
37073711

3708-
public completionsAt(markerName: string, completions: string[]) {
3709-
this.state.verifyCompletionsAt(markerName, completions);
3712+
public completionsAt(markerName: string, completions: string[], options?: CompletionsAtOptions) {
3713+
this.state.verifyCompletionsAt(markerName, completions, options);
37103714
}
37113715

37123716
public quickInfoIs(expectedText: string, expectedDocumentation?: string) {
@@ -4314,4 +4318,8 @@ namespace FourSlashInterface {
43144318
actionName: string;
43154319
actionDescription: string;
43164320
}
4321+
4322+
export interface CompletionsAtOptions {
4323+
isNewIdentifierLocation?: boolean;
4324+
}
43174325
}

src/services/completions.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace ts.Completions {
2424
return undefined;
2525
}
2626

27-
const { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, request, keywordFilters } = completionData;
27+
const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters } = completionData;
2828

2929
if (sourceFile.languageVariant === LanguageVariant.JSX &&
3030
location && location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) {
@@ -56,15 +56,15 @@ namespace ts.Completions {
5656
const entries: CompletionEntry[] = [];
5757

5858
if (isSourceFileJavaScript(sourceFile)) {
59-
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
59+
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral);
6060
getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target, entries);
6161
}
6262
else {
6363
if ((!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
6464
return undefined;
6565
}
6666

67-
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
67+
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral);
6868
}
6969

7070
// TODO add filter for keyword based on type/value/namespace and also location
@@ -97,7 +97,7 @@ namespace ts.Completions {
9797
}
9898

9999
uniqueNames.set(realName, true);
100-
const displayName = getCompletionEntryDisplayName(realName, target, /*performCharacterChecks*/ true);
100+
const displayName = getCompletionEntryDisplayName(realName, target, /*performCharacterChecks*/ true, /*allowStringLiteral*/ false);
101101
if (displayName) {
102102
entries.push({
103103
name: displayName,
@@ -109,11 +109,11 @@ namespace ts.Completions {
109109
});
110110
}
111111

112-
function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget): CompletionEntry {
112+
function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, allowStringLiteral: boolean): CompletionEntry {
113113
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
114114
// We would like to only show things that can be added after a dot, so for instance numeric properties can
115115
// not be accessed with a dot (a.1 <- invalid)
116-
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, performCharacterChecks);
116+
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, performCharacterChecks, allowStringLiteral);
117117
if (!displayName) {
118118
return undefined;
119119
}
@@ -134,12 +134,12 @@ namespace ts.Completions {
134134
};
135135
}
136136

137-
function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: Push<CompletionEntry>, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, log: Log): Map<true> {
137+
function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: Push<CompletionEntry>, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, log: Log, allowStringLiteral: boolean): Map<true> {
138138
const start = timestamp();
139139
const uniqueNames = createMap<true>();
140140
if (symbols) {
141141
for (const symbol of symbols) {
142-
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target);
142+
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target, allowStringLiteral);
143143
if (entry) {
144144
const id = entry.name;
145145
if (!uniqueNames.has(id)) {
@@ -224,7 +224,7 @@ namespace ts.Completions {
224224
const type = typeChecker.getContextualType((<ObjectLiteralExpression>element.parent));
225225
const entries: CompletionEntry[] = [];
226226
if (type) {
227-
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, /*performCharacterChecks*/ false, typeChecker, target, log);
227+
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, /*performCharacterChecks*/ false, typeChecker, target, log, /*allowStringLiteral*/ true);
228228
if (entries.length) {
229229
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
230230
}
@@ -253,7 +253,7 @@ namespace ts.Completions {
253253
const type = typeChecker.getTypeAtLocation(node.expression);
254254
const entries: CompletionEntry[] = [];
255255
if (type) {
256-
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, node, /*performCharacterChecks*/ false, typeChecker, target, log);
256+
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, node, /*performCharacterChecks*/ false, typeChecker, target, log, /*allowStringLiteral*/ true);
257257
if (entries.length) {
258258
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
259259
}
@@ -302,13 +302,13 @@ namespace ts.Completions {
302302
// Compute all the completion symbols again.
303303
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
304304
if (completionData) {
305-
const { symbols, location } = completionData;
305+
const { symbols, location, allowStringLiteral } = completionData;
306306

307307
// Find the symbol with the matching entry name.
308308
// We don't need to perform character checks here because we're only comparing the
309309
// name against 'entryName' (which is known to be good), not building a new
310310
// completion entry.
311-
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false) === entryName ? s : undefined);
311+
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName ? s : undefined);
312312

313313
if (symbol) {
314314
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
@@ -345,17 +345,22 @@ namespace ts.Completions {
345345
export function getCompletionEntrySymbol(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): Symbol | undefined {
346346
// Compute all the completion symbols again.
347347
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
348+
if (!completionData) {
349+
return undefined;
350+
}
351+
const { symbols, allowStringLiteral } = completionData;
348352
// Find the symbol with the matching entry name.
349353
// We don't need to perform character checks here because we're only comparing the
350354
// name against 'entryName' (which is known to be good), not building a new
351355
// completion entry.
352-
return completionData && forEach(completionData.symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false) === entryName ? s : undefined);
356+
return forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName ? s : undefined);
353357
}
354358

355359
interface CompletionData {
356360
symbols: Symbol[];
357361
isGlobalCompletion: boolean;
358362
isMemberCompletion: boolean;
363+
allowStringLiteral: boolean;
359364
isNewIdentifierLocation: boolean;
360365
location: Node;
361366
isRightOfDot: boolean;
@@ -436,7 +441,7 @@ namespace ts.Completions {
436441
}
437442

438443
if (request) {
439-
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, keywordFilters: KeywordCompletionFilters.None };
444+
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, allowStringLiteral: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, keywordFilters: KeywordCompletionFilters.None };
440445
}
441446

442447
if (!insideJsDocTagTypeExpression) {
@@ -534,6 +539,7 @@ namespace ts.Completions {
534539
const semanticStart = timestamp();
535540
let isGlobalCompletion = false;
536541
let isMemberCompletion: boolean;
542+
let allowStringLiteral = false;
537543
let isNewIdentifierLocation: boolean;
538544
let keywordFilters = KeywordCompletionFilters.None;
539545
let symbols: Symbol[] = [];
@@ -573,7 +579,7 @@ namespace ts.Completions {
573579

574580
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
575581

576-
return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters };
582+
return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters };
577583

578584
type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
579585

@@ -961,6 +967,7 @@ namespace ts.Completions {
961967
function tryGetObjectLikeCompletionSymbols(objectLikeContainer: ObjectLiteralExpression | ObjectBindingPattern): boolean {
962968
// We're looking up possible property names from contextual/inferred/declared type.
963969
isMemberCompletion = true;
970+
allowStringLiteral = true;
964971

965972
let typeMembers: Symbol[];
966973
let existingMembers: ReadonlyArray<Declaration>;
@@ -1609,7 +1616,7 @@ namespace ts.Completions {
16091616
*
16101617
* @return undefined if the name is of external module
16111618
*/
1612-
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean): string | undefined {
1619+
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, allowStringLiteral: boolean): string | undefined {
16131620
const name = symbol.name;
16141621
if (!name) return undefined;
16151622

@@ -1623,20 +1630,21 @@ namespace ts.Completions {
16231630
}
16241631
}
16251632

1626-
return getCompletionEntryDisplayName(name, target, performCharacterChecks);
1633+
return getCompletionEntryDisplayName(name, target, performCharacterChecks, allowStringLiteral);
16271634
}
16281635

16291636
/**
16301637
* Get a displayName from a given for completion list, performing any necessary quotes stripping
16311638
* and checking whether the name is valid identifier name.
16321639
*/
1633-
function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean): string {
1640+
function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean, allowStringLiteral: boolean): string {
16341641
// If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an
16351642
// invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name.
16361643
// e.g "b a" is valid quoted name but when we strip off the quotes, it is invalid.
16371644
// We, thus, need to check if whatever was inside the quotes is actually a valid identifier name.
16381645
if (performCharacterChecks && !isIdentifierText(name, target)) {
1639-
return undefined;
1646+
// TODO: GH#18169
1647+
return allowStringLiteral ? JSON.stringify(name) : undefined;
16401648
}
16411649

16421650
return name;

tests/cases/fourslash/completionForQuotedPropertyInPropertyAssignment1.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,5 @@
1313
//// '/*1*/': ''
1414
//// }
1515

16-
goTo.marker('0');
17-
verify.completionListContains("jspm");
18-
verify.completionListAllowsNewIdentifier();
19-
verify.completionListCount(1);
20-
21-
goTo.marker('1');
22-
verify.completionListContains("jspm:dev");
23-
verify.completionListAllowsNewIdentifier();
24-
verify.completionListCount(4);
16+
verify.completionsAt("0", ["jspm", '"jspm:browser"', '"jspm:dev"', '"jspm:node"'], { isNewIdentifierLocation: true });
17+
verify.completionsAt("1", ["jspm", "jspm:browser", "jspm:dev", "jspm:node"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionForQuotedPropertyInPropertyAssignment2.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,5 @@
1919
//// }
2020
//// }
2121

22-
goTo.marker('0');
23-
verify.completionListContains("jspm");
24-
verify.completionListAllowsNewIdentifier();
25-
verify.completionListCount(1);
26-
27-
goTo.marker('1');
28-
verify.completionListContains("jspm:dev");
29-
verify.completionListAllowsNewIdentifier();
30-
verify.completionListCount(4);
22+
verify.completionsAt("0", ["jspm", '"jspm:browser"', '"jspm:dev"', '"jspm:node"'], { isNewIdentifierLocation: true });
23+
verify.completionsAt("1", ["jspm", "jspm:browser", "jspm:dev", "jspm:node"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionForQuotedPropertyInPropertyAssignment3.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,16 @@
44
//// jspm: string;
55
//// 'jspm:browser': string;
66
//// } = {
7-
//// /*0*/: "",
7+
//// /*0*/: "",
88
//// }
99

1010
//// let configFiles2: {
1111
//// jspm: string;
1212
//// 'jspm:browser': string;
1313
//// } = {
1414
//// jspm: "",
15-
//// '/*1*/': ""
15+
//// '/*1*/': ""
1616
//// }
1717

18-
goTo.marker('0');
19-
verify.completionListContains("jspm");
20-
verify.completionListAllowsNewIdentifier();
21-
verify.completionListCount(1);
22-
23-
goTo.marker('1');
24-
verify.completionListContains("jspm:browser");
25-
verify.completionListAllowsNewIdentifier();
26-
verify.completionListCount(2);
18+
verify.completionsAt("0", ["jspm", '"jspm:browser"'], { isNewIdentifierLocation: true });
19+
verify.completionsAt("1", ["jspm", "jspm:browser"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionListInvalidMemberNames.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,8 @@
1111
//// "\u0031\u0062": "invalid unicode identifer name (1b)"
1212
////};
1313
////
14-
////x./**/
14+
////x./*a*/;
15+
////x["/*b*/"];
1516

16-
goTo.marker();
17-
18-
verify.completionListContains("bar");
19-
verify.completionListContains("break");
20-
verify.completionListContains("any");
21-
verify.completionListContains("$");
22-
verify.completionListContains("b");
23-
24-
// Nothing else should show up
25-
verify.completionListCount(5);
17+
verify.completionsAt("a", ["bar", "break", "any", "$", "b"]);
18+
verify.completionsAt("b", ["foo ", "bar", "break", "any", "#", "$", "b", "\u0031\u0062"]);

tests/cases/fourslash/completionListInvalidMemberNames2.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
////enum Foo {
44
//// X, Y, '☆'
55
////}
6-
////var x = Foo./**/
6+
////Foo./*a*/;
7+
////Foo["/*b*/"];
78

8-
goTo.marker();
9-
verify.completionListContains("X");
10-
verify.completionListContains("Y");
11-
verify.completionListCount(2);
9+
verify.completionsAt("a", ["X", "Y"]);
10+
verify.completionsAt("b", ["X", "Y", "☆"]);

tests/cases/fourslash/completion_enum-members-with-invalid-identifiers-should-not-show-in-completion.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@
77
//// a,
88
//// b
99
//// }
10-
////
10+
////
1111
//// e./**/
1212

13-
goTo.marker();
14-
verify.not.completionListContains('1');
15-
verify.not.completionListContains('"1"');
16-
verify.not.completionListContains('2');
17-
verify.not.completionListContains('3');
18-
verify.completionListContains('a');
19-
verify.completionListContains('b');
13+
verify.completionsAt("", ["a", "b"]);

tests/cases/fourslash/fourslash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ declare namespace FourSlashInterface {
164164
class verify extends verifyNegatable {
165165
assertHasRanges(ranges: Range[]): void;
166166
caretAtMarker(markerName?: string): void;
167-
completionsAt(markerName: string, completions: string[]): void;
167+
completionsAt(markerName: string, completions: string[], options?: { isNewIdentifierLocation?: boolean }): void;
168168
indentationIs(numberOfSpaces: number): void;
169169
indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle?: ts.IndentStyle, baseIndentSize?: number): void;
170170
textAtCaretIs(text: string): void;

0 commit comments

Comments
 (0)