Skip to content

Commit d17f13a

Browse files
Merge pull request microsoft#3818 from Microsoft/exportSpecifierCompletions
Support completions in exports with module specifiers.
2 parents 22d3894 + 076028c commit d17f13a

17 files changed

+496
-77
lines changed

src/compiler/parser.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5135,7 +5135,12 @@ namespace ts {
51355135
}
51365136
else {
51375137
node.exportClause = parseNamedImportsOrExports(SyntaxKind.NamedExports);
5138-
if (parseOptional(SyntaxKind.FromKeyword)) {
5138+
5139+
// It is not uncommon to accidentally omit the 'from' keyword. Additionally, in editing scenarios,
5140+
// the 'from' keyword can be parsed as a named export when the export clause is unterminated (i.e. `export { from "moduleName";`)
5141+
// If we don't have a 'from' keyword, see if we have a string literal such that ASI won't take effect.
5142+
if (token === SyntaxKind.FromKeyword || (token === SyntaxKind.StringLiteral && !scanner.hasPrecedingLineBreak())) {
5143+
parseExpected(SyntaxKind.FromKeyword)
51395144
node.moduleSpecifier = parseModuleSpecifier();
51405145
}
51415146
}

src/services/services.ts

Lines changed: 98 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3028,17 +3028,17 @@ namespace ts {
30283028

30293029
function tryGetGlobalSymbols(): boolean {
30303030
let objectLikeContainer: ObjectLiteralExpression | BindingPattern;
3031-
let importClause: ImportClause;
3031+
let namedImportsOrExports: NamedImportsOrExports;
30323032
let jsxContainer: JsxOpeningLikeElement;
30333033

30343034
if (objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken)) {
30353035
return tryGetObjectLikeCompletionSymbols(objectLikeContainer);
30363036
}
30373037

3038-
if (importClause = <ImportClause>getAncestor(contextToken, SyntaxKind.ImportClause)) {
3038+
if (namedImportsOrExports = tryGetNamedImportsOrExportsForCompletion(contextToken)) {
30393039
// cursor is in an import clause
30403040
// try to show exported member for imported module
3041-
return tryGetImportClauseCompletionSymbols(importClause);
3041+
return tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports);
30423042
}
30433043

30443044
if (jsxContainer = tryGetContainingJsxElement(contextToken)) {
@@ -3048,7 +3048,7 @@ namespace ts {
30483048
attrsType = typeChecker.getJsxElementAttributesType(<JsxOpeningLikeElement>jsxContainer);
30493049

30503050
if (attrsType) {
3051-
symbols = filterJsxAttributes((<JsxOpeningLikeElement>jsxContainer).attributes, typeChecker.getPropertiesOfType(attrsType));
3051+
symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (<JsxOpeningLikeElement>jsxContainer).attributes);
30523052
isMemberCompletion = true;
30533053
isNewIdentifierLocation = false;
30543054
return true;
@@ -3117,24 +3117,12 @@ namespace ts {
31173117
function isCompletionListBlocker(contextToken: Node): boolean {
31183118
let start = new Date().getTime();
31193119
let result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) ||
3120-
isIdentifierDefinitionLocation(contextToken) ||
3120+
isSolelyIdentifierDefinitionLocation(contextToken) ||
31213121
isDotOfNumericLiteral(contextToken);
31223122
log("getCompletionsAtPosition: isCompletionListBlocker: " + (new Date().getTime() - start));
31233123
return result;
31243124
}
31253125

3126-
function shouldShowCompletionsInImportsClause(node: Node): boolean {
3127-
if (node) {
3128-
// import {|
3129-
// import {a,|
3130-
if (node.kind === SyntaxKind.OpenBraceToken || node.kind === SyntaxKind.CommaToken) {
3131-
return node.parent.kind === SyntaxKind.NamedImports;
3132-
}
3133-
}
3134-
3135-
return false;
3136-
}
3137-
31383126
function isNewIdentifierDefinitionLocation(previousToken: Node): boolean {
31393127
if (previousToken) {
31403128
let containingNodeKind = previousToken.parent.kind;
@@ -3266,38 +3254,42 @@ namespace ts {
32663254
}
32673255

32683256
/**
3269-
* Aggregates relevant symbols for completion in import clauses; for instance,
3257+
* Aggregates relevant symbols for completion in import clauses and export clauses
3258+
* whose declarations have a module specifier; for instance, symbols will be aggregated for
3259+
*
3260+
* import { | } from "moduleName";
3261+
* export { a as foo, | } from "moduleName";
32703262
*
3271-
* import { $ } from "moduleName";
3263+
* but not for
3264+
*
3265+
* export { | };
32723266
*
32733267
* Relevant symbols are stored in the captured 'symbols' variable.
32743268
*
32753269
* @returns true if 'symbols' was successfully populated; false otherwise.
32763270
*/
3277-
function tryGetImportClauseCompletionSymbols(importClause: ImportClause): boolean {
3278-
// cursor is in import clause
3279-
// try to show exported member for imported module
3280-
if (shouldShowCompletionsInImportsClause(contextToken)) {
3281-
isMemberCompletion = true;
3282-
isNewIdentifierLocation = false;
3283-
3284-
let importDeclaration = <ImportDeclaration>importClause.parent;
3285-
Debug.assert(importDeclaration !== undefined && importDeclaration.kind === SyntaxKind.ImportDeclaration);
3271+
function tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports: NamedImportsOrExports): boolean {
3272+
let declarationKind = namedImportsOrExports.kind === SyntaxKind.NamedImports ?
3273+
SyntaxKind.ImportDeclaration :
3274+
SyntaxKind.ExportDeclaration;
3275+
let importOrExportDeclaration = <ImportDeclaration | ExportDeclaration>getAncestor(namedImportsOrExports, declarationKind);
3276+
let moduleSpecifier = importOrExportDeclaration.moduleSpecifier;
3277+
3278+
if (!moduleSpecifier) {
3279+
return false;
3280+
}
32863281

3287-
let exports: Symbol[];
3288-
let moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(importDeclaration.moduleSpecifier);
3289-
if (moduleSpecifierSymbol) {
3290-
exports = typeChecker.getExportsOfModule(moduleSpecifierSymbol);
3291-
}
3282+
isMemberCompletion = true;
3283+
isNewIdentifierLocation = false;
32923284

3293-
//let exports = typeInfoResolver.getExportsOfImportDeclaration(importDeclaration);
3294-
symbols = exports ? filterModuleExports(exports, importDeclaration) : emptyArray;
3295-
}
3296-
else {
3297-
isMemberCompletion = false;
3298-
isNewIdentifierLocation = true;
3285+
let exports: Symbol[];
3286+
let moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(importOrExportDeclaration.moduleSpecifier);
3287+
if (moduleSpecifierSymbol) {
3288+
exports = typeChecker.getExportsOfModule(moduleSpecifierSymbol);
32993289
}
33003290

3291+
symbols = exports ? filterNamedImportOrExportCompletionItems(exports, namedImportsOrExports.elements) : emptyArray;
3292+
33013293
return true;
33023294
}
33033295

@@ -3321,6 +3313,26 @@ namespace ts {
33213313
return undefined;
33223314
}
33233315

3316+
/**
3317+
* Returns the containing list of named imports or exports of a context token,
3318+
* on the condition that one exists and that the context implies completion should be given.
3319+
*/
3320+
function tryGetNamedImportsOrExportsForCompletion(contextToken: Node): NamedImportsOrExports {
3321+
if (contextToken) {
3322+
switch (contextToken.kind) {
3323+
case SyntaxKind.OpenBraceToken: // import { |
3324+
case SyntaxKind.CommaToken: // import { a as 0, |
3325+
switch (contextToken.parent.kind) {
3326+
case SyntaxKind.NamedImports:
3327+
case SyntaxKind.NamedExports:
3328+
return <NamedImportsOrExports>contextToken.parent;
3329+
}
3330+
}
3331+
}
3332+
3333+
return undefined;
3334+
}
3335+
33243336
function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement {
33253337
if (contextToken) {
33263338
let parent = contextToken.parent;
@@ -3368,7 +3380,10 @@ namespace ts {
33683380
return false;
33693381
}
33703382

3371-
function isIdentifierDefinitionLocation(contextToken: Node): boolean {
3383+
/**
3384+
* @returns true if we are certain that the currently edited location must define a new location; false otherwise.
3385+
*/
3386+
function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean {
33723387
let containingNodeKind = contextToken.parent.kind;
33733388
switch (contextToken.kind) {
33743389
case SyntaxKind.CommaToken:
@@ -3425,6 +3440,11 @@ namespace ts {
34253440
case SyntaxKind.ProtectedKeyword:
34263441
return containingNodeKind === SyntaxKind.Parameter;
34273442

3443+
case SyntaxKind.AsKeyword:
3444+
containingNodeKind === SyntaxKind.ImportSpecifier ||
3445+
containingNodeKind === SyntaxKind.ExportSpecifier ||
3446+
containingNodeKind === SyntaxKind.NamespaceImport;
3447+
34283448
case SyntaxKind.ClassKeyword:
34293449
case SyntaxKind.EnumKeyword:
34303450
case SyntaxKind.InterfaceKeyword:
@@ -3466,33 +3486,41 @@ namespace ts {
34663486
return false;
34673487
}
34683488

3469-
function filterModuleExports(exports: Symbol[], importDeclaration: ImportDeclaration): Symbol[] {
3470-
let exisingImports: Map<boolean> = {};
3471-
3472-
if (!importDeclaration.importClause) {
3473-
return exports;
3474-
}
3475-
3476-
if (importDeclaration.importClause.namedBindings &&
3477-
importDeclaration.importClause.namedBindings.kind === SyntaxKind.NamedImports) {
3489+
/**
3490+
* Filters out completion suggestions for named imports or exports.
3491+
*
3492+
* @param exportsOfModule The list of symbols which a module exposes.
3493+
* @param namedImportsOrExports The list of existing import/export specifiers in the import/export clause.
3494+
*
3495+
* @returns Symbols to be suggested at an import/export clause, barring those whose named imports/exports
3496+
* do not occur at the current position and have not otherwise been typed.
3497+
*/
3498+
function filterNamedImportOrExportCompletionItems(exportsOfModule: Symbol[], namedImportsOrExports: ImportOrExportSpecifier[]): Symbol[] {
3499+
let exisingImportsOrExports: Map<boolean> = {};
34783500

3479-
forEach((<NamedImports>importDeclaration.importClause.namedBindings).elements, el => {
3480-
// If this is the current item we are editing right now, do not filter it out
3481-
if (el.getStart() <= position && position <= el.getEnd()) {
3482-
return;
3483-
}
3501+
for (let element of namedImportsOrExports) {
3502+
// If this is the current item we are editing right now, do not filter it out
3503+
if (element.getStart() <= position && position <= element.getEnd()) {
3504+
continue;
3505+
}
34843506

3485-
let name = el.propertyName || el.name;
3486-
exisingImports[name.text] = true;
3487-
});
3507+
let name = element.propertyName || element.name;
3508+
exisingImportsOrExports[name.text] = true;
34883509
}
34893510

3490-
if (isEmpty(exisingImports)) {
3491-
return exports;
3511+
if (isEmpty(exisingImportsOrExports)) {
3512+
return exportsOfModule;
34923513
}
3493-
return filter(exports, e => !lookUp(exisingImports, e.name));
3514+
3515+
return filter(exportsOfModule, e => !lookUp(exisingImportsOrExports, e.name));
34943516
}
34953517

3518+
/**
3519+
* Filters out completion suggestions for named imports or exports.
3520+
*
3521+
* @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations
3522+
* do not occur at the current position and have not otherwise been typed.
3523+
*/
34963524
function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: Declaration[]): Symbol[] {
34973525
if (!existingMembers || existingMembers.length === 0) {
34983526
return contextualMemberSymbols;
@@ -3527,17 +3555,16 @@ namespace ts {
35273555
existingMemberNames[existingName] = true;
35283556
}
35293557

3530-
let filteredMembers: Symbol[] = [];
3531-
forEach(contextualMemberSymbols, s => {
3532-
if (!existingMemberNames[s.name]) {
3533-
filteredMembers.push(s);
3534-
}
3535-
});
3536-
3537-
return filteredMembers;
3558+
return filter(contextualMemberSymbols, m => !lookUp(existingMemberNames, m.name));
35383559
}
35393560

3540-
function filterJsxAttributes(attributes: NodeArray<JsxAttribute | JsxSpreadAttribute>, symbols: Symbol[]): Symbol[] {
3561+
/**
3562+
* Filters out completion suggestions from 'symbols' according to existing JSX attributes.
3563+
*
3564+
* @returns Symbols to be suggested in a JSX element, barring those whose attributes
3565+
* do not occur at the current position and have not otherwise been typed.
3566+
*/
3567+
function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray<JsxAttribute | JsxSpreadAttribute>): Symbol[] {
35413568
let seenNames: Map<boolean> = {};
35423569
for (let attr of attributes) {
35433570
// If this is the current item we are editing right now, do not filter it out
@@ -3549,13 +3576,8 @@ namespace ts {
35493576
seenNames[(<JsxAttribute>attr).name.text] = true;
35503577
}
35513578
}
3552-
let result: Symbol[] = [];
3553-
for (let sym of symbols) {
3554-
if (!seenNames[sym.name]) {
3555-
result.push(sym);
3556-
}
3557-
}
3558-
return result;
3579+
3580+
return filter(symbols, a => !lookUp(seenNames, a.name));
35593581
}
35603582
}
35613583

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//// [tests/cases/compiler/exportDeclarationWithModuleSpecifierNameOnNextLine1.ts] ////
2+
3+
//// [t1.ts]
4+
5+
export var x = "x";
6+
7+
//// [t2.ts]
8+
export { x } from
9+
"./t1";
10+
11+
//// [t3.ts]
12+
export { } from
13+
"./t1";
14+
15+
//// [t4.ts]
16+
export { x as a } from
17+
"./t1";
18+
19+
//// [t5.ts]
20+
export { x as a, } from
21+
"./t1";
22+
23+
//// [t1.js]
24+
exports.x = "x";
25+
//// [t2.js]
26+
var t1_1 = require("./t1");
27+
exports.x = t1_1.x;
28+
//// [t3.js]
29+
//// [t4.js]
30+
var t1_1 = require("./t1");
31+
exports.a = t1_1.x;
32+
//// [t5.js]
33+
var t1_1 = require("./t1");
34+
exports.a = t1_1.x;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
=== tests/cases/compiler/t1.ts ===
2+
3+
export var x = "x";
4+
>x : Symbol(x, Decl(t1.ts, 1, 10))
5+
6+
=== tests/cases/compiler/t2.ts ===
7+
export { x } from
8+
>x : Symbol(x, Decl(t2.ts, 0, 8))
9+
10+
"./t1";
11+
12+
=== tests/cases/compiler/t3.ts ===
13+
export { } from
14+
No type information for this code. "./t1";
15+
No type information for this code.
16+
No type information for this code.=== tests/cases/compiler/t4.ts ===
17+
export { x as a } from
18+
>x : Symbol(a, Decl(t4.ts, 0, 8))
19+
>a : Symbol(a, Decl(t4.ts, 0, 8))
20+
21+
"./t1";
22+
23+
=== tests/cases/compiler/t5.ts ===
24+
export { x as a, } from
25+
>x : Symbol(a, Decl(t5.ts, 0, 8))
26+
>a : Symbol(a, Decl(t5.ts, 0, 8))
27+
28+
"./t1";
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
=== tests/cases/compiler/t1.ts ===
2+
3+
export var x = "x";
4+
>x : string
5+
>"x" : string
6+
7+
=== tests/cases/compiler/t2.ts ===
8+
export { x } from
9+
>x : string
10+
11+
"./t1";
12+
13+
=== tests/cases/compiler/t3.ts ===
14+
export { } from
15+
No type information for this code. "./t1";
16+
No type information for this code.
17+
No type information for this code.=== tests/cases/compiler/t4.ts ===
18+
export { x as a } from
19+
>x : string
20+
>a : string
21+
22+
"./t1";
23+
24+
=== tests/cases/compiler/t5.ts ===
25+
export { x as a, } from
26+
>x : string
27+
>a : string
28+
29+
"./t1";

0 commit comments

Comments
 (0)