Flow enums parsing (#10344)
* Flow enums parsing * Parse exporting enums * Enums parsing remove lookahead, other improvements * Enums: add EnumBody and EnumMember aliases, change boolean members to use BooleaLiteral value * Fix enum member init flow type, now that boolean members have a BooleanLiteral value * Flow enums: use contextual utils, change members length checks to use logic operators, remove reserved word logic modification * Flow enums: remove unnecessary code in generator, fix error message
This commit is contained in:
committed by
Nicolò Ribaudo
parent
4b3a19ea9f
commit
ec3345bb57
@@ -81,6 +81,20 @@ function partition<T>(
|
||||
|
||||
const FLOW_PRAGMA_REGEX = /\*?\s*@((?:no)?flow)\b/;
|
||||
|
||||
// Flow enums types
|
||||
type EnumExplicitType = null | "boolean" | "number" | "string" | "symbol";
|
||||
type EnumContext = {|
|
||||
enumName: string,
|
||||
explicitType: EnumExplicitType,
|
||||
memberName: string,
|
||||
|};
|
||||
type EnumMemberInit =
|
||||
| {| type: "number", pos: number, value: N.Node |}
|
||||
| {| type: "string", pos: number, value: N.Node |}
|
||||
| {| type: "boolean", pos: number, value: N.Node |}
|
||||
| {| type: "invalid", pos: number |}
|
||||
| {| type: "none", pos: number |};
|
||||
|
||||
export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
class extends superClass {
|
||||
// The value of the @flow/@noflow pragma. Initially undefined, transitions
|
||||
@@ -97,6 +111,10 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
return this.getPluginOption("flow", "all") || this.flowPragma === "flow";
|
||||
}
|
||||
|
||||
shouldParseEnums(): boolean {
|
||||
return !!this.getPluginOption("flow", "enums");
|
||||
}
|
||||
|
||||
finishToken(type: TokenType, val: any): void {
|
||||
if (
|
||||
type !== tt.string &&
|
||||
@@ -1604,7 +1622,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
super.parseFunctionBodyAndFinish(node, type, isMethod);
|
||||
}
|
||||
|
||||
// interfaces
|
||||
// interfaces and enums
|
||||
parseStatement(context: ?string, topLevel?: boolean): N.Statement {
|
||||
// strict mode handling of `interface` since it's a reserved word
|
||||
if (
|
||||
@@ -1615,6 +1633,10 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
const node = this.startNode();
|
||||
this.next();
|
||||
return this.flowParseInterface(node);
|
||||
} else if (this.shouldParseEnums() && this.isContextual("enum")) {
|
||||
const node = this.startNode();
|
||||
this.next();
|
||||
return this.flowParseEnumDeclaration(node);
|
||||
} else {
|
||||
const stmt = super.parseStatement(context, topLevel);
|
||||
// We will parse a flow pragma in any comment before the first statement.
|
||||
@@ -1661,6 +1683,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
this.isContextual("type") ||
|
||||
this.isContextual("interface") ||
|
||||
this.isContextual("opaque") ||
|
||||
(this.shouldParseEnums() && this.isContextual("enum")) ||
|
||||
super.shouldParseExportDeclaration()
|
||||
);
|
||||
}
|
||||
@@ -1670,7 +1693,8 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
this.match(tt.name) &&
|
||||
(this.state.value === "type" ||
|
||||
this.state.value === "interface" ||
|
||||
this.state.value === "opaque")
|
||||
this.state.value === "opaque" ||
|
||||
(this.shouldParseEnums() && this.state.value === "enum"))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -1678,6 +1702,15 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
return super.isExportDefaultSpecifier();
|
||||
}
|
||||
|
||||
parseExportDefaultExpression(): N.Expression | N.Declaration {
|
||||
if (this.shouldParseEnums() && this.isContextual("enum")) {
|
||||
const node = this.startNode();
|
||||
this.next();
|
||||
return this.flowParseEnumDeclaration(node);
|
||||
}
|
||||
return super.parseExportDefaultExpression();
|
||||
}
|
||||
|
||||
parseConditional(
|
||||
expr: N.Expression,
|
||||
noIn: ?boolean,
|
||||
@@ -1935,6 +1968,11 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
const declarationNode = this.startNode();
|
||||
this.next();
|
||||
return this.flowParseInterface(declarationNode);
|
||||
} else if (this.shouldParseEnums() && this.isContextual("enum")) {
|
||||
node.exportKind = "value";
|
||||
const declarationNode = this.startNode();
|
||||
this.next();
|
||||
return this.flowParseEnumDeclaration(declarationNode);
|
||||
} else {
|
||||
return super.parseExportDeclaration(node);
|
||||
}
|
||||
@@ -2839,4 +2877,418 @@ export default (superClass: Class<Parser>): Class<Parser> =>
|
||||
this.raise(this.state.pos, "Unterminated comment");
|
||||
}
|
||||
}
|
||||
|
||||
// Flow enum parsing
|
||||
|
||||
flowEnumErrorBooleanMemberNotInitialized(
|
||||
pos: number,
|
||||
{ enumName, memberName }: { enumName: string, memberName: string },
|
||||
): void {
|
||||
this.raise(
|
||||
pos,
|
||||
`Boolean enum members need to be initialized. Use either \`${memberName} = true,\` ` +
|
||||
`or \`${memberName} = false,\` in enum \`${enumName}\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
flowEnumErrorInvalidMemberName(
|
||||
pos: number,
|
||||
{ enumName, memberName }: { enumName: string, memberName: string },
|
||||
): void {
|
||||
const suggestion = memberName[0].toUpperCase() + memberName.slice(1);
|
||||
this.raise(
|
||||
pos,
|
||||
`Enum member names cannot start with lowercase 'a' through 'z'. Instead of using ` +
|
||||
`\`${memberName}\`, consider using \`${suggestion}\`, in enum \`${enumName}\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
flowEnumErrorDuplicateMemberName(
|
||||
pos: number,
|
||||
{ enumName, memberName }: { enumName: string, memberName: string },
|
||||
): void {
|
||||
this.raise(
|
||||
pos,
|
||||
`Enum member names need to be unique, but the name \`${memberName}\` has already been used ` +
|
||||
`before in enum \`${enumName}\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
flowEnumErrorInconsistentMemberValues(
|
||||
pos: number,
|
||||
{ enumName }: { enumName: string },
|
||||
): void {
|
||||
this.raise(
|
||||
pos,
|
||||
`Enum \`${enumName}\` has inconsistent member initializers. Either use no initializers, or ` +
|
||||
`consistently use literals (either booleans, numbers, or strings) for all member initializers.`,
|
||||
);
|
||||
}
|
||||
|
||||
flowEnumErrorInvalidExplicitType(
|
||||
pos: number,
|
||||
{
|
||||
enumName,
|
||||
suppliedType,
|
||||
}: { enumName: string, suppliedType: null | string },
|
||||
): void {
|
||||
const suggestion =
|
||||
`Use one of \`boolean\`, \`number\`, \`string\`, or \`symbol\` in ` +
|
||||
`enum \`${enumName}\`.`;
|
||||
const message =
|
||||
suppliedType === null
|
||||
? `Supplied enum type is not valid. ${suggestion}`
|
||||
: `Enum type \`${suppliedType}\` is not valid. ${suggestion}`;
|
||||
this.raise(pos, message);
|
||||
}
|
||||
|
||||
flowEnumErrorInvalidMemberInitializer(
|
||||
pos: number,
|
||||
{ enumName, explicitType, memberName }: EnumContext,
|
||||
): void {
|
||||
let message = null;
|
||||
switch (explicitType) {
|
||||
case "boolean":
|
||||
case "number":
|
||||
case "string":
|
||||
message =
|
||||
`Enum \`${enumName}\` has type \`${explicitType}\`, so the initializer of ` +
|
||||
`\`${memberName}\` needs to be a ${explicitType} literal.`;
|
||||
break;
|
||||
case "symbol":
|
||||
message =
|
||||
`Symbol enum members cannot be initialized. Use \`${memberName},\` in ` +
|
||||
`enum \`${enumName}\`.`;
|
||||
break;
|
||||
default:
|
||||
// null
|
||||
message =
|
||||
`The enum member initializer for \`${memberName}\` needs to be a literal (either ` +
|
||||
`a boolean, number, or string) in enum \`${enumName}\`.`;
|
||||
}
|
||||
this.raise(pos, message);
|
||||
}
|
||||
|
||||
flowEnumErrorNumberMemberNotInitialized(
|
||||
pos: number,
|
||||
{ enumName, memberName }: { enumName: string, memberName: string },
|
||||
): void {
|
||||
this.raise(
|
||||
pos,
|
||||
`Number enum members need to be initialized, e.g. \`${memberName} = 1\` in enum \`${enumName}\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
flowEnumErrorStringMemberInconsistentlyInitailized(
|
||||
pos: number,
|
||||
{ enumName }: { enumName: string },
|
||||
): void {
|
||||
this.raise(
|
||||
pos,
|
||||
`String enum members need to consistently either all use initializers, or use no initializers, ` +
|
||||
`in enum \`${enumName}\`.`,
|
||||
);
|
||||
}
|
||||
|
||||
flowEnumMemberInit(): EnumMemberInit {
|
||||
const startPos = this.state.start;
|
||||
const endOfInit = () => this.match(tt.comma) || this.match(tt.braceR);
|
||||
switch (this.state.type) {
|
||||
case tt.num: {
|
||||
const literal = this.parseLiteral(this.state.value, "NumericLiteral");
|
||||
if (endOfInit()) {
|
||||
return { type: "number", pos: literal.start, value: literal };
|
||||
}
|
||||
return { type: "invalid", pos: startPos };
|
||||
}
|
||||
case tt.string: {
|
||||
const literal = this.parseLiteral(this.state.value, "StringLiteral");
|
||||
if (endOfInit()) {
|
||||
return { type: "string", pos: literal.start, value: literal };
|
||||
}
|
||||
return { type: "invalid", pos: startPos };
|
||||
}
|
||||
case tt._true:
|
||||
case tt._false: {
|
||||
const literal = this.parseBooleanLiteral();
|
||||
if (endOfInit()) {
|
||||
return {
|
||||
type: "boolean",
|
||||
pos: literal.start,
|
||||
value: literal,
|
||||
};
|
||||
}
|
||||
return { type: "invalid", pos: startPos };
|
||||
}
|
||||
default:
|
||||
return { type: "invalid", pos: startPos };
|
||||
}
|
||||
}
|
||||
|
||||
flowEnumMemberRaw(): { id: N.Node, init: EnumMemberInit } {
|
||||
const pos = this.state.start;
|
||||
const id = this.parseIdentifier(true);
|
||||
const init = this.eat(tt.eq)
|
||||
? this.flowEnumMemberInit()
|
||||
: { type: "none", pos };
|
||||
return { id, init };
|
||||
}
|
||||
|
||||
flowEnumCheckExplicitTypeMismatch(
|
||||
pos: number,
|
||||
context: EnumContext,
|
||||
expectedType: EnumExplicitType,
|
||||
): void {
|
||||
const { explicitType } = context;
|
||||
if (explicitType === null) {
|
||||
return;
|
||||
}
|
||||
if (explicitType !== expectedType) {
|
||||
this.flowEnumErrorInvalidMemberInitializer(pos, context);
|
||||
}
|
||||
}
|
||||
|
||||
flowEnumMembers({
|
||||
enumName,
|
||||
explicitType,
|
||||
}: {
|
||||
enumName: string,
|
||||
explicitType: EnumExplicitType,
|
||||
}): {|
|
||||
booleanMembers: Array<N.Node>,
|
||||
numberMembers: Array<N.Node>,
|
||||
stringMembers: Array<N.Node>,
|
||||
defaultedMembers: Array<N.Node>,
|
||||
|} {
|
||||
const seenNames = new Set();
|
||||
const members = {
|
||||
booleanMembers: [],
|
||||
numberMembers: [],
|
||||
stringMembers: [],
|
||||
defaultedMembers: [],
|
||||
};
|
||||
while (!this.match(tt.braceR)) {
|
||||
const memberNode = this.startNode();
|
||||
const { id, init } = this.flowEnumMemberRaw();
|
||||
const memberName = id.name;
|
||||
if (memberName === "") {
|
||||
continue;
|
||||
}
|
||||
if (/^[a-z]/.test(memberName)) {
|
||||
this.flowEnumErrorInvalidMemberName(id.start, {
|
||||
enumName,
|
||||
memberName,
|
||||
});
|
||||
}
|
||||
if (seenNames.has(memberName)) {
|
||||
this.flowEnumErrorDuplicateMemberName(id.start, {
|
||||
enumName,
|
||||
memberName,
|
||||
});
|
||||
}
|
||||
seenNames.add(memberName);
|
||||
const context = { enumName, explicitType, memberName };
|
||||
memberNode.id = id;
|
||||
switch (init.type) {
|
||||
case "boolean": {
|
||||
this.flowEnumCheckExplicitTypeMismatch(
|
||||
init.pos,
|
||||
context,
|
||||
"boolean",
|
||||
);
|
||||
memberNode.init = init.value;
|
||||
members.booleanMembers.push(
|
||||
this.finishNode(memberNode, "EnumBooleanMember"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "number": {
|
||||
this.flowEnumCheckExplicitTypeMismatch(init.pos, context, "number");
|
||||
memberNode.init = init.value;
|
||||
members.numberMembers.push(
|
||||
this.finishNode(memberNode, "EnumNumberMember"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "string": {
|
||||
this.flowEnumCheckExplicitTypeMismatch(init.pos, context, "string");
|
||||
memberNode.init = init.value;
|
||||
members.stringMembers.push(
|
||||
this.finishNode(memberNode, "EnumStringMember"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "invalid": {
|
||||
this.flowEnumErrorInvalidMemberInitializer(init.pos, context);
|
||||
break;
|
||||
}
|
||||
case "none": {
|
||||
switch (explicitType) {
|
||||
case "boolean":
|
||||
this.flowEnumErrorBooleanMemberNotInitialized(
|
||||
init.pos,
|
||||
context,
|
||||
);
|
||||
break;
|
||||
case "number":
|
||||
this.flowEnumErrorNumberMemberNotInitialized(init.pos, context);
|
||||
break;
|
||||
default:
|
||||
members.defaultedMembers.push(
|
||||
this.finishNode(memberNode, "EnumDefaultedMember"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.match(tt.braceR)) {
|
||||
this.expect(tt.comma);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
flowEnumStringBody(
|
||||
bodyNode: N.Node,
|
||||
initializedMembers: Array<N.Node>,
|
||||
defaultedMembers: Array<N.Node>,
|
||||
{ enumName }: { enumName: string },
|
||||
): N.Node {
|
||||
if (initializedMembers.length === 0) {
|
||||
bodyNode.members = defaultedMembers;
|
||||
} else if (defaultedMembers.length === 0) {
|
||||
bodyNode.members = initializedMembers;
|
||||
} else if (defaultedMembers.length > initializedMembers.length) {
|
||||
bodyNode.members = defaultedMembers;
|
||||
for (const member of initializedMembers) {
|
||||
this.flowEnumErrorStringMemberInconsistentlyInitailized(
|
||||
member.start,
|
||||
{ enumName },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
bodyNode.members = initializedMembers;
|
||||
for (const member of defaultedMembers) {
|
||||
this.flowEnumErrorStringMemberInconsistentlyInitailized(
|
||||
member.start,
|
||||
{ enumName },
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.finishNode(bodyNode, "EnumStringBody");
|
||||
}
|
||||
|
||||
flowEnumParseExplicitType({
|
||||
enumName,
|
||||
}: {
|
||||
enumName: string,
|
||||
}): EnumExplicitType {
|
||||
if (this.eatContextual("of")) {
|
||||
if (this.match(tt.name)) {
|
||||
switch (this.state.value) {
|
||||
case "boolean":
|
||||
case "number":
|
||||
case "string":
|
||||
case "symbol": {
|
||||
const explicitType = this.state.value;
|
||||
this.next();
|
||||
return explicitType;
|
||||
}
|
||||
default:
|
||||
this.flowEnumErrorInvalidExplicitType(this.state.start, {
|
||||
enumName,
|
||||
suppliedType: this.state.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.flowEnumErrorInvalidExplicitType(this.state.start, {
|
||||
enumName,
|
||||
suppliedType: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
flowParseEnumDeclaration(node: N.Node): N.Node {
|
||||
const id = this.parseIdentifier();
|
||||
node.id = id;
|
||||
const enumName = id.name;
|
||||
const explicitType = this.flowEnumParseExplicitType({ enumName });
|
||||
this.expect(tt.braceL);
|
||||
const bodyNode = this.startNode();
|
||||
const members = this.flowEnumMembers({ enumName, explicitType });
|
||||
|
||||
switch (explicitType) {
|
||||
case "boolean":
|
||||
bodyNode.explicitType = true;
|
||||
bodyNode.members = members.booleanMembers;
|
||||
node.body = this.finishNode(bodyNode, "EnumBooleanBody");
|
||||
break;
|
||||
case "number":
|
||||
bodyNode.explicitType = true;
|
||||
bodyNode.members = members.numberMembers;
|
||||
node.body = this.finishNode(bodyNode, "EnumNumberBody");
|
||||
break;
|
||||
case "string":
|
||||
bodyNode.explicitType = true;
|
||||
node.body = this.flowEnumStringBody(
|
||||
bodyNode,
|
||||
members.stringMembers,
|
||||
members.defaultedMembers,
|
||||
{ enumName },
|
||||
);
|
||||
break;
|
||||
case "symbol":
|
||||
bodyNode.members = members.defaultedMembers;
|
||||
node.body = this.finishNode(bodyNode, "EnumSymbolBody");
|
||||
break;
|
||||
default: {
|
||||
// null
|
||||
const empty = () => {
|
||||
bodyNode.members = [];
|
||||
return this.finishNode(bodyNode, "EnumStringBody");
|
||||
};
|
||||
bodyNode.explicitType = false;
|
||||
const boolsLen = members.booleanMembers.length;
|
||||
const numsLen = members.numberMembers.length;
|
||||
const strsLen = members.stringMembers.length;
|
||||
const defaultedLen = members.defaultedMembers.length;
|
||||
|
||||
if (!boolsLen && !numsLen && !strsLen && !defaultedLen) {
|
||||
node.body = empty();
|
||||
} else if (!boolsLen && !numsLen) {
|
||||
node.body = this.flowEnumStringBody(
|
||||
bodyNode,
|
||||
members.stringMembers,
|
||||
members.defaultedMembers,
|
||||
{ enumName },
|
||||
);
|
||||
} else if (!numsLen && !strsLen && boolsLen >= defaultedLen) {
|
||||
bodyNode.members = members.booleanMembers;
|
||||
node.body = this.finishNode(bodyNode, "EnumBooleanBody");
|
||||
for (const member of members.defaultedMembers) {
|
||||
this.flowEnumErrorBooleanMemberNotInitialized(member.start, {
|
||||
enumName,
|
||||
memberName: member.id.name,
|
||||
});
|
||||
}
|
||||
} else if (!boolsLen && !strsLen && numsLen >= defaultedLen) {
|
||||
bodyNode.members = members.numberMembers;
|
||||
node.body = this.finishNode(bodyNode, "EnumNumberBody");
|
||||
for (const member of members.defaultedMembers) {
|
||||
this.flowEnumErrorNumberMemberNotInitialized(member.start, {
|
||||
enumName,
|
||||
memberName: member.id.name,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
node.body = empty();
|
||||
this.flowEnumErrorInconsistentMemberValues(id.start, { enumName });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.expect(tt.braceR);
|
||||
return this.finishNode(node, "EnumDeclaration");
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user