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": 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": [],
}