Merge pull request #3818 from Microsoft/exportSpecifierCompletions

Support completions in exports with module specifiers.
This commit is contained in:
Daniel Rosenwasser
2015-07-15 14:23:20 -07:00
17 changed files with 496 additions and 77 deletions

View File

@@ -5135,7 +5135,12 @@ namespace ts {
}
else {
node.exportClause = parseNamedImportsOrExports(SyntaxKind.NamedExports);
if (parseOptional(SyntaxKind.FromKeyword)) {
// It is not uncommon to accidentally omit the 'from' keyword. Additionally, in editing scenarios,
// the 'from' keyword can be parsed as a named export when the export clause is unterminated (i.e. `export { from "moduleName";`)
// If we don't have a 'from' keyword, see if we have a string literal such that ASI won't take effect.
if (token === SyntaxKind.FromKeyword || (token === SyntaxKind.StringLiteral && !scanner.hasPrecedingLineBreak())) {
parseExpected(SyntaxKind.FromKeyword)
node.moduleSpecifier = parseModuleSpecifier();
}
}

View File

@@ -3028,17 +3028,17 @@ namespace ts {
function tryGetGlobalSymbols(): boolean {
let objectLikeContainer: ObjectLiteralExpression | BindingPattern;
let importClause: ImportClause;
let namedImportsOrExports: NamedImportsOrExports;
let jsxContainer: JsxOpeningLikeElement;
if (objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken)) {
return tryGetObjectLikeCompletionSymbols(objectLikeContainer);
}
if (importClause = <ImportClause>getAncestor(contextToken, SyntaxKind.ImportClause)) {
if (namedImportsOrExports = tryGetNamedImportsOrExportsForCompletion(contextToken)) {
// cursor is in an import clause
// try to show exported member for imported module
return tryGetImportClauseCompletionSymbols(importClause);
return tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports);
}
if (jsxContainer = tryGetContainingJsxElement(contextToken)) {
@@ -3048,7 +3048,7 @@ namespace ts {
attrsType = typeChecker.getJsxElementAttributesType(<JsxOpeningLikeElement>jsxContainer);
if (attrsType) {
symbols = filterJsxAttributes((<JsxOpeningLikeElement>jsxContainer).attributes, typeChecker.getPropertiesOfType(attrsType));
symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (<JsxOpeningLikeElement>jsxContainer).attributes);
isMemberCompletion = true;
isNewIdentifierLocation = false;
return true;
@@ -3117,24 +3117,12 @@ namespace ts {
function isCompletionListBlocker(contextToken: Node): boolean {
let start = new Date().getTime();
let result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) ||
isIdentifierDefinitionLocation(contextToken) ||
isSolelyIdentifierDefinitionLocation(contextToken) ||
isDotOfNumericLiteral(contextToken);
log("getCompletionsAtPosition: isCompletionListBlocker: " + (new Date().getTime() - start));
return result;
}
function shouldShowCompletionsInImportsClause(node: Node): boolean {
if (node) {
// import {|
// import {a,|
if (node.kind === SyntaxKind.OpenBraceToken || node.kind === SyntaxKind.CommaToken) {
return node.parent.kind === SyntaxKind.NamedImports;
}
}
return false;
}
function isNewIdentifierDefinitionLocation(previousToken: Node): boolean {
if (previousToken) {
let containingNodeKind = previousToken.parent.kind;
@@ -3266,38 +3254,42 @@ namespace ts {
}
/**
* Aggregates relevant symbols for completion in import clauses; for instance,
* Aggregates relevant symbols for completion in import clauses and export clauses
* whose declarations have a module specifier; for instance, symbols will be aggregated for
*
* import { $ } from "moduleName";
* import { | } from "moduleName";
* export { a as foo, | } from "moduleName";
*
* but not for
*
* export { | };
*
* Relevant symbols are stored in the captured 'symbols' variable.
*
* @returns true if 'symbols' was successfully populated; false otherwise.
*/
function tryGetImportClauseCompletionSymbols(importClause: ImportClause): boolean {
// cursor is in import clause
// try to show exported member for imported module
if (shouldShowCompletionsInImportsClause(contextToken)) {
isMemberCompletion = true;
isNewIdentifierLocation = false;
function tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports: NamedImportsOrExports): boolean {
let declarationKind = namedImportsOrExports.kind === SyntaxKind.NamedImports ?
SyntaxKind.ImportDeclaration :
SyntaxKind.ExportDeclaration;
let importOrExportDeclaration = <ImportDeclaration | ExportDeclaration>getAncestor(namedImportsOrExports, declarationKind);
let moduleSpecifier = importOrExportDeclaration.moduleSpecifier;
let importDeclaration = <ImportDeclaration>importClause.parent;
Debug.assert(importDeclaration !== undefined && importDeclaration.kind === SyntaxKind.ImportDeclaration);
let exports: Symbol[];
let moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(importDeclaration.moduleSpecifier);
if (moduleSpecifierSymbol) {
exports = typeChecker.getExportsOfModule(moduleSpecifierSymbol);
}
//let exports = typeInfoResolver.getExportsOfImportDeclaration(importDeclaration);
symbols = exports ? filterModuleExports(exports, importDeclaration) : emptyArray;
if (!moduleSpecifier) {
return false;
}
else {
isMemberCompletion = false;
isNewIdentifierLocation = true;
isMemberCompletion = true;
isNewIdentifierLocation = false;
let exports: Symbol[];
let moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(importOrExportDeclaration.moduleSpecifier);
if (moduleSpecifierSymbol) {
exports = typeChecker.getExportsOfModule(moduleSpecifierSymbol);
}
symbols = exports ? filterNamedImportOrExportCompletionItems(exports, namedImportsOrExports.elements) : emptyArray;
return true;
}
@@ -3321,6 +3313,26 @@ namespace ts {
return undefined;
}
/**
* Returns the containing list of named imports or exports of a context token,
* on the condition that one exists and that the context implies completion should be given.
*/
function tryGetNamedImportsOrExportsForCompletion(contextToken: Node): NamedImportsOrExports {
if (contextToken) {
switch (contextToken.kind) {
case SyntaxKind.OpenBraceToken: // import { |
case SyntaxKind.CommaToken: // import { a as 0, |
switch (contextToken.parent.kind) {
case SyntaxKind.NamedImports:
case SyntaxKind.NamedExports:
return <NamedImportsOrExports>contextToken.parent;
}
}
}
return undefined;
}
function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement {
if (contextToken) {
let parent = contextToken.parent;
@@ -3368,7 +3380,10 @@ namespace ts {
return false;
}
function isIdentifierDefinitionLocation(contextToken: Node): boolean {
/**
* @returns true if we are certain that the currently edited location must define a new location; false otherwise.
*/
function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean {
let containingNodeKind = contextToken.parent.kind;
switch (contextToken.kind) {
case SyntaxKind.CommaToken:
@@ -3425,6 +3440,11 @@ namespace ts {
case SyntaxKind.ProtectedKeyword:
return containingNodeKind === SyntaxKind.Parameter;
case SyntaxKind.AsKeyword:
containingNodeKind === SyntaxKind.ImportSpecifier ||
containingNodeKind === SyntaxKind.ExportSpecifier ||
containingNodeKind === SyntaxKind.NamespaceImport;
case SyntaxKind.ClassKeyword:
case SyntaxKind.EnumKeyword:
case SyntaxKind.InterfaceKeyword:
@@ -3466,33 +3486,41 @@ namespace ts {
return false;
}
function filterModuleExports(exports: Symbol[], importDeclaration: ImportDeclaration): Symbol[] {
let exisingImports: Map<boolean> = {};
/**
* Filters out completion suggestions for named imports or exports.
*
* @param exportsOfModule The list of symbols which a module exposes.
* @param namedImportsOrExports The list of existing import/export specifiers in the import/export clause.
*
* @returns Symbols to be suggested at an import/export clause, barring those whose named imports/exports
* do not occur at the current position and have not otherwise been typed.
*/
function filterNamedImportOrExportCompletionItems(exportsOfModule: Symbol[], namedImportsOrExports: ImportOrExportSpecifier[]): Symbol[] {
let exisingImportsOrExports: Map<boolean> = {};
if (!importDeclaration.importClause) {
return exports;
for (let element of namedImportsOrExports) {
// If this is the current item we are editing right now, do not filter it out
if (element.getStart() <= position && position <= element.getEnd()) {
continue;
}
let name = element.propertyName || element.name;
exisingImportsOrExports[name.text] = true;
}
if (importDeclaration.importClause.namedBindings &&
importDeclaration.importClause.namedBindings.kind === SyntaxKind.NamedImports) {
forEach((<NamedImports>importDeclaration.importClause.namedBindings).elements, el => {
// If this is the current item we are editing right now, do not filter it out
if (el.getStart() <= position && position <= el.getEnd()) {
return;
}
let name = el.propertyName || el.name;
exisingImports[name.text] = true;
});
if (isEmpty(exisingImportsOrExports)) {
return exportsOfModule;
}
if (isEmpty(exisingImports)) {
return exports;
}
return filter(exports, e => !lookUp(exisingImports, e.name));
return filter(exportsOfModule, e => !lookUp(exisingImportsOrExports, e.name));
}
/**
* Filters out completion suggestions for named imports or exports.
*
* @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations
* do not occur at the current position and have not otherwise been typed.
*/
function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: Declaration[]): Symbol[] {
if (!existingMembers || existingMembers.length === 0) {
return contextualMemberSymbols;
@@ -3527,17 +3555,16 @@ namespace ts {
existingMemberNames[existingName] = true;
}
let filteredMembers: Symbol[] = [];
forEach(contextualMemberSymbols, s => {
if (!existingMemberNames[s.name]) {
filteredMembers.push(s);
}
});
return filteredMembers;
return filter(contextualMemberSymbols, m => !lookUp(existingMemberNames, m.name));
}
function filterJsxAttributes(attributes: NodeArray<JsxAttribute | JsxSpreadAttribute>, symbols: Symbol[]): Symbol[] {
/**
* Filters out completion suggestions from 'symbols' according to existing JSX attributes.
*
* @returns Symbols to be suggested in a JSX element, barring those whose attributes
* do not occur at the current position and have not otherwise been typed.
*/
function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray<JsxAttribute | JsxSpreadAttribute>): Symbol[] {
let seenNames: Map<boolean> = {};
for (let attr of attributes) {
// If this is the current item we are editing right now, do not filter it out
@@ -3549,13 +3576,8 @@ namespace ts {
seenNames[(<JsxAttribute>attr).name.text] = true;
}
}
let result: Symbol[] = [];
for (let sym of symbols) {
if (!seenNames[sym.name]) {
result.push(sym);
}
}
return result;
return filter(symbols, a => !lookUp(seenNames, a.name));
}
}

View File

@@ -0,0 +1,34 @@
//// [tests/cases/compiler/exportDeclarationWithModuleSpecifierNameOnNextLine1.ts] ////
//// [t1.ts]
export var x = "x";
//// [t2.ts]
export { x } from
"./t1";
//// [t3.ts]
export { } from
"./t1";
//// [t4.ts]
export { x as a } from
"./t1";
//// [t5.ts]
export { x as a, } from
"./t1";
//// [t1.js]
exports.x = "x";
//// [t2.js]
var t1_1 = require("./t1");
exports.x = t1_1.x;
//// [t3.js]
//// [t4.js]
var t1_1 = require("./t1");
exports.a = t1_1.x;
//// [t5.js]
var t1_1 = require("./t1");
exports.a = t1_1.x;

View File

@@ -0,0 +1,28 @@
=== tests/cases/compiler/t1.ts ===
export var x = "x";
>x : Symbol(x, Decl(t1.ts, 1, 10))
=== tests/cases/compiler/t2.ts ===
export { x } from
>x : Symbol(x, Decl(t2.ts, 0, 8))
"./t1";
=== tests/cases/compiler/t3.ts ===
export { } from
No type information for this code. "./t1";
No type information for this code.
No type information for this code.=== tests/cases/compiler/t4.ts ===
export { x as a } from
>x : Symbol(a, Decl(t4.ts, 0, 8))
>a : Symbol(a, Decl(t4.ts, 0, 8))
"./t1";
=== tests/cases/compiler/t5.ts ===
export { x as a, } from
>x : Symbol(a, Decl(t5.ts, 0, 8))
>a : Symbol(a, Decl(t5.ts, 0, 8))
"./t1";

View File

@@ -0,0 +1,29 @@
=== tests/cases/compiler/t1.ts ===
export var x = "x";
>x : string
>"x" : string
=== tests/cases/compiler/t2.ts ===
export { x } from
>x : string
"./t1";
=== tests/cases/compiler/t3.ts ===
export { } from
No type information for this code. "./t1";
No type information for this code.
No type information for this code.=== tests/cases/compiler/t4.ts ===
export { x as a } from
>x : string
>a : string
"./t1";
=== tests/cases/compiler/t5.ts ===
export { x as a, } from
>x : string
>a : string
"./t1";

View File

@@ -0,0 +1,44 @@
tests/cases/compiler/t2.ts(1,13): error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
tests/cases/compiler/t2.ts(1,18): error TS1005: ',' expected.
tests/cases/compiler/t3.ts(1,10): error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
tests/cases/compiler/t3.ts(1,15): error TS1005: ',' expected.
tests/cases/compiler/t4.ts(1,17): error TS1005: ',' expected.
tests/cases/compiler/t4.ts(1,17): error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
tests/cases/compiler/t4.ts(1,22): error TS1005: ',' expected.
tests/cases/compiler/t5.ts(1,18): error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
tests/cases/compiler/t5.ts(1,23): error TS1005: ',' expected.
==== tests/cases/compiler/t1.ts (0 errors) ====
export var x = "x";
==== tests/cases/compiler/t2.ts (2 errors) ====
export { x, from "./t1"
~~~~
!!! error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
~~~~~~
!!! error TS1005: ',' expected.
==== tests/cases/compiler/t3.ts (2 errors) ====
export { from "./t1"
~~~~
!!! error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
~~~~~~
!!! error TS1005: ',' expected.
==== tests/cases/compiler/t4.ts (3 errors) ====
export { x as a from "./t1"
~~~~
!!! error TS1005: ',' expected.
~~~~
!!! error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
~~~~~~
!!! error TS1005: ',' expected.
==== tests/cases/compiler/t5.ts (2 errors) ====
export { x as a, from "./t1"
~~~~
!!! error TS2305: Module '"tests/cases/compiler/t1"' has no exported member 'from'.
~~~~~~
!!! error TS1005: ',' expected.

View File

@@ -0,0 +1,30 @@
//// [tests/cases/compiler/unclosedExportClause01.ts] ////
//// [t1.ts]
export var x = "x";
//// [t2.ts]
export { x, from "./t1"
//// [t3.ts]
export { from "./t1"
//// [t4.ts]
export { x as a from "./t1"
//// [t5.ts]
export { x as a, from "./t1"
//// [t1.js]
exports.x = "x";
//// [t2.js]
var t1_1 = require("./t1");
exports.x = t1_1.x;
//// [t3.js]
//// [t4.js]
var t1_1 = require("./t1");
exports.a = t1_1.x;
//// [t5.js]
var t1_1 = require("./t1");
exports.a = t1_1.x;

View File

@@ -0,0 +1,57 @@
tests/cases/compiler/t2.ts(1,10): error TS2304: Cannot find name 'x'.
tests/cases/compiler/t2.ts(1,13): error TS2304: Cannot find name 'from'.
tests/cases/compiler/t2.ts(2,5): error TS1005: ',' expected.
tests/cases/compiler/t3.ts(1,10): error TS2304: Cannot find name 'from'.
tests/cases/compiler/t3.ts(2,5): error TS1005: ',' expected.
tests/cases/compiler/t4.ts(1,10): error TS2304: Cannot find name 'x'.
tests/cases/compiler/t4.ts(1,17): error TS1005: ',' expected.
tests/cases/compiler/t4.ts(1,17): error TS2304: Cannot find name 'from'.
tests/cases/compiler/t4.ts(2,5): error TS1005: ',' expected.
tests/cases/compiler/t5.ts(1,10): error TS2304: Cannot find name 'x'.
tests/cases/compiler/t5.ts(1,18): error TS2304: Cannot find name 'from'.
tests/cases/compiler/t5.ts(2,5): error TS1005: ',' expected.
==== tests/cases/compiler/t1.ts (0 errors) ====
export var x = "x";
==== tests/cases/compiler/t2.ts (3 errors) ====
export { x, from
~
!!! error TS2304: Cannot find name 'x'.
~~~~
!!! error TS2304: Cannot find name 'from'.
"./t1";
~~~~~~
!!! error TS1005: ',' expected.
==== tests/cases/compiler/t3.ts (2 errors) ====
export { from
~~~~
!!! error TS2304: Cannot find name 'from'.
"./t1";
~~~~~~
!!! error TS1005: ',' expected.
==== tests/cases/compiler/t4.ts (4 errors) ====
export { x as a from
~
!!! error TS2304: Cannot find name 'x'.
~~~~
!!! error TS1005: ',' expected.
~~~~
!!! error TS2304: Cannot find name 'from'.
"./t1";
~~~~~~
!!! error TS1005: ',' expected.
==== tests/cases/compiler/t5.ts (3 errors) ====
export { x as a, from
~
!!! error TS2304: Cannot find name 'x'.
~~~~
!!! error TS2304: Cannot find name 'from'.
"./t1";
~~~~~~
!!! error TS1005: ',' expected.

View File

@@ -0,0 +1,32 @@
//// [tests/cases/compiler/unclosedExportClause02.ts] ////
//// [t1.ts]
export var x = "x";
//// [t2.ts]
export { x, from
"./t1";
//// [t3.ts]
export { from
"./t1";
//// [t4.ts]
export { x as a from
"./t1";
//// [t5.ts]
export { x as a, from
"./t1";
//// [t1.js]
exports.x = "x";
//// [t2.js]
"./t1";
//// [t3.js]
"./t1";
//// [t4.js]
"./t1";
//// [t5.js]
"./t1";

View File

@@ -0,0 +1,20 @@
// @module: commonjs
// @filename: t1.ts
export var x = "x";
// @filename: t2.ts
export { x } from
"./t1";
// @filename: t3.ts
export { } from
"./t1";
// @filename: t4.ts
export { x as a } from
"./t1";
// @filename: t5.ts
export { x as a, } from
"./t1";

View File

@@ -0,0 +1,16 @@
// @module: commonjs
// @filename: t1.ts
export var x = "x";
// @filename: t2.ts
export { x, from "./t1"
// @filename: t3.ts
export { from "./t1"
// @filename: t4.ts
export { x as a from "./t1"
// @filename: t5.ts
export { x as a, from "./t1"

View File

@@ -0,0 +1,20 @@
// @module: commonjs
// @filename: t1.ts
export var x = "x";
// @filename: t2.ts
export { x, from
"./t1";
// @filename: t3.ts
export { from
"./t1";
// @filename: t4.ts
export { x as a from
"./t1";
// @filename: t5.ts
export { x as a, from
"./t1";

View File

@@ -0,0 +1,40 @@
/// <reference path='fourslash.ts'/>
// @Filename: m1.ts
////export var foo: number = 1;
////export function bar() { return 10; }
////export function baz() { return 10; }
// @Filename: m2.ts
////export {/*1*/, /*2*/ from "m1"
////export {/*3*/} from "m1"
////export {foo,/*4*/ from "m1"
////export {bar as /*5*/, /*6*/ from "m1"
////export {foo, bar, baz as b,/*7*/} from "m1"
function verifyCompletionAtMarker(marker: string, showBuilder: boolean, ...completions: string[]) {
goTo.marker(marker);
if (completions.length) {
for (let completion of completions) {
verify.completionListContains(completion);
}
}
else {
verify.completionListIsEmpty();
}
if (showBuilder) {
verify.completionListAllowsNewIdentifier();
}
else {
verify.not.completionListAllowsNewIdentifier();
}
}
verifyCompletionAtMarker("1", /*showBuilder*/ false, "foo", "bar", "baz");
verifyCompletionAtMarker("2", /*showBuilder*/ false, "foo", "bar", "baz");
verifyCompletionAtMarker("3", /*showBuilder*/ false, "foo", "bar", "baz");
verifyCompletionAtMarker("4", /*showBuilder*/ false, "bar", "baz");
verifyCompletionAtMarker("5", /*showBuilder*/ true);
verifyCompletionAtMarker("6", /*showBuilder*/ false, "foo", "baz");
verifyCompletionAtMarker("7", /*showBuilder*/ false);

View File

@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts'/>
////declare module "M1" {
//// export var V;
////}
////var W;
////declare module "M2" {
//// export { /**/ } from "M1"
////}
goTo.marker();
verify.completionListContains("V");
verify.not.completionListContains("W");
verify.not.completionListAllowsNewIdentifier();

View File

@@ -0,0 +1,16 @@
/// <reference path="fourslash.ts" />
////declare module "M1" {
//// export var abc: number;
//// export var def: string;
////}
////
////declare module "M2" {
//// export { abc/**/ } from "M1";
////}
// Ensure we don't filter out the current item.
goTo.marker();
verify.completionListContains("abc");
verify.completionListContains("def");
verify.not.completionListAllowsNewIdentifier();

View File

@@ -11,6 +11,7 @@
////import {foo,/*4*/ from "m1"
////import {bar as /*5*/, /*6*/ from "m1"
////import {foo, bar, baz as b,/*7*/} from "m1"
function verifyCompletionAtMarker(marker: string, showBuilder: boolean, ...completions: string[]) {
goTo.marker(marker);
if (completions.length) {

View File

@@ -0,0 +1,11 @@
/// <reference path='fourslash.ts'/>
// @Filename: m1.ts
////export var foo: number = 1;
// @Filename: m2.ts
////import * as /**/ from "m1"
goTo.marker();
verify.completionListIsEmpty();
verify.completionListAllowsNewIdentifier();