Skip to content

Commit 2d0d92c

Browse files
committed
add optional chaining
1 parent eec9b37 commit 2d0d92c

File tree

9 files changed

+1498
-10
lines changed

9 files changed

+1498
-10
lines changed

acorn-loose/src/expression.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,24 +159,33 @@ lp.parseExprSubscripts = function() {
159159
}
160160

161161
lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
162+
const optionalSupported = this.options.ecmaVersion >= 11
163+
let optionalChained = false
162164
for (;;) {
163165
if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine()) {
164166
if (this.tok.type === tt.dot && this.curIndent === startIndent)
165167
--startIndent
166168
else
167-
return base
169+
break
168170
}
169171

170172
let maybeAsyncArrow = base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon()
173+
let optional = optionalSupported && this.eat(tt.questionDot)
174+
if (optional) {
175+
optionalChained = true
176+
}
171177

172-
if (this.eat(tt.dot)) {
178+
if ((optional && this.tok.type !== tt.parenL && this.tok.type !== tt.bracketL && this.tok.type !== tt.backQuote) || this.eat(tt.dot)) {
173179
let node = this.startNodeAt(start)
174180
node.object = base
175181
if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine())
176182
node.property = this.dummyIdent()
177183
else
178184
node.property = this.parsePropertyAccessor() || this.dummyIdent()
179185
node.computed = false
186+
if (optionalSupported) {
187+
node.optional = optional
188+
}
180189
base = this.finishNode(node, "MemberExpression")
181190
} else if (this.tok.type === tt.bracketL) {
182191
this.pushCx()
@@ -185,6 +194,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
185194
node.object = base
186195
node.property = this.parseExpression()
187196
node.computed = true
197+
if (optionalSupported) {
198+
node.optional = optional
199+
}
188200
this.popCx()
189201
this.expect(tt.bracketR)
190202
base = this.finishNode(node, "MemberExpression")
@@ -195,16 +207,26 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
195207
let node = this.startNodeAt(start)
196208
node.callee = base
197209
node.arguments = exprList
210+
if (optionalSupported) {
211+
node.optional = optional
212+
}
198213
base = this.finishNode(node, "CallExpression")
199214
} else if (this.tok.type === tt.backQuote) {
200215
let node = this.startNodeAt(start)
201216
node.tag = base
202217
node.quasi = this.parseTemplate()
203218
base = this.finishNode(node, "TaggedTemplateExpression")
204219
} else {
205-
return base
220+
break
206221
}
207222
}
223+
224+
if (optionalChained) {
225+
const chainNode = this.startNodeAt(start)
226+
chainNode.expression = base
227+
base = this.finishNode(chainNode, "ChainExpression")
228+
}
229+
return base
208230
}
209231

210232
lp.parseExprAtom = function() {

acorn-walk/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ base.Program = base.BlockStatement = (node, st, c) => {
192192
}
193193
base.Statement = skipThrough
194194
base.EmptyStatement = ignore
195-
base.ExpressionStatement = base.ParenthesizedExpression =
195+
base.ExpressionStatement = base.ParenthesizedExpression = base.ChainExpression =
196196
(node, st, c) => c(node.expression, st, "Expression")
197197
base.IfStatement = (node, st, c) => {
198198
c(node.test, st, "Expression")

acorn/src/expression.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,29 +273,47 @@ pp.parseSubscripts = function(base, startPos, startLoc, noCalls) {
273273
let maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" &&
274274
this.lastTokEnd === base.end && !this.canInsertSemicolon() && base.end - base.start === 5 &&
275275
this.potentialArrowAt === base.start
276+
let optionalChained = false
277+
276278
while (true) {
277-
let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow)
278-
if (element === base || element.type === "ArrowFunctionExpression") return element
279+
let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained)
280+
281+
if (element.optional) optionalChained = true
282+
if (element === base || element.type === "ArrowFunctionExpression") {
283+
if (optionalChained) {
284+
const chainNode = this.startNodeAt(startPos, startLoc)
285+
chainNode.expression = element
286+
element = this.finishNode(chainNode, "ChainExpression")
287+
}
288+
return element
289+
}
290+
279291
base = element
280292
}
281293
}
282294

283-
pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) {
295+
pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained) {
296+
let optional = this.options.ecmaVersion >= 11 && this.eat(tt.questionDot)
297+
if (noCalls && optional) this.raise(this.lastTokStart, "Optional chaining cannot appear in the callee of new expressions")
298+
284299
let computed = this.eat(tt.bracketL)
285-
if (computed || this.eat(tt.dot)) {
300+
if (computed || (optional && this.type !== tt.parenL && this.type !== tt.backQuote) || this.eat(tt.dot)) {
286301
let node = this.startNodeAt(startPos, startLoc)
287302
node.object = base
288303
node.property = computed ? this.parseExpression() : this.parseIdent(this.options.allowReserved !== "never")
289304
node.computed = !!computed
290305
if (computed) this.expect(tt.bracketR)
306+
if (this.options.ecmaVersion >= 11) {
307+
node.optional = optional
308+
}
291309
base = this.finishNode(node, "MemberExpression")
292310
} else if (!noCalls && this.eat(tt.parenL)) {
293311
let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos
294312
this.yieldPos = 0
295313
this.awaitPos = 0
296314
this.awaitIdentPos = 0
297315
let exprList = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors)
298-
if (maybeAsyncArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
316+
if (maybeAsyncArrow && !optional && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
299317
this.checkPatternErrors(refDestructuringErrors, false)
300318
this.checkYieldAwaitInDefaultParams()
301319
if (this.awaitIdentPos > 0)
@@ -312,8 +330,14 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow)
312330
let node = this.startNodeAt(startPos, startLoc)
313331
node.callee = base
314332
node.arguments = exprList
333+
if (this.options.ecmaVersion >= 11) {
334+
node.optional = optional
335+
}
315336
base = this.finishNode(node, "CallExpression")
316337
} else if (this.type === tt.backQuote) {
338+
if (optional || optionalChained) {
339+
this.raise(this.start, "Optional chaining cannot appear in the tag of tagged template expressions")
340+
}
317341
let node = this.startNodeAt(startPos, startLoc)
318342
node.tag = base
319343
node.quasi = this.parseTemplate({isTagged: true})

acorn/src/lval.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ pp.toAssignable = function(node, isBinding, refDestructuringErrors) {
7373
this.toAssignable(node.expression, isBinding, refDestructuringErrors)
7474
break
7575

76+
case "ChainExpression":
77+
this.raiseRecoverable(node.start, "Optional chaining cannot appear in left-hand side")
78+
break
79+
7680
case "MemberExpression":
7781
if (!isBinding) break
7882

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

208+
case "ChainExpression":
209+
this.raiseRecoverable(expr.start, "Optional chaining cannot appear in left-hand side")
210+
break
211+
204212
case "MemberExpression":
205213
if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression")
206214
break

acorn/src/tokenize.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ pp.readToken_eq_excl = function(code) { // '=!'
292292
pp.readToken_question = function() { // '?'
293293
if (this.options.ecmaVersion >= 11) {
294294
let next = this.input.charCodeAt(this.pos + 1)
295+
if (next === 46) {
296+
let next2 = this.input.charCodeAt(this.pos + 2)
297+
if (next2 < 48 || next2 > 57) return this.finishOp(tt.questionDot, 2)
298+
}
295299
if (next === 63) return this.finishOp(tt.coalesce, 2)
296300
}
297301
return this.finishOp(tt.question, 1)

acorn/src/tokentype.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const types = {
7070
colon: new TokenType(":", beforeExpr),
7171
dot: new TokenType("."),
7272
question: new TokenType("?", beforeExpr),
73+
questionDot: new TokenType("?."),
7374
arrow: new TokenType("=>", beforeExpr),
7475
template: new TokenType("template"),
7576
invalidTemplate: new TokenType("invalidTemplate"),

bin/run_test262.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const unsupportedFeatures = [
1111
"class-static-fields-public",
1212
"class-static-methods-private",
1313
"numeric-separator-literal",
14-
"optional-chaining",
1514
];
1615

1716
run(

test/run.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
require("./tests-export-all-as-ns-from-source.js");
2121
require("./tests-import-meta.js");
2222
require("./tests-nullish-coalescing.js");
23+
require("./tests-optional-chaining.js");
2324
var acorn = require("../acorn")
2425
var acorn_loose = require("../acorn-loose")
2526

0 commit comments

Comments
 (0)