Skip to content

Commit

Permalink
add optional chaining
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea committed Jun 6, 2020
1 parent eec9b37 commit 61ad097
Show file tree
Hide file tree
Showing 8 changed files with 1,435 additions and 5 deletions.
30 changes: 29 additions & 1 deletion acorn-loose/src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ lp.parseExprSubscripts = function() {
}

lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
const optionalSupported = this.options.ecmaVersion >= 11
for (;;) {
if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine()) {
if (this.tok.type === tt.dot && this.curIndent === startIndent)
Expand All @@ -167,16 +168,37 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
return base
}

// Wrap the `base` node by a `ChainExpression` node to disconnect the optional chaining
// if all of the following conditions are true:
// - The next node is a `(Call|Member)Expression` node as well (check the current token).
// - The `base` node is parenthesized.
// - The `base` node is a `(Call|Member)Expression` node.
// - The `base` node can be short-circuited by optional chaining.
if (
optionalSupported &&
(this.toks.type === tt.dot || this.toks.type === tt.optionalChaining || (!noCalls && this.toks.type === tt.parenL))
) {
if (base.end !== this.toks.lastTokEnd && this.toks.isOptionalChained(base)) {
base = this.toks.createChainExpressionNode(base)
} else if (base.type === "ParenthesizedExpression" && this.toks.isOptionalChained(base.expression)) {
base.expression = this.toks.createChainExpressionNode(base.expression)
}
}

let maybeAsyncArrow = base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon()
let optional = optionalSupported && this.eat(tt.optionalChaining)

if (this.eat(tt.dot)) {
if ((optional && this.tok.type !== tt.parenL && this.tok.type !== tt.bracketL && this.tok.type !== tt.backQuote) || this.eat(tt.dot)) {
let node = this.startNodeAt(start)
node.object = base
if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine())
node.property = this.dummyIdent()
else
node.property = this.parsePropertyAccessor() || this.dummyIdent()
node.computed = false
if (optionalSupported) {
node.optional = optional
}
base = this.finishNode(node, "MemberExpression")
} else if (this.tok.type === tt.bracketL) {
this.pushCx()
Expand All @@ -185,6 +207,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
node.object = base
node.property = this.parseExpression()
node.computed = true
if (optionalSupported) {
node.optional = optional
}
this.popCx()
this.expect(tt.bracketR)
base = this.finishNode(node, "MemberExpression")
Expand All @@ -195,6 +220,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
let node = this.startNodeAt(start)
node.callee = base
node.arguments = exprList
if (optionalSupported) {
node.optional = optional
}
base = this.finishNode(node, "CallExpression")
} else if (this.tok.type === tt.backQuote) {
let node = this.startNodeAt(start)
Expand Down
69 changes: 67 additions & 2 deletions acorn/src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,29 +273,66 @@ pp.parseSubscripts = function(base, startPos, startLoc, noCalls) {
let maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" &&
this.lastTokEnd === base.end && !this.canInsertSemicolon() && base.end - base.start === 5 &&
this.potentialArrowAt === base.start

// Wrap the `base` node by a `ChainExpression` node to disconnect the optional chaining
// if all of the following conditions are true:
// - The next node is a `(Call|Member)Expression` node as well (check the current token).
// - The `base` node is parenthesized.
// - The `base` node is a `(Call|Member)Expression` node.
// - The `base` node can be short-circuited by optional chaining.
if (
this.options.ecmaVersion >= 11 &&
(this.type === tt.dot || this.type === tt.optionalChaining || (!noCalls && this.type === tt.parenL))
) {
if (base.end !== this.lastTokEnd && this.isOptionalChained(base)) {
base = this.createChainExpressionNode(base)
} else if (base.type === "ParenthesizedExpression" && this.isOptionalChained(base.expression)) {
base.expression = this.createChainExpressionNode(base.expression)
}
}

while (true) {
let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow)
if (element === base || element.type === "ArrowFunctionExpression") return element
base = element
}
}

pp.createChainExpressionNode = function(expression) {
const {start, end, loc} = expression
let startLoc, endLoc
if (loc) {
startLoc = loc.start
endLoc = loc.end
}

const node = this.startNodeAt(start, startLoc)
node.expression = expression
return this.finishNodeAt(node, "ChainExpression", end, endLoc)
}

pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) {
let optional = this.options.ecmaVersion >= 11 && this.eat(tt.optionalChaining)
if (noCalls && optional) this.raise(this.lastTokStart, "Optional chaining cannot appear in the callee of new expressions")

let computed = this.eat(tt.bracketL)
if (computed || this.eat(tt.dot)) {
if (computed || (optional && this.type !== tt.parenL && this.type !== tt.backQuote) || this.eat(tt.dot)) {
let node = this.startNodeAt(startPos, startLoc)
node.object = base
node.property = computed ? this.parseExpression() : this.parseIdent(this.options.allowReserved !== "never")
node.computed = !!computed
if (computed) this.expect(tt.bracketR)
if (this.options.ecmaVersion >= 11) {
node.optional = optional
}
base = this.finishNode(node, "MemberExpression")
} else if (!noCalls && this.eat(tt.parenL)) {
let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos
this.yieldPos = 0
this.awaitPos = 0
this.awaitIdentPos = 0
let exprList = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors)
if (maybeAsyncArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
if (maybeAsyncArrow && !optional && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
this.checkPatternErrors(refDestructuringErrors, false)
this.checkYieldAwaitInDefaultParams()
if (this.awaitIdentPos > 0)
Expand All @@ -312,8 +349,14 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow)
let node = this.startNodeAt(startPos, startLoc)
node.callee = base
node.arguments = exprList
if (this.options.ecmaVersion >= 11) {
node.optional = optional
}
base = this.finishNode(node, "CallExpression")
} else if (this.type === tt.backQuote) {
if (optional || (base.end === this.lastTokEnd && this.isOptionalChained(base))) {
this.raise(this.start, "Optional chaining cannot appear in the tag of tagged template expressions")
}
let node = this.startNodeAt(startPos, startLoc)
node.tag = base
node.quasi = this.parseTemplate({isTagged: true})
Expand All @@ -322,6 +365,28 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow)
return base
}

pp.isOptionalChained = function(node) {
if (this.options.ecmaVersion >= 11) {
while (true) {
switch (node.type) {
case "CallExpression":
if (node.optional) return true
node = node.callee
break

case "MemberExpression":
if (node.optional) return true
node = node.object
break

default:
return false
}
}
}
return false
}

// Parse an atomic expression — either a single token that is an
// expression, an expression started by a keyword like `function` or
// `new`, or an expression wrapped in punctuation like `()`, `[]`,
Expand Down
9 changes: 8 additions & 1 deletion acorn/src/lval.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ pp.toAssignable = function(node, isBinding, refDestructuringErrors) {
break

case "MemberExpression":
if (!isBinding) break
if (isBinding) this.raise(node.start, "Assigning to rvalue")
if (this.isOptionalChained(node)) {
this.raise(node.start, "Optional chaining cannot appear in left-hand side")
}
break

default:
this.raise(node.start, "Assigning to rvalue")
Expand Down Expand Up @@ -203,6 +207,9 @@ pp.checkLVal = function(expr, bindingType = BIND_NONE, checkClashes) {

case "MemberExpression":
if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression")
if (this.isOptionalChained(expr)) {
this.raise(expr.start, "Optional chaining cannot appear in left-hand side")
}
break

case "ObjectPattern":
Expand Down
4 changes: 4 additions & 0 deletions acorn/src/tokenize.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ pp.readToken_eq_excl = function(code) { // '=!'
pp.readToken_question = function() { // '?'
if (this.options.ecmaVersion >= 11) {
let next = this.input.charCodeAt(this.pos + 1)
if (next === 46) {
let next2 = this.input.charCodeAt(this.pos + 2)
if (next2 < 48 || next2 > 57) return this.finishOp(tt.optionalChaining, 2)
}
if (next === 63) return this.finishOp(tt.coalesce, 2)
}
return this.finishOp(tt.question, 1)
Expand Down
1 change: 1 addition & 0 deletions acorn/src/tokentype.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const types = {
ellipsis: new TokenType("...", beforeExpr),
backQuote: new TokenType("`", startsExpr),
dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}),
optionalChaining: new TokenType("?."),

// Operators. These carry several kinds of properties to help the
// parser use them properly (the presence of these properties is
Expand Down
1 change: 0 additions & 1 deletion bin/run_test262.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const unsupportedFeatures = [
"class-static-fields-public",
"class-static-methods-private",
"numeric-separator-literal",
"optional-chaining",
];

run(
Expand Down
1 change: 1 addition & 0 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
require("./tests-export-all-as-ns-from-source.js");
require("./tests-import-meta.js");
require("./tests-nullish-coalescing.js");
require("./tests-optional-chaining.js");
var acorn = require("../acorn")
var acorn_loose = require("../acorn-loose")

Expand Down
Loading

0 comments on commit 61ad097

Please sign in to comment.