Update coalesce precedence (#11017)

* refactor: reimplement nullish coalescing precedence tracking

Co-authored-by: Toru Nagashima <public@mysticatea.dev>

* fix: Coalesce has same precedence with LogicalOR

* fix flow errors

Co-authored-by: Toru Nagashima <public@mysticatea.dev>
This commit is contained in:
Huáng Jùnliàng 2020-01-17 14:57:07 -05:00 committed by Nicolò Ribaudo
parent 6648d62f40
commit 45301c5304
7 changed files with 43 additions and 60 deletions

View File

@ -320,7 +320,7 @@ export default class ExpressionParser extends LValParser {
minPrec: number, minPrec: number,
noIn: ?boolean, noIn: ?boolean,
): N.Expression { ): N.Expression {
const prec = this.state.type.binop; let prec = this.state.type.binop;
if (prec != null && (!noIn || !this.match(tt._in))) { if (prec != null && (!noIn || !this.match(tt._in))) {
if (prec > minPrec) { if (prec > minPrec) {
const operator = this.state.value; const operator = this.state.value;
@ -343,11 +343,17 @@ export default class ExpressionParser extends LValParser {
} }
const op = this.state.type; const op = this.state.type;
const logical = op === tt.logicalOR || op === tt.logicalAND;
const coalesce = op === tt.nullishCoalescing;
if (op === tt.pipeline) { if (op === tt.pipeline) {
this.expectPlugin("pipelineOperator"); this.expectPlugin("pipelineOperator");
this.state.inPipeline = true; this.state.inPipeline = true;
this.checkPipelineAtInfixOperator(left, leftStartPos); this.checkPipelineAtInfixOperator(left, leftStartPos);
} else if (coalesce) {
// Handle the precedence of `tt.coalesce` as equal to the range of logical expressions.
// In other words, `node.right` shouldn't contain logical expressions in order to check the mixed error.
prec = ((tt.logicalAND: any): { binop: number }).binop;
} }
this.next(); this.next();
@ -369,48 +375,25 @@ export default class ExpressionParser extends LValParser {
} }
node.right = this.parseExprOpRightExpr(op, prec, noIn); node.right = this.parseExprOpRightExpr(op, prec, noIn);
/* this check is for all ?? operators
* a ?? b && c for this example
* b && c => This is considered as a logical expression in the ast tree
* a => Identifier
* so for ?? operator we need to check in this case the right expression to have parenthesis
* second case a && b ?? c
* here a && b => This is considered as a logical expression in the ast tree
* c => identifier
* so now here for ?? operator we need to check the left expression to have parenthesis
* if the parenthesis is missing we raise an error and throw it
*/
if (op === tt.nullishCoalescing) {
if (
left.type === "LogicalExpression" &&
left.operator !== "??" &&
!(left.extra && left.extra.parenthesized)
) {
throw this.raise(
left.start,
`Nullish coalescing operator(??) requires parens when mixing with logical operators`,
);
} else if (
node.right.type === "LogicalExpression" &&
node.right.operator !== "??" &&
!(node.right.extra && node.right.extra.parenthesized)
) {
throw this.raise(
node.right.start,
`Nullish coalescing operator(??) requires parens when mixing with logical operators`,
);
}
}
this.finishNode( this.finishNode(
node, node,
op === tt.logicalOR || logical || coalesce ? "LogicalExpression" : "BinaryExpression",
op === tt.logicalAND ||
op === tt.nullishCoalescing
? "LogicalExpression"
: "BinaryExpression",
); );
/* this check is for all ?? operators
* a ?? b && c for this example
* when op is coalesce and nextOp is logical (&&), throw at the pos of nextOp that it can not be mixed.
* Symmetrically it also throws when op is logical and nextOp is coalesce
*/
const nextOp = this.state.type;
if (
(coalesce && (nextOp === tt.logicalOR || nextOp === tt.logicalAND)) ||
(logical && nextOp === tt.nullishCoalescing)
) {
throw this.raise(
this.state.start,
`Nullish coalescing operator(??) requires parens when mixing with logical operators`,
);
}
return this.parseExprOp( return this.parseExprOp(
node, node,

View File

@ -139,22 +139,22 @@ export const types: { [name: string]: TokenType } = {
tilde: new TokenType("~", { beforeExpr, prefix, startsExpr }), tilde: new TokenType("~", { beforeExpr, prefix, startsExpr }),
pipeline: createBinop("|>", 0), pipeline: createBinop("|>", 0),
nullishCoalescing: createBinop("??", 1), nullishCoalescing: createBinop("??", 1),
logicalOR: createBinop("||", 2), logicalOR: createBinop("||", 1),
logicalAND: createBinop("&&", 3), logicalAND: createBinop("&&", 2),
bitwiseOR: createBinop("|", 4), bitwiseOR: createBinop("|", 3),
bitwiseXOR: createBinop("^", 5), bitwiseXOR: createBinop("^", 4),
bitwiseAND: createBinop("&", 6), bitwiseAND: createBinop("&", 5),
equality: createBinop("==/!=/===/!==", 7), equality: createBinop("==/!=/===/!==", 6),
relational: createBinop("</>/<=/>=", 8), relational: createBinop("</>/<=/>=", 7),
bitShift: createBinop("<</>>/>>>", 9), bitShift: createBinop("<</>>/>>>", 8),
plusMin: new TokenType("+/-", { beforeExpr, binop: 10, prefix, startsExpr }), plusMin: new TokenType("+/-", { beforeExpr, binop: 9, prefix, startsExpr }),
// startsExpr: required by v8intrinsic plugin // startsExpr: required by v8intrinsic plugin
modulo: new TokenType("%", { beforeExpr, binop: 11, startsExpr }), modulo: new TokenType("%", { beforeExpr, binop: 10, startsExpr }),
star: createBinop("*", 11), star: createBinop("*", 10),
slash: createBinop("/", 11), slash: createBinop("/", 10),
exponent: new TokenType("**", { exponent: new TokenType("**", {
beforeExpr, beforeExpr,
binop: 12, binop: 11,
rightAssociative: true, rightAssociative: true,
}), }),
@ -189,8 +189,8 @@ export const types: { [name: string]: TokenType } = {
_null: createKeyword("null", { startsExpr }), _null: createKeyword("null", { startsExpr }),
_true: createKeyword("true", { startsExpr }), _true: createKeyword("true", { startsExpr }),
_false: createKeyword("false", { startsExpr }), _false: createKeyword("false", { startsExpr }),
_in: createKeyword("in", { beforeExpr, binop: 8 }), _in: createKeyword("in", { beforeExpr, binop: 7 }),
_instanceof: createKeyword("instanceof", { beforeExpr, binop: 8 }), _instanceof: createKeyword("instanceof", { beforeExpr, binop: 7 }),
_typeof: createKeyword("typeof", { beforeExpr, prefix, startsExpr }), _typeof: createKeyword("typeof", { beforeExpr, prefix, startsExpr }),
_void: createKeyword("void", { beforeExpr, prefix, startsExpr }), _void: createKeyword("void", { beforeExpr, prefix, startsExpr }),
_delete: createKeyword("delete", { beforeExpr, prefix, startsExpr }), _delete: createKeyword("delete", { beforeExpr, prefix, startsExpr }),

View File

@ -457,7 +457,7 @@
"isAssign": false, "isAssign": false,
"prefix": true, "prefix": true,
"postfix": false, "postfix": false,
"binop": 10, "binop": 9,
"updateContext": null "updateContext": null
}, },
"value": "+", "value": "+",

View File

@ -1,4 +1,4 @@
{ {
"plugins": ["estree"], "plugins": ["estree"],
"throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:0)" "throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:7)"
} }

View File

@ -1,3 +1,3 @@
{ {
"throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:5)" "throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:7)"
} }

View File

@ -1,3 +1,3 @@
{ {
"throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:10)" "throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:12)"
} }

View File

@ -1,4 +1,4 @@
{ {
"plugins": ["estree"], "plugins": ["estree"],
"throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:0)" "throws": "Nullish coalescing operator(??) requires parens when mixing with logical operators (1:7)"
} }