Skip to content

Commit

Permalink
add optional chaining
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea committed Dec 9, 2019
1 parent 9675cfa commit 184b9d1
Show file tree
Hide file tree
Showing 8 changed files with 1,150 additions and 8 deletions.
20 changes: 19 additions & 1 deletion acorn-loose/src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ lp.parseExprSubscripts = function() {
}

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

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
node.shortCircuited = shortCircuited
if (optional) shortCircuited = true
}
base = this.finishNode(node, "MemberExpression")
} else if (this.tok.type === tt.bracketL) {
this.pushCx()
Expand All @@ -185,6 +193,11 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
node.object = base
node.property = this.parseExpression()
node.computed = true
if (optionalSupported) {
node.optional = optional
node.shortCircuited = shortCircuited
if (optional) shortCircuited = true
}
this.popCx()
this.expect(tt.bracketR)
base = this.finishNode(node, "MemberExpression")
Expand All @@ -195,6 +208,11 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
let node = this.startNodeAt(start)
node.callee = base
node.arguments = exprList
if (optionalSupported) {
node.optional = optional
node.shortCircuited = shortCircuited
if (optional) shortCircuited = true
}
base = this.finishNode(node, "CallExpression")
} else if (this.tok.type === tt.backQuote) {
let node = this.startNodeAt(start)
Expand Down
24 changes: 20 additions & 4 deletions acorn/src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,29 +260,38 @@ pp.parseExprSubscripts = function(refDestructuringErrors) {
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() && this.input.slice(base.start, base.end) === "async"
let shortCircuited = false
while (true) {
let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow)
let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, shortCircuited)
if (this.options.ecmaVersion >= 11 && element.optional) shortCircuited = true
if (element === base || element.type === "ArrowFunctionExpression") return element
base = element
}
}

pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) {
pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow, shortCircuited) {
let optional = this.options.ecmaVersion >= 11 && this.eat(tt.optionalChaining)
if (noCalls && optional) this.raiseRecoverable(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 ((optional && this.type !== tt.parenL && this.type !== tt.backQuote) || computed || 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
node.shortCircuited = !!shortCircuited
}
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 @@ -299,8 +308,15 @@ 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
node.shortCircuited = !!shortCircuited
}
base = this.finishNode(node, "CallExpression")
} else if (this.type === tt.backQuote) {
if (optional || shortCircuited) {
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 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.options.ecmaVersion >= 11 && (node.optional || node.shortCircuited)) {
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.options.ecmaVersion >= 11 && (expr.optional || expr.shortCircuited)) {
this.raise(expr.start, "Optional chaining cannot appear in left-hand side")
}
break

case "ObjectPattern":
Expand Down
15 changes: 14 additions & 1 deletion acorn/src/tokenize.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,17 @@ pp.readToken_eq_excl = function(code) { // '=!'
return this.finishOp(code === 61 ? tt.eq : tt.prefix, 1)
}

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)
}
}
return this.finishOp(tt.question, 1)
}

pp.getTokenFromCode = function(code) {
switch (code) {
// The interpretation of a dot depends on whether it is followed
Expand All @@ -306,7 +317,6 @@ pp.getTokenFromCode = function(code) {
case 123: ++this.pos; return this.finishToken(tt.braceL)
case 125: ++this.pos; return this.finishToken(tt.braceR)
case 58: ++this.pos; return this.finishToken(tt.colon)
case 63: ++this.pos; return this.finishToken(tt.question)

case 96: // '`'
if (this.options.ecmaVersion < 6) break
Expand Down Expand Up @@ -356,6 +366,9 @@ pp.getTokenFromCode = function(code) {
case 61: case 33: // '=!'
return this.readToken_eq_excl(code)

case 63: // '?'
return this.readToken_question()

case 126: // '~'
return this.finishOp(tt.prefix, 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 @@ -14,7 +14,6 @@ const unsupportedFeatures = [
"export-star-as-namespace-from-module",
"import.meta",
"numeric-separator-literal",
"optional-chaining",
"top-level-await"
];

Expand Down
1 change: 1 addition & 0 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require("./tests-optional-catch-binding.js");
require("./tests-bigint.js");
require("./tests-dynamic-import.js");
require("./tests-optional-chaining.js");
var acorn = require("../acorn")
var acorn_loose = require("../acorn-loose")

Expand Down
Loading

0 comments on commit 184b9d1

Please sign in to comment.