Skip to content

Commit e72d0d8

Browse files
committed
Merge pull request #3643 from Microsoft/completionListWithLocalName
Completion list show correct entry for function expression and class expression
2 parents 505a854 + f16f9d1 commit e72d0d8

14 files changed

+264
-62
lines changed

src/compiler/checker.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13414,18 +13414,25 @@ namespace ts {
1341413414
copySymbols(getSymbolOfNode(location).exports, meaning & SymbolFlags.EnumMember);
1341513415
break;
1341613416
case SyntaxKind.ClassExpression:
13417-
if ((<ClassExpression>location).name) {
13417+
let className = (<ClassExpression>location).name;
13418+
if (className) {
1341813419
copySymbol(location.symbol, meaning);
1341913420
}
13420-
// Fall through
13421+
// fall through; this fall-through is necessary because we would like to handle
13422+
// type parameter inside class expression similar to how we handle it in classDeclaration and interface Declaration
1342113423
case SyntaxKind.ClassDeclaration:
1342213424
case SyntaxKind.InterfaceDeclaration:
13425+
// If we didn't come from static member of class or interface,
13426+
// add the type parameters into the symbol table
13427+
// (type parameters of classDeclaration/classExpression and interface are in member property of the symbol.
13428+
// Note: that the memberFlags come from previous iteration.
1342313429
if (!(memberFlags & NodeFlags.Static)) {
1342413430
copySymbols(getSymbolOfNode(location).members, meaning & SymbolFlags.Type);
1342513431
}
1342613432
break;
1342713433
case SyntaxKind.FunctionExpression:
13428-
if ((<FunctionExpression>location).name) {
13434+
let funcName = (<FunctionExpression>location).name;
13435+
if (funcName) {
1342913436
copySymbol(location.symbol, meaning);
1343013437
}
1343113438
break;
@@ -13438,11 +13445,20 @@ namespace ts {
1343813445
copySymbols(globals, meaning);
1343913446
}
1344013447

13441-
// Returns 'true' if we should stop processing symbols.
13448+
/**
13449+
* Copy the given symbol into symbol tables if the symbol has the given meaning
13450+
* and it doesn't already existed in the symbol table
13451+
* @param key a key for storing in symbol table; if undefined, use symbol.name
13452+
* @param symbol the symbol to be added into symbol table
13453+
* @param meaning meaning of symbol to filter by before adding to symbol table
13454+
*/
1344213455
function copySymbol(symbol: Symbol, meaning: SymbolFlags): void {
1344313456
if (symbol.flags & meaning) {
1344413457
let id = symbol.name;
13445-
if (!isReservedMemberName(id) && !hasProperty(symbols, id)) {
13458+
// We will copy all symbol regardless of its reserved name because
13459+
// symbolsToArray will check whether the key is a reserved name and
13460+
// it will not copy symbol with reserved name to the array
13461+
if (!hasProperty(symbols, id)) {
1344613462
symbols[id] = symbol;
1344713463
}
1344813464
}
@@ -13451,9 +13467,8 @@ namespace ts {
1345113467
function copySymbols(source: SymbolTable, meaning: SymbolFlags): void {
1345213468
if (meaning) {
1345313469
for (let id in source) {
13454-
if (hasProperty(source, id)) {
13455-
copySymbol(source[id], meaning);
13456-
}
13470+
let symbol = source[id];
13471+
copySymbol(symbol, meaning);
1345713472
}
1345813473
}
1345913474
}

src/compiler/utilities.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ namespace ts {
1616

1717
export function getDeclarationOfKind(symbol: Symbol, kind: SyntaxKind): Declaration {
1818
let declarations = symbol.declarations;
19-
for (let declaration of declarations) {
20-
if (declaration.kind === kind) {
21-
return declaration;
19+
if (declarations) {
20+
for (let declaration of declarations) {
21+
if (declaration.kind === kind) {
22+
return declaration;
23+
}
2224
}
2325
}
2426

src/harness/fourslash.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -704,13 +704,61 @@ module FourSlash {
704704
}
705705
}
706706

707-
public verifyCompletionListDoesNotContain(symbol: string) {
707+
/**
708+
* Verify that the completion list does NOT contain the given symbol.
709+
* The symbol is considered matched with the symbol in the list if and only if all given parameters must matched.
710+
* When any parameter is omitted, the parameter is ignored during comparison and assumed that the parameter with
711+
* that property of the symbol in the list.
712+
* @param symbol the name of symbol
713+
* @param expectedText the text associated with the symbol
714+
* @param expectedDocumentation the documentation text associated with the symbol
715+
* @param expectedKind the kind of symbol (see ScriptElementKind)
716+
*/
717+
public verifyCompletionListDoesNotContain(symbol: string, expectedText?: string, expectedDocumentation?: string, expectedKind?: string) {
718+
let that = this;
719+
function filterByTextOrDocumentation(entry: ts.CompletionEntry) {
720+
let details = that.getCompletionEntryDetails(entry.name);
721+
let documentation = ts.displayPartsToString(details.documentation);
722+
let text = ts.displayPartsToString(details.displayParts);
723+
if (expectedText && expectedDocumentation) {
724+
return (documentation === expectedDocumentation && text === expectedText) ? true : false;
725+
}
726+
else if (expectedText && !expectedDocumentation) {
727+
return text === expectedText ? true : false;
728+
}
729+
else if (expectedDocumentation && !expectedText) {
730+
return documentation === expectedDocumentation ? true : false;
731+
}
732+
// Because expectedText and expectedDocumentation are undefined, we assume that
733+
// users don't care to compare them so we will treat that entry as if the entry has matching text and documentation
734+
// and keep it in the list of filtered entry.
735+
return true;
736+
}
708737
this.scenarioActions.push('<ShowCompletionList />');
709738
this.scenarioActions.push('<VerifyCompletionDoesNotContainItem ItemName="' + escapeXmlAttributeValue(symbol) + '" />');
710739

711740
var completions = this.getCompletionListAtCaret();
712-
if (completions && completions.entries.filter(e => e.name === symbol).length !== 0) {
713-
this.raiseError('Completion list did contain ' + symbol);
741+
if (completions) {
742+
let filterCompletions = completions.entries.filter(e => e.name === symbol);
743+
filterCompletions = expectedKind ? filterCompletions.filter(e => e.kind === expectedKind) : filterCompletions;
744+
filterCompletions = filterCompletions.filter(filterByTextOrDocumentation);
745+
if (filterCompletions.length !== 0) {
746+
// After filtered using all present criterion, if there are still symbol left in the list
747+
// then these symbols must meet the criterion for Not supposed to be in the list. So we
748+
// raise an error
749+
let error = "Completion list did contain \'" + symbol + "\'.";
750+
let details = this.getCompletionEntryDetails(filterCompletions[0].name);
751+
if (expectedText) {
752+
error += "Expected text: " + expectedText + " to equal: " + ts.displayPartsToString(details.displayParts) + ".";
753+
}
754+
if (expectedDocumentation) {
755+
error += "Expected documentation: " + expectedDocumentation + " to equal: " + ts.displayPartsToString(details.documentation) + ".";
756+
}
757+
if (expectedKind) {
758+
error += "Expected kind: " + expectedKind + " to equal: " + filterCompletions[0].kind + "."
759+
}
760+
this.raiseError(error);
761+
}
714762
}
715763
}
716764

@@ -2167,7 +2215,7 @@ module FourSlash {
21672215

21682216
var itemsString = items.map((item) => JSON.stringify({ name: item.name, kind: item.kind })).join(",\n");
21692217

2170-
this.raiseError('Expected "' + JSON.stringify({ name: name, text: text, documentation: documentation, kind: kind }) + '" to be in list [' + itemsString + ']');
2218+
this.raiseError('Expected "' + JSON.stringify({ name, text, documentation, kind }) + '" to be in list [' + itemsString + ']');
21712219
}
21722220

21732221
private findFile(indexOrName: any) {

src/services/services.ts

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,9 @@ namespace ts {
14281428
// class X {}
14291429
export const classElement = "class";
14301430

1431+
// var x = class X {}
1432+
export const localClassElement = "local class";
1433+
14311434
// interface Y {}
14321435
export const interfaceElement = "interface";
14331436

@@ -2799,18 +2802,15 @@ namespace ts {
27992802
program.getGlobalDiagnostics(cancellationToken));
28002803
}
28012804

2802-
/// Completion
2803-
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean): string {
2804-
let displayName = symbol.getName();
2805-
if (displayName) {
2806-
// If this is the default export, get the name of the declaration if it exists
2807-
if (displayName === "default") {
2808-
let localSymbol = getLocalSymbolForExportDefault(symbol);
2809-
if (localSymbol && localSymbol.name) {
2810-
displayName = symbol.valueDeclaration.localSymbol.name;
2811-
}
2812-
}
2805+
/**
2806+
* Get the name to be display in completion from a given symbol.
2807+
*
2808+
* @return undefined if the name is of external module otherwise a name with striped of any quote
2809+
*/
2810+
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, location: Node): string {
2811+
let displayName: string = getDeclaredName(program.getTypeChecker(), symbol, location);
28132812

2813+
if (displayName) {
28142814
let firstCharCode = displayName.charCodeAt(0);
28152815
// First check of the displayName is not external module; if it is an external module, it is not valid entry
28162816
if ((symbol.flags & SymbolFlags.Namespace) && (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote)) {
@@ -2823,37 +2823,38 @@ namespace ts {
28232823
return getCompletionEntryDisplayName(displayName, target, performCharacterChecks);
28242824
}
28252825

2826-
function getCompletionEntryDisplayName(displayName: string, target: ScriptTarget, performCharacterChecks: boolean): string {
2827-
if (!displayName) {
2826+
/**
2827+
* Get a displayName from a given for completion list, performing any necessary quotes stripping
2828+
* and checking whether the name is valid identifier name.
2829+
*/
2830+
function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean): string {
2831+
if (!name) {
28282832
return undefined;
28292833
}
28302834

2831-
let firstCharCode = displayName.charCodeAt(0);
2832-
if (displayName.length >= 2 &&
2833-
firstCharCode === displayName.charCodeAt(displayName.length - 1) &&
2834-
(firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote)) {
2835-
// If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an
2836-
// invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name.
2837-
displayName = displayName.substring(1, displayName.length - 1);
2838-
}
2835+
name = stripQuotes(name);
28392836

2840-
if (!displayName) {
2837+
if (!name) {
28412838
return undefined;
28422839
}
28432840

2841+
// If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an
2842+
// invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name.
2843+
// e.g "b a" is valid quoted name but when we strip off the quotes, it is invalid.
2844+
// We, thus, need to check if whatever was inside the quotes is actually a valid identifier name.
28442845
if (performCharacterChecks) {
2845-
if (!isIdentifierStart(displayName.charCodeAt(0), target)) {
2846+
if (!isIdentifierStart(name.charCodeAt(0), target)) {
28462847
return undefined;
28472848
}
28482849

2849-
for (let i = 1, n = displayName.length; i < n; i++) {
2850-
if (!isIdentifierPart(displayName.charCodeAt(i), target)) {
2850+
for (let i = 1, n = name.length; i < n; i++) {
2851+
if (!isIdentifierPart(name.charCodeAt(i), target)) {
28512852
return undefined;
28522853
}
28532854
}
28542855
}
28552856

2856-
return unescapeIdentifier(displayName);
2857+
return name;
28572858
}
28582859

28592860
function getCompletionData(fileName: string, position: number) {
@@ -3607,7 +3608,7 @@ namespace ts {
36073608
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
36083609
// We would like to only show things that can be added after a dot, so for instance numeric properties can
36093610
// not be accessed with a dot (a.1 <- invalid)
3610-
let displayName = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, /*performCharacterChecks:*/ true);
3611+
let displayName = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, /*performCharacterChecks:*/ true, location);
36113612
if (!displayName) {
36123613
return undefined;
36133614
}
@@ -3664,7 +3665,7 @@ namespace ts {
36643665
// We don't need to perform character checks here because we're only comparing the
36653666
// name against 'entryName' (which is known to be good), not building a new
36663667
// completion entry.
3667-
let symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, target, /*performCharacterChecks:*/ false) === entryName ? s : undefined);
3668+
let symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, target, /*performCharacterChecks:*/ false, location) === entryName ? s : undefined);
36683669

36693670
if (symbol) {
36703671
let { displayParts, documentation, symbolKind } = getSymbolDisplayPartsDocumentationAndSymbolKind(symbol, getValidSourceFile(fileName), location, location, SemanticMeaning.All);
@@ -3697,7 +3698,8 @@ namespace ts {
36973698
function getSymbolKind(symbol: Symbol, location: Node): string {
36983699
let flags = symbol.getFlags();
36993700

3700-
if (flags & SymbolFlags.Class) return ScriptElementKind.classElement;
3701+
if (flags & SymbolFlags.Class) return getDeclarationOfKind(symbol, SyntaxKind.ClassExpression) ?
3702+
ScriptElementKind.localClassElement : ScriptElementKind.classElement;
37013703
if (flags & SymbolFlags.Enum) return ScriptElementKind.enumElement;
37023704
if (flags & SymbolFlags.TypeAlias) return ScriptElementKind.typeElement;
37033705
if (flags & SymbolFlags.Interface) return ScriptElementKind.interfaceElement;
@@ -3906,7 +3908,16 @@ namespace ts {
39063908
}
39073909
}
39083910
if (symbolFlags & SymbolFlags.Class && !hasAddedSymbolInfo) {
3909-
displayParts.push(keywordPart(SyntaxKind.ClassKeyword));
3911+
if (getDeclarationOfKind(symbol, SyntaxKind.ClassExpression)) {
3912+
// Special case for class expressions because we would like to indicate that
3913+
// the class name is local to the class body (similar to function expression)
3914+
// (local class) class <className>
3915+
pushTypePart(ScriptElementKind.localClassElement);
3916+
}
3917+
else {
3918+
// Class declaration has name which is not local.
3919+
displayParts.push(keywordPart(SyntaxKind.ClassKeyword));
3920+
}
39103921
displayParts.push(spacePart());
39113922
addFullSymbolName(symbol);
39123923
writeTypeParametersOfSymbol(symbol, sourceFile);
@@ -5112,7 +5123,7 @@ namespace ts {
51125123

51135124
// Get the text to search for.
51145125
// Note: if this is an external module symbol, the name doesn't include quotes.
5115-
let declaredName = getDeclaredName(typeChecker, symbol, node);
5126+
let declaredName = stripQuotes(getDeclaredName(typeChecker, symbol, node));
51165127

51175128
// Try to get the smallest valid scope that we can limit our search to;
51185129
// otherwise we'll need to search globally (i.e. include each file).
@@ -5189,10 +5200,10 @@ namespace ts {
51895200
* a reference to a symbol can occur anywhere.
51905201
*/
51915202
function getSymbolScope(symbol: Symbol): Node {
5192-
// If this is the symbol of a function expression, then named references
5193-
// are limited to its own scope.
5203+
// If this is the symbol of a named function expression or named class expression,
5204+
// then named references are limited to its own scope.
51945205
let valueDeclaration = symbol.valueDeclaration;
5195-
if (valueDeclaration && valueDeclaration.kind === SyntaxKind.FunctionExpression) {
5206+
if (valueDeclaration && (valueDeclaration.kind === SyntaxKind.FunctionExpression || valueDeclaration.kind === SyntaxKind.ClassExpression)) {
51965207
return valueDeclaration;
51975208
}
51985209

@@ -6863,7 +6874,7 @@ namespace ts {
68636874
}
68646875
}
68656876

6866-
let displayName = getDeclaredName(typeChecker, symbol, node);
6877+
let displayName = stripQuotes(getDeclaredName(typeChecker, symbol, node));
68676878
let kind = getSymbolKind(symbol, node);
68686879
if (kind) {
68696880
return {

src/services/utilities.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ namespace ts {
667667

668668
let name = typeChecker.symbolToString(localExportDefaultSymbol || symbol);
669669

670-
return stripQuotes(name);
670+
return name;
671671
}
672672

673673
export function isImportOrExportSpecifierName(location: Node): boolean {
@@ -676,9 +676,16 @@ namespace ts {
676676
(<ImportOrExportSpecifier>location.parent).propertyName === location;
677677
}
678678

679+
/**
680+
* Strip off existed single quotes or double quotes from a given string
681+
*
682+
* @return non-quoted string
683+
*/
679684
export function stripQuotes(name: string) {
680685
let length = name.length;
681-
if (length >= 2 && name.charCodeAt(0) === CharacterCodes.doubleQuote && name.charCodeAt(length - 1) === CharacterCodes.doubleQuote) {
686+
if (length >= 2 &&
687+
name.charCodeAt(0) === name.charCodeAt(length - 1) &&
688+
(name.charCodeAt(0) === CharacterCodes.doubleQuote || name.charCodeAt(0) === CharacterCodes.singleQuote)) {
682689
return name.substring(1, length - 1);
683690
};
684691
return name;

0 commit comments

Comments
 (0)