Skip to content

Instantly share code, notes, and snippets.

@mysticatea
Last active December 14, 2019 13:10
Show Gist options
  • Save mysticatea/f3a87f3e02632797ec59d9b447fdf05e to your computer and use it in GitHub Desktop.
Save mysticatea/f3a87f3e02632797ec59d9b447fdf05e to your computer and use it in GitHub Desktop.

Context

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.

Solution

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 ]
}

Examples

// 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": [],
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment