Like the above, trying to represent short-circuit behavior by a binary tree is failed because the object
property of leaf nodes is lost. Therefore, we need a new node to represent the chain of member accesses and function calls.
Introducing ChainingExpression
node to represent chaining and it represents member accesses and function calls. It starts with ChainingExpression#base
expression and applies ChainingExpression#chain
to the base
as similar to Array#reduce
. MemberChain
node corresponds to MemberExpression
and CallChain
node corresponds to CallExpression
. If an optional chain is found while applying and the base
of that time is nullish, it stops the evaluation and returns undefined
.
For backward compatibility, non-optional chains in the left of the first optional chain are represented by the existing nodes (MemberExpression
and CallExpression
). The new ChainingExpression
represents the first optional chain and the right of it with MemberChain
and CallChain
.
interface ChainingExpression extends Expression {
type: "ChainingExpression"
base: Expression
chain: [ Chain ] //< requires one element at least
}
interface Chain extends Node {
optional: boolean
}
interface MemberChain extends Chain {
type: "MemberChain"
computed: boolean
property: Expression
}
interface CallChain extends Chain {
type: "CallChain"
arguments: [ Expression ]
}
// obj?.aaa?.bbb
{
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
},
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "bbb" },
}
],
}
// obj?.aaa.bbb
{
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
},
{
"type": "MemberChain",
"optional": false,
"computed": false,
"property": { "type": "Identifier", "name": "bbb" },
}
],
}
// obj.aaa?.bbb
{
"type": "ChainingExpression",
"base": {
// For backward compatibility, non-optional chains in the left of the first
// optional chain are represented by the existing nodes.
"type": "MemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
},
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "bbb" },
}
],
}
// obj.aaa.bbb
{
// For backward compatibility, non-optional chains in the left of the first
// optional chain are represented by the existing nodes.
"type": "MemberExpression",
"object": {
"type": "MemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
},
"property": { "type": "Identifier", "name": "bbb" },
}
// (obj?.aaa)?.bbb
{
// Parentheses disconnect chains. Therefore, short-circuit behavior doesn't
// propagate.
"type": "ChainingExpression",
"base": {
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
}
],
},
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "bbb" },
}
],
}
// (obj?.aaa).bbb
{
// Parentheses disconnect chains. Therefore, short-circuit behavior doesn't
// propagate.
"type": "MemberExpression",
"object": {
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
}
],
},
"property": { "type": "Identifier", "name": "bbb" },
}
// func?.()?.bbb
{
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "func" },
"chain": [
{
"type": "CallChain",
"optional": true,
"arguments": [],
},
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "bbb" },
}
],
}
// func?.().bbb
{
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "func" },
"chain": [
{
"type": "CallChain",
"optional": true,
"arguments": [],
},
{
"type": "MemberChain",
"optional": false,
"computed": false,
"property": { "type": "Identifier", "name": "bbb" },
}
],
}
// (func?.())?.bbb
{
// Parentheses disconnect chains. Therefore, short-circuit behavior doesn't
// propagate.
"type": "ChainingExpression",
"base": {
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "func" },
"chain": [
{
"type": "CallChain",
"optional": true,
"arguments": [],
}
],
},
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "bbb" },
}
],
}
// (func?.()).bbb
{
// Parentheses disconnect chains. Therefore, short-circuit behavior doesn't
// propagate.
"type": "MemberExpression",
"object": {
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "func" },
"chain": [
{
"type": "CallChain",
"optional": true,
"arguments": [],
}
],
},
"property": { "type": "Identifier", "name": "bbb" },
}
// obj?.aaa?.()
{
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
},
{
"type": "CallChain",
"optional": true,
"arguments": [],
}
],
}
// obj?.aaa()
{
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
},
{
"type": "CallChain",
"optional": false,
"arguments": [],
}
],
}
// (obj?.aaa)?.()
{
// Parentheses disconnect chains. Therefore, short-circuit behavior doesn't
// propagate.
"type": "ChainingExpression",
"base": {
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
}
],
},
"chain": [
{
"type": "CallChain",
"optional": true,
"arguments": [],
}
],
}
// (obj?.aaa)()
{
// Parentheses disconnect chains. Therefore, short-circuit behavior doesn't
// propagate.
"type": "CallExpression",
"base": {
"type": "ChainingExpression",
"base": { "type": "Identifier", "name": "obj" },
"chain": [
{
"type": "MemberChain",
"optional": true,
"computed": false,
"property": { "type": "Identifier", "name": "aaa" },
}
],
},
"arguments": [],
}