Add support for declare export + fixes (#224)

* Add support for declare export

* Do not allow declare module inside declare module

* Reallow module exports outside module

* Add handling of `declare export default`

Add check for multiple module.export declarations

* Disallow export let/const/type

Refactor parsing object properties to share more code and add support
for getters and setters

* Rename tests

* Update test fixtures

* Optimize for performance

* disallow declare export interface outside of module

* Refactor code to be more readable and less lookahead

* Add comments

* Add test for export star as

* Test for number literal getter/setter

* Add more tests

* Fix tests

* Allow union types and correctly eat semi after type

* Use non computed keys

* Fix tests
This commit is contained in:
Daniel Tschinder
2017-04-21 14:41:59 +02:00
committed by GitHub
parent 28ccd05bab
commit d1a5220b89
117 changed files with 4857 additions and 708 deletions

View File

@@ -105,7 +105,7 @@ pp.flowParseDeclareFunction = function (node) {
return this.finishNode(node, "DeclareFunction");
};
pp.flowParseDeclare = function (node) {
pp.flowParseDeclare = function (node, insideModule) {
if (this.match(tt._class)) {
return this.flowParseDeclareClass(node);
} else if (this.match(tt._function)) {
@@ -116,12 +116,15 @@ pp.flowParseDeclare = function (node) {
if (this.lookahead().type === tt.dot) {
return this.flowParseDeclareModuleExports(node);
} else {
if (insideModule) this.unexpected(null, "`declare module` cannot be used inside another `declare module`");
return this.flowParseDeclareModule(node);
}
} else if (this.isContextual("type")) {
return this.flowParseDeclareTypeAlias(node);
} else if (this.isContextual("interface")) {
return this.flowParseDeclareInterface(node);
} else if (this.match(tt._export)) {
return this.flowParseDeclareExportDeclaration(node, insideModule);
} else {
this.unexpected();
}
@@ -134,6 +137,17 @@ pp.flowParseDeclareVariable = function (node) {
return this.finishNode(node, "DeclareVariable");
};
function isEsModuleType(bodyElement) {
return bodyElement.type === "DeclareExportAllDeclaration" ||
(
bodyElement.type === "DeclareExportDeclaration" &&
(
!bodyElement.declaration ||
(bodyElement.declaration.type !== "TypeAlias" && bodyElement.declaration.type !== "InterfaceDeclaration")
)
);
}
pp.flowParseDeclareModule = function (node) {
this.next();
@@ -167,9 +181,94 @@ pp.flowParseDeclareModule = function (node) {
this.expect(tt.braceR);
this.finishNode(bodyNode, "BlockStatement");
let kind = null;
let hasModuleExport = false;
const errorMessage = "Found both `declare module.exports` and `declare export` in the same module. Modules can only have 1 since they are either an ES module or they are a CommonJS module";
body.forEach((bodyElement) => {
if (isEsModuleType(bodyElement)) {
if (kind === "CommonJS") this.unexpected(bodyElement.start, errorMessage);
kind = "ES";
} else if (bodyElement.type === "DeclareModuleExports") {
if (hasModuleExport) this.unexpected(bodyElement.start, "Duplicate `declare module.exports` statement");
if (kind === "ES") this.unexpected(bodyElement.start, errorMessage);
kind = "CommonJS";
hasModuleExport = true;
}
});
node.kind = kind || "CommonJS";
return this.finishNode(node, "DeclareModule");
};
const exportSuggestions = {
const: "declare export var",
let: "declare export var",
type: "export type",
interface: "export interface",
};
pp.flowParseDeclareExportDeclaration = function (node, insideModule) {
this.expect(tt._export);
if (this.eat(tt._default)) {
if (this.match(tt._function) || this.match(tt._class)) {
// declare export default class ...
// declare export default function ...
node.declaration = this.flowParseDeclare(this.startNode());
} else {
// declare export default [type];
node.declaration = this.flowParseType();
this.semicolon();
}
node.default = true;
return this.finishNode(node, "DeclareExportDeclaration");
} else {
if (
this.match(tt._const) || this.match(tt._let) ||
(
(this.isContextual("type") || this.isContextual("interface")) &&
!insideModule
)
) {
const label = this.state.value;
const suggestion = exportSuggestions[label];
this.unexpected(this.state.start, `\`declare export ${label}\` is not supported. Use \`${suggestion}\` instead`);
}
if (
this.match(tt._var) || // declare export var ...
this.match(tt._function) || // declare export function ...
this.match(tt._class) // declare export class ...
) {
node.declaration = this.flowParseDeclare(this.startNode());
node.default = false;
return this.finishNode(node, "DeclareExportDeclaration");
} else if (
this.match(tt.star) || // declare export * from ''
this.match(tt.braceL) || // declare export {} ...
this.isContextual("interface") || // declare export interface ...
this.isContextual("type") // declare export type ...
) {
node = this.parseExport(node);
if (node.type === "ExportNamedDeclaration") {
// flow does not support the ExportNamedDeclaration
node.type = "ExportDeclaration";
node.default = false;
delete node.exportKind;
}
node.type = "Declare" + node.type;
return node;
}
}
this.unexpected();
};
pp.flowParseDeclareModuleExports = function (node) {
this.expectContextual("module");
this.expect(tt.dot);
@@ -381,15 +480,6 @@ pp.flowParseObjectTypeMethodish = function (node) {
return this.finishNode(node, "FunctionTypeAnnotation");
};
pp.flowParseObjectTypeMethod = function (startPos, startLoc, isStatic, key) {
const node = this.startNodeAt(startPos, startLoc);
node.value = this.flowParseObjectTypeMethodish(this.startNodeAt(startPos, startLoc));
node.static = isStatic;
node.key = key;
node.optional = false;
return this.finishNode(node, "ObjectTypeProperty");
};
pp.flowParseObjectTypeCallProperty = function (node, isStatic) {
const valueNode = this.startNode();
node.static = isStatic;
@@ -402,9 +492,6 @@ pp.flowParseObjectType = function (allowStatic, allowExact, allowSpread) {
this.state.inType = true;
const nodeStart = this.startNode();
let node;
let propertyKey;
let isStatic = false;
nodeStart.callProperties = [];
nodeStart.properties = [];
@@ -425,10 +512,8 @@ pp.flowParseObjectType = function (allowStatic, allowExact, allowSpread) {
nodeStart.exact = exact;
while (!this.match(endDelim)) {
let optional = false;
const startPos = this.state.start;
const startLoc = this.state.startLoc;
node = this.startNode();
let isStatic = false;
const node = this.startNode();
if (allowStatic && this.isContextual("static") && this.lookahead().type !== tt.colon) {
this.next();
isStatic = true;
@@ -444,44 +529,24 @@ pp.flowParseObjectType = function (allowStatic, allowExact, allowSpread) {
}
nodeStart.callProperties.push(this.flowParseObjectTypeCallProperty(node, isStatic));
} else {
if (this.match(tt.ellipsis)) {
if (!allowSpread) {
this.unexpected(
null,
"Spread operator cannot appear in class or interface definitions"
);
}
if (variance) {
this.unexpected(variance.start, "Spread properties cannot have variance");
}
this.expect(tt.ellipsis);
node.argument = this.flowParseType();
nodeStart.properties.push(this.finishNode(node, "ObjectTypeSpreadProperty"));
} else {
propertyKey = this.flowParseObjectPropertyKey();
if (this.isRelational("<") || this.match(tt.parenL)) {
// This is a method property
if (variance) {
this.unexpected(variance.start);
}
nodeStart.properties.push(this.flowParseObjectTypeMethod(startPos, startLoc, isStatic, propertyKey));
} else {
if (this.eat(tt.question)) {
optional = true;
}
node.key = propertyKey;
node.value = this.flowParseTypeInitialiser();
node.optional = optional;
node.static = isStatic;
node.variance = variance;
nodeStart.properties.push(this.finishNode(node, "ObjectTypeProperty"));
let kind = "init";
if (this.isContextual("get") || this.isContextual("set")) {
const lookahead = this.lookahead();
if (
lookahead.type === tt.name ||
lookahead.type === tt.string ||
lookahead.type === tt.num
) {
kind = this.state.value;
this.next();
}
}
nodeStart.properties.push(this.flowParseObjectTypeProperty(node, isStatic, variance, kind, allowSpread));
}
this.flowObjectTypeSemicolon();
isStatic = false;
}
this.expect(endDelim);
@@ -493,6 +558,65 @@ pp.flowParseObjectType = function (allowStatic, allowExact, allowSpread) {
return out;
};
pp.flowParseObjectTypeProperty = function (node, isStatic, variance, kind, allowSpread) {
if (this.match(tt.ellipsis)) {
if (!allowSpread) {
this.unexpected(
null,
"Spread operator cannot appear in class or interface definitions"
);
}
if (variance) {
this.unexpected(variance.start, "Spread properties cannot have variance");
}
this.expect(tt.ellipsis);
node.argument = this.flowParseType();
return this.finishNode(node, "ObjectTypeSpreadProperty");
} else {
node.key = this.flowParseObjectPropertyKey();
node.static = isStatic;
node.kind = kind;
let optional = false;
if (this.isRelational("<") || this.match(tt.parenL)) {
// This is a method property
if (variance) {
this.unexpected(variance.start);
}
node.value = this.flowParseObjectTypeMethodish(this.startNodeAt(node.start, node.loc.start));
if (kind === "get" || kind === "set") this.flowCheckGetterSetterParamCount(node);
} else {
if (kind !== "init") this.unexpected();
if (this.eat(tt.question)) {
optional = true;
}
node.value = this.flowParseTypeInitialiser();
node.variance = variance;
}
node.optional = optional;
return this.finishNode(node, "ObjectTypeProperty");
}
};
// This is similar to checkGetterSetterParamCount, but as
// babylon uses non estree properties we cannot reuse it here
pp.flowCheckGetterSetterParamCount = function (property) {
const paramCount = property.kind === "get" ? 0 : 1;
if (property.value.params.length !== paramCount) {
const start = property.start;
if (property.kind === "get") {
this.raise(start, "getter should have no params");
} else {
this.raise(start, "setter should have exactly one param");
}
}
};
pp.flowObjectTypeSemicolon = function () {
if (!this.eat(tt.semi) && !this.eat(tt.comma) &&
!this.match(tt.braceR) && !this.match(tt.braceBarR)) {
@@ -898,7 +1022,7 @@ export default (superClass) => class extends superClass {
parseExpressionStatement(node, expr) {
if (expr.type === "Identifier") {
if (expr.name === "declare") {
if (this.match(tt._class) || this.match(tt.name) || this.match(tt._function) || this.match(tt._var)) {
if (this.match(tt._class) || this.match(tt.name) || this.match(tt._function) || this.match(tt._var) || this.match(tt._export)) {
return this.flowParseDeclare(node);
}
} else if (this.match(tt.name)) {