Skip to content

Instantly share code, notes, and snippets.

@JLHwung
Created June 2, 2020 22:17
Show Gist options
  • Save JLHwung/567fb29fa2b82bbe164ad9067ff3290f to your computer and use it in GitHub Desktop.
Save JLHwung/567fb29fa2b82bbe164ad9067ff3290f to your computer and use it in GitHub Desktop.
Babel's optional chaining AST spec

Expressions

OptionalMemberExpression

interface OptionalMemberExpression <: Expression {
  type: "OptionalMemberExpression";
  object: Expression;
  property: Expression;
  computed: boolean;
  optional: boolean;
}

OptionalCallExpression

interface OptionalCallExpression <: Expression {
  type: "OptionalCallExpression";
  callee: Expression;
  arguments: [ Expression | SpreadElement ];
  optional: boolean;
}

The Optional(Call|Member)Expression aligns to the OptionalChain production. When optional is true, the property/arguments must follow a ?. token. Any member/call in the optional chain is considered optional call or optional member. That said, if a node is Optional(Call|Member)Expression, there must exist a Optional(Call|Member)Expression[optional=true] to its left that starts this optional chain.


Examples:

computed property is always false in the following examples. It can extend to computed properties w.l.o.g.

// obj?.aaa?.bbb
{
  "type": "OptionalMemberExpression",
  "object": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" },
  "optional": true
}
// obj?.aaa.bbb
{
  "type": "OptionalMemberExpression",
  "object": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" },
  "optional": false
}

bbb is a part of optional chain, so it is OptionalMemberExpression, but it does not follow a ?. token, so it has optional: false setting.

// obj.aaa?.bbb
{
  "type": "OptionalMemberExpression",
  "object": {
    "type": "MemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" }
  },
  "property": { "type": "Identifier", "name": "bbb" },
  "optional": true
}
// obj.aaa.bbb
{
  "type": "MemberExpression",
  "object": {
    "type": "MemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" }
  },
  "property": { "type": "Identifier", "name": "bbb" }
}

This proposal does not introduce changes for member expressions obj.aaa.bbb.

// (obj?.aaa)?.bbb
{
  "type": "OptionalMemberExpression",
  "object": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" },
  "optional": true
}

Both (obj?.aaa)?.bbb and obj?.aaa?.bbb shares the same AST structures because they are equivalent. Just like (obj.aaa).bbb is same as obj.aaa.bbb.

// (obj?.aaa).bbb
{
  "type": "MemberExpression",
  "object": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" }
}

However (obj?.aaa).bbb is not equivalent to obj?.aaa.bbb. Because .bbb is not a part of the optional chain, it is now a member expression.

// func?.()?.bbb
{
  "type": "OptionalMemberExpression",
  "object": {
    "type": "OptionalCallExpression",
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" },
  "optional": true
}
// func?.().bbb
{
  "type": "OptionalMemberExpression",
  "object": {
    "type": "OptionalCallExpression",
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" },
  "optional": false
}

.bbb is a part of optional chain, so it is OptionalMemberExpression, but it does not follow a ?. token, so it has optional: false setting.

// (func?.())?.bbb
{
  "type": "OptionalMemberExpression",
  "object": {
    "type": "OptionalCallExpression",
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" },
  "optional": true
}

Both (func?.())?.bbb and func?.()?.bbb shares the same AST structures because they are equivalent. Just like (func()).bbb is same as func().bbb.

// (func?.()).bbb
{
  "type": "MemberExpression",
  "object": {
    "type": "OptionalCallExpression",
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
    "optional": true
  },
  "property": { "type": "Identifier", "name": "bbb" }
}

However (func?.()).bbb is not equivalent to func?.().bbb. Because .bbb is not a part of the optional chain, it is now a member expression.

// obj?.aaa?.()
{
  "type": "OptionalCallExpression",
  "callee": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "arguments": [],
  "optional": true
}
// obj?.aaa()
{
  "type": "OptionalCallExpression",
  "callee": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "arguments": [],
  "optional": false
}

() is a part of optional chain, so it is OptionalCallExpression, but it does not follow a ?. token, so it has optional: false setting.

// (obj?.aaa)?.()
{
  "type": "OptionalCallExpression",
  "callee": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "arguments": [],
  "optional": true
}

Both (obj?.aaa)?.() and obj?.aaa?.() shares the same AST structures because they are equivalent. Just like (obj.aaa)() is same as obj.aaa().

// (obj?.aaa)()
{
  "type": "CallExpression",
  "callee": {
    "type": "OptionalMemberExpression",
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
    "optional": true
  },
  "arguments": []
}

However (obj?.aaa)() is not equivalent to obj?.aaa(). Because () is not a part of the optional chain, it is now a call expression.

@tdd
Copy link

tdd commented Jun 19, 2020

Hey @JLHwung is this to be found in Babel already? i've been trying to grab a dedicated AST from the latest @babel/standalone for optional chains but only get already-transpiled conditional expressions. I need non-transpiled ASTs for optional chains for my code to be relevant, wondering whether that's on the stove somewhere…?

@JLHwung
Copy link
Author

JLHwung commented Jun 19, 2020

The Babel AST spec is available on https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md. You can use @babel/parser to generate AST from your source code.

@tdd
Copy link

tdd commented Jun 19, 2020

Hey there!

Thank you for this quick and useful reply 😉

@tdd
Copy link

tdd commented Jun 19, 2020

So the current spec indeed doesn't include optional chains. I'm assuming you're spearheading that effort right now?

@JLHwung
Copy link
Author

JLHwung commented Jun 19, 2020

The PR of adding optional chaining to AST spec is under review: babel/babel#11729. Feel free to leave comments there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment