Skip to content

Commit 61ad097

Browse files
committed
add optional chaining
1 parent eec9b37 commit 61ad097

File tree

8 files changed

+1435
-5
lines changed

8 files changed

+1435
-5
lines changed

acorn-loose/src/expression.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ lp.parseExprSubscripts = function() {
159159
}
160160

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

171+
// Wrap the `base` node by a `ChainExpression` node to disconnect the optional chaining
172+
// if all of the following conditions are true:
173+
// - The next node is a `(Call|Member)Expression` node as well (check the current token).
174+
// - The `base` node is parenthesized.
175+
// - The `base` node is a `(Call|Member)Expression` node.
176+
// - The `base` node can be short-circuited by optional chaining.
177+
if (
178+
optionalSupported &&
179+
(this.toks.type === tt.dot || this.toks.type === tt.optionalChaining || (!noCalls && this.toks.type === tt.parenL))
180+
) {
181+
if (base.end !== this.toks.lastTokEnd && this.toks.isOptionalChained(base)) {
182+
base = this.toks.createChainExpressionNode(base)
183+
} else if (base.type === "ParenthesizedExpression" && this.toks.isOptionalChained(base.expression)) {
184+
base.expression = this.toks.createChainExpressionNode(base.expression)
185+
}
186+
}
187+
170188
let maybeAsyncArrow = base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon()
189+
let optional = optionalSupported && this.eat(tt.optionalChaining)
171190

172-
if (this.eat(tt.dot)) {
191+
if ((optional && this.tok.type !== tt.parenL && this.tok.type !== tt.bracketL && this.tok.type !== tt.backQuote) || this.eat(tt.dot)) {
173192
let node = this.startNodeAt(start)
174193
node.object = base
175194
if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine())
176195
node.property = this.dummyIdent()
177196
else
178197
node.property = this.parsePropertyAccessor() || this.dummyIdent()
179198
node.computed = false
199+
if (optionalSupported) {
200+
node.optional = optional
201+
}
180202
base = this.finishNode(node, "MemberExpression")
181203
} else if (this.tok.type === tt.bracketL) {
182204
this.pushCx()
@@ -185,6 +207,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
185207
node.object = base
186208
node.property = this.parseExpression()
187209
node.computed = true
210+
if (optionalSupported) {
211+
node.optional = optional
212+
}
188213
this.popCx()
189214
this.expect(tt.bracketR)
190215
base = this.finishNode(node, "MemberExpression")
@@ -195,6 +220,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) {
195220
let node = this.startNodeAt(start)
196221
node.callee = base
197222
node.arguments = exprList
223+
if (optionalSupported) {
224+
node.optional = optional
225+
}
198226
base = this.finishNode(node, "CallExpression")
199227
} else if (this.tok.type === tt.backQuote) {
200228
let node = this.startNodeAt(start)

acorn/src/expression.js

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,29 +273,66 @@ 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+
277+
// Wrap the `base` node by a `ChainExpression` node to disconnect the optional chaining
278+
// if all of the following conditions are true:
279+
// - The next node is a `(Call|Member)Expression` node as well (check the current token).
280+
// - The `base` node is parenthesized.
281+
// - The `base` node is a `(Call|Member)Expression` node.
282+
// - The `base` node can be short-circuited by optional chaining.
283+
if (
284+
this.options.ecmaVersion >= 11 &&
285+
(this.type === tt.dot || this.type === tt.optionalChaining || (!noCalls && this.type === tt.parenL))
286+
) {
287+
if (base.end !== this.lastTokEnd && this.isOptionalChained(base)) {
288+
base = this.createChainExpressionNode(base)
289+
} else if (base.type === "ParenthesizedExpression" && this.isOptionalChained(base.expression)) {
290+
base.expression = this.createChainExpressionNode(base.expression)
291+
}
292+
}
293+
276294
while (true) {
277295
let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow)
278296
if (element === base || element.type === "ArrowFunctionExpression") return element
279297
base = element
280298
}
281299
}
282300

301+
pp.createChainExpressionNode = function(expression) {
302+
const {start, end, loc} = expression
303+
let startLoc, endLoc
304+
if (loc) {
305+
startLoc = loc.start
306+
endLoc = loc.end
307+
}
308+
309+
const node = this.startNodeAt(start, startLoc)
310+
node.expression = expression
311+
return this.finishNodeAt(node, "ChainExpression", end, endLoc)
312+
}
313+
283314
pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) {
315+
let optional = this.options.ecmaVersion >= 11 && this.eat(tt.optionalChaining)
316+
if (noCalls && optional) this.raise(this.lastTokStart, "Optional chaining cannot appear in the callee of new expressions")
317+
284318
let computed = this.eat(tt.bracketL)
285-
if (computed || this.eat(tt.dot)) {
319+
if (computed || (optional && this.type !== tt.parenL && this.type !== tt.backQuote) || this.eat(tt.dot)) {
286320
let node = this.startNodeAt(startPos, startLoc)
287321
node.object = base
288322
node.property = computed ? this.parseExpression() : this.parseIdent(this.options.allowReserved !== "never")
289323
node.computed = !!computed
290324
if (computed) this.expect(tt.bracketR)
325+
if (this.options.ecmaVersion >= 11) {
326+
node.optional = optional
327+
}
291328
base = this.finishNode(node, "MemberExpression")
292329
} else if (!noCalls && this.eat(tt.parenL)) {
293330
let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos
294331
this.yieldPos = 0
295332
this.awaitPos = 0
296333
this.awaitIdentPos = 0
297334
let exprList = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors)
298-
if (maybeAsyncArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
335+
if (maybeAsyncArrow && !optional && !this.canInsertSemicolon() && this.eat(tt.arrow)) {
299336
this.checkPatternErrors(refDestructuringErrors, false)
300337
this.checkYieldAwaitInDefaultParams()
301338
if (this.awaitIdentPos > 0)
@@ -312,8 +349,14 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow)
312349
let node = this.startNodeAt(startPos, startLoc)
313350
node.callee = base
314351
node.arguments = exprList
352+
if (this.options.ecmaVersion >= 11) {
353+
node.optional = optional
354+
}
315355
base = this.finishNode(node, "CallExpression")
316356
} else if (this.type === tt.backQuote) {
357+
if (optional || (base.end === this.lastTokEnd && this.isOptionalChained(base))) {
358+
this.raise(this.start, "Optional chaining cannot appear in the tag of tagged template expressions")
359+
}
317360
let node = this.startNodeAt(startPos, startLoc)
318361
node.tag = base
319362
node.quasi = this.parseTemplate({isTagged: true})
@@ -322,6 +365,28 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow)
322365
return base
323366
}
324367

368+
pp.isOptionalChained = function(node) {
369+
if (this.options.ecmaVersion >= 11) {
370+
while (true) {
371+
switch (node.type) {
372+
case "CallExpression":
373+
if (node.optional) return true
374+
node = node.callee
375+
break
376+
377+
case "MemberExpression":
378+
if (node.optional) return true
379+
node = node.object
380+
break
381+
382+
default:
383+
return false
384+
}
385+
}
386+
}
387+
return false
388+
}
389+
325390
// Parse an atomic expression — either a single token that is an
326391
// expression, an expression started by a keyword like `function` or
327392
// `new`, or an expression wrapped in punctuation like `()`, `[]`,

acorn/src/lval.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ pp.toAssignable = function(node, isBinding, refDestructuringErrors) {
7474
break
7575

7676
case "MemberExpression":
77-
if (!isBinding) break
77+
if (isBinding) this.raise(node.start, "Assigning to rvalue")
78+
if (this.isOptionalChained(node)) {
79+
this.raise(node.start, "Optional chaining cannot appear in left-hand side")
80+
}
81+
break
7882

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

204208
case "MemberExpression":
205209
if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression")
210+
if (this.isOptionalChained(expr)) {
211+
this.raise(expr.start, "Optional chaining cannot appear in left-hand side")
212+
}
206213
break
207214

208215
case "ObjectPattern":

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.optionalChaining, 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
@@ -76,6 +76,7 @@ export const types = {
7676
ellipsis: new TokenType("...", beforeExpr),
7777
backQuote: new TokenType("`", startsExpr),
7878
dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}),
79+
optionalChaining: new TokenType("?."),
7980

8081
// Operators. These carry several kinds of properties to help the
8182
// parser use them properly (the presence of these properties is

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)