Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Optional Chaining #891

Merged
merged 1 commit into from
Jun 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
mysticatea marked this conversation as resolved.
Show resolved Hide resolved
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