Skip to content

Commit

Permalink
add optional chaining
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea authored and marijnh committed Jun 11, 2020
1 parent eec9b37 commit 4adea30
Show file tree
Hide file tree
Showing 9 changed files with 1,499 additions and 10 deletions.
28 changes: 25 additions & 3 deletions acorn-loose/src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,24 +159,33 @@ lp.parseExprSubscripts = function() {
}

lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
const optionalSupported = this.options.ecmaVersion >= 11
let optionalChained = false
for (;;) {
if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine()) {
if (this.tok.type === tt.dot && this.curIndent === startIndent)
--startIndent
else
return base
break
}

let maybeAsyncArrow = base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon()
let optional = optionalSupported && this.eat(tt.questionDot)
if (optional) {
optionalChained = true
}

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 +194,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,16 +207,26 @@ 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)
node.tag = base
node.quasi = this.parseTemplate()
base = this.finishNode(node, "TaggedTemplateExpression")
} else {
return base
break
}
}

if (optionalChained) {
const chainNode = this.startNodeAt(start)
chainNode.expression = base
base = this.finishNode(chainNode, "ChainExpression")
}
return base
}

lp.parseExprAtom = function() {
Expand Down
2 changes: 1 addition & 1 deletion acorn-walk/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ base.Program = base.BlockStatement = (node, st, c) => {
}
base.Statement = skipThrough
base.EmptyStatement = ignore
base.ExpressionStatement = base.ParenthesizedExpression =
base.ExpressionStatement = base.ParenthesizedExpression = base.ChainExpression =
(node, st, c) => c(node.expression, st, "Expression")
base.IfStatement = (node, st, c) => {
c(node.test, st, "Expression")
Expand Down
35 changes: 30 additions & 5 deletions acorn/src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,29 +273,48 @@ 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
let optionalChained = false

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

if (element.optional) optionalChained = true
if (element === base || element.type === "ArrowFunctionExpression") {
if (optionalChained) {
const chainNode = this.startNodeAt(startPos, startLoc)
chainNode.expression = element
element = this.finishNode(chainNode, "ChainExpression")
}
return element
}

base = element
}
}

pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) {
pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained) {
let optionalSupported = this.options.ecmaVersion >= 11
let optional = optionalSupported && this.eat(tt.questionDot)
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 (optionalSupported) {
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 +331,14 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow)
let node = this.startNodeAt(startPos, startLoc)
node.callee = base
node.arguments = exprList
if (optionalSupported) {
node.optional = optional
}
base = this.finishNode(node, "CallExpression")
} else if (this.type === tt.backQuote) {
if (optional || optionalChained) {
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
8 changes: 8 additions & 0 deletions acorn/src/lval.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ pp.toAssignable = function(node, isBinding, refDestructuringErrors) {
this.toAssignable(node.expression, isBinding, refDestructuringErrors)
break

case "ChainExpression":
this.raiseRecoverable(node.start, "Optional chaining cannot appear in left-hand side")
break

case "MemberExpression":
if (!isBinding) break

Expand Down Expand Up @@ -201,6 +205,10 @@ pp.checkLVal = function(expr, bindingType = BIND_NONE, checkClashes) {
if (bindingType !== BIND_NONE && bindingType !== BIND_OUTSIDE) this.declareName(expr.name, bindingType, expr.start)
break

case "ChainExpression":
this.raiseRecoverable(expr.start, "Optional chaining cannot appear in left-hand side")
break

case "MemberExpression":
if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression")
break
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.questionDot, 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 @@ -70,6 +70,7 @@ export const types = {
colon: new TokenType(":", beforeExpr),
dot: new TokenType("."),
question: new TokenType("?", beforeExpr),
questionDot: new TokenType("?."),
arrow: new TokenType("=>", beforeExpr),
template: new TokenType("template"),
invalidTemplate: new TokenType("invalidTemplate"),
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 4adea30

Please sign in to comment.