Skip to content

Commit aa2d100

Browse files
author
Andy
authored
Completion for default export should be '.default' (microsoft#16742)
* Completion for default export should be '.default' * Don't include empty string in name table * getSymbolsInScope() should return local symbols, not exported symbols * Fix bug: getSymbolAtLocation should work for local symbol too
1 parent a94e0c3 commit aa2d100

20 files changed

+200
-158
lines changed

src/compiler/checker.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11875,6 +11875,8 @@ namespace ts {
1187511875
}
1187611876

1187711877
function getTypeOfSymbolAtLocation(symbol: Symbol, location: Node) {
11878+
symbol = symbol.exportSymbol || symbol;
11879+
1187811880
// If we have an identifier or a property access at the given location, if the location is
1187911881
// an dotted name expression, and if the location is not an assignment target, obtain the type
1188011882
// of the expression (which will reflect control flow analysis). If the expression indeed
@@ -22281,11 +22283,6 @@ namespace ts {
2228122283
}
2228222284

2228322285
switch (location.kind) {
22284-
case SyntaxKind.SourceFile:
22285-
if (!isExternalOrCommonJsModule(<SourceFile>location)) {
22286-
break;
22287-
}
22288-
// falls through
2228922286
case SyntaxKind.ModuleDeclaration:
2229022287
copySymbols(getSymbolOfNode(location).exports, meaning & SymbolFlags.ModuleMember);
2229122288
break;
@@ -22337,7 +22334,7 @@ namespace ts {
2233722334
* @param meaning meaning of symbol to filter by before adding to symbol table
2233822335
*/
2233922336
function copySymbol(symbol: Symbol, meaning: SymbolFlags): void {
22340-
if (symbol.flags & meaning) {
22337+
if (getCombinedLocalAndExportSymbolFlags(symbol) & meaning) {
2234122338
const id = symbol.name;
2234222339
// We will copy all symbol regardless of its reserved name because
2234322340
// symbolsToArray will check whether the key is a reserved name and

src/compiler/utilities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3598,6 +3598,11 @@ namespace ts {
35983598
}
35993599
return previous[previous.length - 1];
36003600
}
3601+
3602+
/** See comment on `declareModuleMember` in `binder.ts`. */
3603+
export function getCombinedLocalAndExportSymbolFlags(symbol: Symbol): SymbolFlags {
3604+
return symbol.exportSymbol ? symbol.exportSymbol.flags | symbol.flags : symbol.flags;
3605+
}
36013606
}
36023607

36033608
namespace ts {

src/harness/fourslash.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,22 @@ namespace FourSlash {
949949
this.verifySymbol(symbol, declarationRanges);
950950
}
951951

952+
public symbolsInScope(range: Range): ts.Symbol[] {
953+
const node = this.goToAndGetNode(range);
954+
return this.getChecker().getSymbolsInScope(node, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace);
955+
}
956+
957+
public verifyTypeOfSymbolAtLocation(range: Range, symbol: ts.Symbol, expected: string): void {
958+
const node = this.goToAndGetNode(range);
959+
const checker = this.getChecker();
960+
const type = checker.getTypeOfSymbolAtLocation(symbol, node);
961+
962+
const actual = checker.typeToString(type);
963+
if (actual !== expected) {
964+
this.raiseError(`Expected: '${expected}', actual: '${actual}'`);
965+
}
966+
}
967+
952968
private verifyReferencesAre(expectedReferences: Range[]) {
953969
const actualReferences = this.getReferencesAtCaret() || [];
954970

@@ -3426,6 +3442,10 @@ namespace FourSlashInterface {
34263442
public markerByName(s: string): FourSlash.Marker {
34273443
return this.state.getMarkerByName(s);
34283444
}
3445+
3446+
public symbolsInScope(range: FourSlash.Range): ts.Symbol[] {
3447+
return this.state.symbolsInScope(range);
3448+
}
34293449
}
34303450

34313451
export class GoTo {
@@ -3694,6 +3714,10 @@ namespace FourSlashInterface {
36943714
this.state.verifySymbolAtLocation(startRange, declarationRanges);
36953715
}
36963716

3717+
public typeOfSymbolAtLocation(range: FourSlash.Range, symbol: ts.Symbol, expected: string) {
3718+
this.state.verifyTypeOfSymbolAtLocation(range, symbol, expected);
3719+
}
3720+
36973721
public referencesOf(start: FourSlash.Range, references: FourSlash.Range[]) {
36983722
this.state.verifyReferencesOf(start, references);
36993723
}

src/services/completions.ts

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ namespace ts.Completions {
6161
}
6262
else {
6363
if ((!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
64-
return undefined;
64+
return undefined;
6565
}
6666

6767
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
@@ -112,7 +112,7 @@ namespace ts.Completions {
112112
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
113113
// We would like to only show things that can be added after a dot, so for instance numeric properties can
114114
// not be accessed with a dot (a.1 <- invalid)
115-
const displayName = getCompletionEntryDisplayNameForSymbol(typeChecker, symbol, target, performCharacterChecks, location);
115+
const displayName = getCompletionEntryDisplayNameForSymbol(symbol, target, performCharacterChecks);
116116
if (!displayName) {
117117
return undefined;
118118
}
@@ -307,7 +307,7 @@ namespace ts.Completions {
307307
// We don't need to perform character checks here because we're only comparing the
308308
// name against 'entryName' (which is known to be good), not building a new
309309
// completion entry.
310-
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);
310+
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false) === entryName ? s : undefined);
311311

312312
if (symbol) {
313313
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
@@ -341,20 +341,14 @@ namespace ts.Completions {
341341
return undefined;
342342
}
343343

344-
export function getCompletionEntrySymbol(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): Symbol {
344+
export function getCompletionEntrySymbol(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): Symbol | undefined {
345345
// Compute all the completion symbols again.
346346
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
347-
if (completionData) {
348-
const { symbols, location } = completionData;
349-
350-
// Find the symbol with the matching entry name.
351-
// We don't need to perform character checks here because we're only comparing the
352-
// name against 'entryName' (which is known to be good), not building a new
353-
// completion entry.
354-
return forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);
355-
}
356-
357-
return undefined;
347+
// Find the symbol with the matching entry name.
348+
// We don't need to perform character checks here because we're only comparing the
349+
// name against 'entryName' (which is known to be good), not building a new
350+
// completion entry.
351+
return completionData && forEach(completionData.symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false) === entryName ? s : undefined);
358352
}
359353

360354
interface CompletionData {
@@ -369,7 +363,7 @@ namespace ts.Completions {
369363
}
370364
type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag };
371365

372-
function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number): CompletionData {
366+
function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number): CompletionData | undefined {
373367
const isJavaScriptFile = isSourceFileJavaScript(sourceFile);
374368

375369
let request: Request | undefined;
@@ -615,7 +609,7 @@ namespace ts.Completions {
615609
// Extract module or enum members
616610
const exportedSymbols = typeChecker.getExportsOfModule(symbol);
617611
const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess(<PropertyAccessExpression>(node.parent), symbol.getUnescapedName());
618-
const isValidTypeAccess = (symbol: Symbol) => symbolCanbeReferencedAtTypeLocation(symbol);
612+
const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol);
619613
const isValidAccess = isRhsOfImportDeclaration ?
620614
// Any kind is allowed when dotting off namespace in internal import equals declaration
621615
(symbol: Symbol) => isValidTypeAccess(symbol) || isValidValueAccess(symbol) :
@@ -630,7 +624,7 @@ namespace ts.Completions {
630624

631625
if (!isTypeLocation) {
632626
const type = typeChecker.getTypeAtLocation(node);
633-
addTypeProperties(type);
627+
if (type) addTypeProperties(type);
634628
}
635629
}
636630

@@ -642,17 +636,17 @@ namespace ts.Completions {
642636
symbols.push(symbol);
643637
}
644638
}
639+
}
645640

646-
if (isJavaScriptFile && type.flags & TypeFlags.Union) {
647-
// In javascript files, for union types, we don't just get the members that
648-
// the individual types have in common, we also include all the members that
649-
// each individual type has. This is because we're going to add all identifiers
650-
// anyways. So we might as well elevate the members that were at least part
651-
// of the individual types to a higher status since we know what they are.
652-
const unionType = <UnionType>type;
653-
for (const elementType of unionType.types) {
654-
addTypeProperties(elementType);
655-
}
641+
if (isJavaScriptFile && type.flags & TypeFlags.Union) {
642+
// In javascript files, for union types, we don't just get the members that
643+
// the individual types have in common, we also include all the members that
644+
// each individual type has. This is because we're going to add all identifiers
645+
// anyways. So we might as well elevate the members that were at least part
646+
// of the individual types to a higher status since we know what they are.
647+
const unionType = <UnionType>type;
648+
for (const elementType of unionType.types) {
649+
addTypeProperties(elementType);
656650
}
657651
}
658652
}
@@ -777,12 +771,12 @@ namespace ts.Completions {
777771
(!isContextTokenValueLocation(contextToken) &&
778772
(isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)))) {
779773
// Its a type, but you can reach it by namespace.type as well
780-
return symbolCanbeReferencedAtTypeLocation(symbol);
774+
return symbolCanBeReferencedAtTypeLocation(symbol);
781775
}
782776
}
783777

784778
// expressions are value space (which includes the value namespaces)
785-
return !!(symbol.flags & SymbolFlags.Value);
779+
return !!(getCombinedLocalAndExportSymbolFlags(symbol) & SymbolFlags.Value);
786780
});
787781
}
788782

@@ -812,7 +806,9 @@ namespace ts.Completions {
812806
}
813807
}
814808

815-
function symbolCanbeReferencedAtTypeLocation(symbol: Symbol): boolean {
809+
function symbolCanBeReferencedAtTypeLocation(symbol: Symbol): boolean {
810+
symbol = symbol.exportSymbol || symbol;
811+
816812
// This is an alias, follow what it aliases
817813
if (symbol && symbol.flags & SymbolFlags.Alias) {
818814
symbol = typeChecker.getAliasedSymbol(symbol);
@@ -826,7 +822,7 @@ namespace ts.Completions {
826822
const exportedSymbols = typeChecker.getExportsOfModule(symbol);
827823
// If the exported symbols contains type,
828824
// symbol can be referenced at locations where type is allowed
829-
return forEach(exportedSymbols, symbolCanbeReferencedAtTypeLocation);
825+
return forEach(exportedSymbols, symbolCanBeReferencedAtTypeLocation);
830826
}
831827
}
832828

@@ -1598,47 +1594,36 @@ namespace ts.Completions {
15981594
/**
15991595
* Get the name to be display in completion from a given symbol.
16001596
*
1601-
* @return undefined if the name is of external module otherwise a name with striped of any quote
1597+
* @return undefined if the name is of external module
16021598
*/
1603-
function getCompletionEntryDisplayNameForSymbol(typeChecker: TypeChecker, symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, location: Node): string {
1604-
const displayName: string = getDeclaredName(typeChecker, symbol, location);
1605-
1606-
if (displayName) {
1607-
const firstCharCode = displayName.charCodeAt(0);
1608-
// First check of the displayName is not external module; if it is an external module, it is not valid entry
1609-
if ((symbol.flags & SymbolFlags.Namespace) && (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote)) {
1599+
function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean): string | undefined {
1600+
const name = symbol.getUnescapedName();
1601+
if (!name) return undefined;
1602+
1603+
// First check of the displayName is not external module; if it is an external module, it is not valid entry
1604+
if (symbol.flags & SymbolFlags.Namespace) {
1605+
const firstCharCode = name.charCodeAt(0);
1606+
if (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote) {
16101607
// If the symbol is external module, don't show it in the completion list
16111608
// (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there)
16121609
return undefined;
16131610
}
16141611
}
16151612

1616-
return getCompletionEntryDisplayName(displayName, target, performCharacterChecks);
1613+
return getCompletionEntryDisplayName(name, target, performCharacterChecks);
16171614
}
16181615

16191616
/**
16201617
* Get a displayName from a given for completion list, performing any necessary quotes stripping
16211618
* and checking whether the name is valid identifier name.
16221619
*/
16231620
function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean): string {
1624-
if (!name) {
1625-
return undefined;
1626-
}
1627-
1628-
name = stripQuotes(name);
1629-
1630-
if (!name) {
1631-
return undefined;
1632-
}
1633-
16341621
// If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an
16351622
// invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name.
16361623
// e.g "b a" is valid quoted name but when we strip off the quotes, it is invalid.
16371624
// We, thus, need to check if whatever was inside the quotes is actually a valid identifier name.
1638-
if (performCharacterChecks) {
1639-
if (!isIdentifierText(name, target)) {
1640-
return undefined;
1641-
}
1625+
if (performCharacterChecks && !isIdentifierText(name, target)) {
1626+
return undefined;
16421627
}
16431628

16441629
return name;

src/services/services.ts

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,42 +2063,33 @@ namespace ts {
20632063
}
20642064

20652065
function initializeNameTable(sourceFile: SourceFile): void {
2066-
const nameTable = createUnderscoreEscapedMap<number>();
2067-
2068-
walk(sourceFile);
2069-
sourceFile.nameTable = nameTable;
2066+
const nameTable = sourceFile.nameTable = createUnderscoreEscapedMap<number>();
2067+
sourceFile.forEachChild(function walk(node) {
2068+
if ((isIdentifier(node) || isStringOrNumericLiteral(node) && literalIsName(node)) && node.text) {
2069+
const text = getEscapedTextOfIdentifierOrLiteral(node);
2070+
nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1);
2071+
}
20702072

2071-
function walk(node: Node) {
2072-
switch (node.kind) {
2073-
case SyntaxKind.Identifier:
2074-
setNameTable((<Identifier>node).text, node);
2075-
break;
2076-
case SyntaxKind.StringLiteral:
2077-
case SyntaxKind.NumericLiteral:
2078-
// We want to store any numbers/strings if they were a name that could be
2079-
// related to a declaration. So, if we have 'import x = require("something")'
2080-
// then we want 'something' to be in the name table. Similarly, if we have
2081-
// "a['propname']" then we want to store "propname" in the name table.
2082-
if (isDeclarationName(node) ||
2083-
node.parent.kind === SyntaxKind.ExternalModuleReference ||
2084-
isArgumentOfElementAccessExpression(node) ||
2085-
isLiteralComputedPropertyDeclarationName(node)) {
2086-
setNameTable(getEscapedTextOfIdentifierOrLiteral((<LiteralExpression>node)), node);
2087-
}
2088-
break;
2089-
default:
2090-
forEachChild(node, walk);
2091-
if (node.jsDoc) {
2092-
for (const jsDoc of node.jsDoc) {
2093-
forEachChild(jsDoc, walk);
2094-
}
2095-
}
2073+
forEachChild(node, walk);
2074+
if (node.jsDoc) {
2075+
for (const jsDoc of node.jsDoc) {
2076+
forEachChild(jsDoc, walk);
2077+
}
20962078
}
2097-
}
2079+
});
2080+
}
20982081

2099-
function setNameTable(text: __String, node: ts.Node): void {
2100-
nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1);
2101-
}
2082+
/**
2083+
* We want to store any numbers/strings if they were a name that could be
2084+
* related to a declaration. So, if we have 'import x = require("something")'
2085+
* then we want 'something' to be in the name table. Similarly, if we have
2086+
* "a['propname']" then we want to store "propname" in the name table.
2087+
*/
2088+
function literalIsName(node: ts.StringLiteral | ts.NumericLiteral): boolean {
2089+
return isDeclarationName(node) ||
2090+
node.parent.kind === SyntaxKind.ExternalModuleReference ||
2091+
isArgumentOfElementAccessExpression(node) ||
2092+
isLiteralComputedPropertyDeclarationName(node);
21022093
}
21032094

21042095
function isObjectLiteralElement(node: Node): node is ObjectLiteralElement {

0 commit comments

Comments
 (0)