Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional chaining #204

Merged
merged 4 commits into from Jun 11, 2020
Merged

Add optional chaining #204

merged 4 commits into from Jun 11, 2020

Conversation

mysticatea
Copy link
Contributor

@mysticatea mysticatea commented Nov 20, 2019

This is a draft to update es2020.md for the TC39 meeting in Dec 2019. The meeting contains the following items on its agenda (https://github.com/tc39/agendas/blob/master/2019/12.md).

  • Optional Chaining for Stage 4
  • Nullish Coalescing for Stage 4

Therefore, this PR contains the AST proposal of those three syntaxes. If any of them didn't arrive at Stage 4, I will remove those from this PR.

(Because we needed over 2 months to merge a PR in the previous time, I wanted to start discussion earlier.)


Optional Chaining:

This proposal is inspired by #146 (comment). It adds two properties optional and shortCircuited to two existing nodes CallExpression and MemberExpression.

The following examples describe how the two properties represent the syntax.

In the following code blocks, the first comment is the source code and the rest is the AST of the code.

// obj?.aaa?.bbb
{
  "type": "MemberExpression",
  "optional": true,     // If `obj.aaa` was nullish, this node is evaluated to undefined.
  "shortCircuited": true, // If `obj` was nullish, this node is short-circuited.
  "object": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// obj?.aaa.bbb
{
  "type": "MemberExpression",
  "optional": false,    // If `obj.aaa` was nullish, this node is evaluated to throwing TypeError.
  "shortCircuited": true, // If `obj` was nullish, this node is short-circuited.
  "object": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// (obj?.aaa)?.bbb
{
  "type": "MemberExpression",
  "optional": true,      // If `obj.aaa` was nullish, this node is evaluated to undefined.
  "shortCircuited": false, // This node isn't short-circuited.
  "object": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// (obj?.aaa).bbb
{
  "type": "MemberExpression",
  "optional": false,     // If `obj.aaa` was nullish, this node is evaluated to throwing TypeError.
  "shortCircuited": false, // This node isn't short-circuited.
  "object": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// func?.()?.bbb
{
  "type": "MemberExpression",
  "optional": true,     // If the result of `func()` was nullish, this node is evaluated to undefined.
  "shortCircuited": true, // If `func` was nullish, this node is short-circuited.
  "object": {
    "type": "CallExpression",
    "optional": true,      // If `func` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// func?.().bbb
{
  "type": "MemberExpression",
  "optional": false,    // If the result of `func()` was nullish, this node is evaluated to throwing TypeError.
  "shortCircuited": true, // If `func` was nullish, this node is short-circuited.
  "object": {
    "type": "CallExpression",
    "optional": true,      // If `func` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// (func?.())?.bbb
{
  "type": "MemberExpression",
  "optional": true,      // If the result of `func()` was nullish, this node is evaluated to undefined.
  "shortCircuited": false, // This node isn't short-circuited.
  "object": {
    "type": "CallExpression",
    "optional": true,      // If `func` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// (func?.()).bbb
{
  "type": "MemberExpression",
  "optional": false,     // If the result of `func()` was nullish, this node is evaluated to throwing TypeError.
  "shortCircuited": false, // This node isn't short-circuited.
  "object": {
    "type": "CallExpression",
    "optional": true,      // If `func` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "callee": { "type": "Identifier", "name": "func" },
    "arguments": [],
  },
  "property": { "type": "Identifier", "name": "bbb" },
}
// obj?.aaa?.()
{
  "type": "CallExpression",
  "optional": true,     // If `obj.aaa` was nullish, this node is evaluated to undefined.
  "shortCircuited": true, // If `obj` was nullish, this node is short-circuited.
  "callee": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "arguments": [],
}
// obj?.aaa()
{
  "type": "CallExpression",
  "optional": false,    // If `obj.aaa` was nullish, this node is evaluated to throwing TypeError.
  "shortCircuited": true, // If `obj` was nullish, this node is short-circuited.
  "callee": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "arguments": [],
}
// (obj?.aaa)?.()
{
  "type": "CallExpression",
  "optional": true,      // If `obj.aaa` was nullish, this node is evaluated to undefined.
  "shortCircuited": false, // This node isn't short-circuited.
  "callee": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "arguments": [],
}
// (obj?.aaa)()
{
  "type": "CallExpression",
  "optional": false,     // If `obj.aaa` was nullish, this node is evaluated to throwing TypeError.
  "shortCircuited": false, // This node isn't short-circuited.
  "callee": {
    "type": "MemberExpression",
    "optional": true,      // If `obj` was nullish, this node is evaluated to undefined.
    "shortCircuited": false, // This node isn't short-circuited.
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" },
  },
  "arguments": [],
}

I don't see the advantages of introducing new node types such as OptionalMemberExpression because:

  • the optional chaining has short-circuit behavior for the right side and parentheses can cancel the short-circuit behavior. Therefore, the related nodes must have the properties of optional chaining regardless of optional or not.
  • the new syntax inherits the existing syntax. Even if tools that don't know the new syntax ignored the optional and shortCircuited properties, I think that the behavior will be not so bad.

Nullish Coalescing:

In #203, we look to have a consensus that we should represent the syntax by LogicalExpression. This PR follows it.


This PR closes #146, #153, and #203.

@mysticatea
Copy link
Contributor Author

mysticatea commented Dec 5, 2019

@michaelficarra
Copy link
Member

I don't like this representation of optional chaining at all. I prefer @devsnek's design.

Even if tools that don't know the new syntax ignored the optional and shortCircuited properties, I think that the behavior will be not so bad

I do not value this at all. In fact, I think that is a negative aspect of this design. I do not want my tools to fail in a surprising and quiet way. I want them to fail fast and loudly.

@mysticatea
Copy link
Contributor Author

I agree that optional and shortCircuited properties are an ugly representation. But I thought that it's an only representation we can keep backward compatibility about existing MemberExpression and CallExpression.

If it's acceptable that we use completely different representation for MemberExpression and CallExpression in short-circuited locations, the @devsnek's design is a better one.

Let's see opinions from other members.

@mysticatea
Copy link
Contributor Author

For reference, the @devsnek's design is the following (if my understanding is correct):

interface OptionalExpression <: Expression {
    type: "OptionalExpression"
    object: Expression
    chain: OptionalChain
}

interface OptionalChain <: Node {
    type: "OptionalChain"
    base: OptionalChain
}

interface OptionalChainProperty <: OptionalChain {
    property: Expression
    computed: boolean
}

interface OptionalChainCall <: OptionalChain {
    arguments: [ Expression ]
}

Therefore, MemberExpression and CallExpression syntaxes in short-circuited locations are represented as the new OptionalChain node.

@devsnek
Copy link

devsnek commented Dec 11, 2019

OptionalExpression is basically separating the normal member expression part from the optional chain part. OptionalChain is basically MemberExpression except that it also deals with the disjunct start by using a null base. ?.a.b.c is actually:

OptionalChain {
  base: OptionalChain {
    base: OptionalChain {
      base: null,
      property: a,
    },
    property: b,
  },
  property: c
}

Some examples:

a.b?.c

OptionalExpression {
  object: MemberExpression {
    object: a,
    property: b,
  },
  chain: OptionalChain {
    base: null,
    property: c,
  },
}
a.b?.c.d?.e.f

OptionalExpression {
  object: OptionalExpression {
    object: MemberExpression {
      object: a,
      property: b,
    },
    chain: OptionalChain {
      base: OptionalChain {
        base: null,
        property: c,
      },
      property: d,
    },
  },
  chain: OptionalChain {
    base: OptionalChain {
      base: null,
      property: e,
    },
    property: f,
  },
}
a?.(x, y, z)

OptionalExpression {
  object: a,
  chain: OptionalChain {
    base: null,
    arguments: [x, y, z],
  },
}

You can see some more examples and a complete parsing implementation here: https://github.com/engine262/engine262/blob/21fb9197453f4d0f5509df498c79b65fc2c94a81/src/parse.mjs#L142-L250

Also, you can just call me "devsnek" instead of "the devsnek" :)

@michaelficarra
Copy link
Member

But I thought that it's an only representation we can keep backward compatibility about existing MemberExpression and CallExpression.

I don't want any kind of "compatibility" between them. They are different, unrelated concepts. Any tooling that operated on an optional member access as if it was a member access would risk silently breaking: the worst kind of breaking.

@mysticatea
Copy link
Contributor Author

@devsnek

OptionalExpression

By the way, I like an array instead of the linked list of base property for OptionalExpression#chain. We need to handle the first one in special in the current form. What do you think?

Also, you can just call me "devsnek" instead of "the devsnek" :)

I'm sorry. Articles are difficult for me because my primary language doesn't have that concept. (my intention was like "the design of devsnek.")

@michaelficarra

About compatibility, I'm talking between non-optional member accesses inside/outside of short-circuited places. For example, in a.b?.c.d case, both the first dot and the last dot should be MemberExpression in the ideal. If the left and right of ?. have different representations, I feel odd. Such a syntax doesn't exist that has different representations in the left and right of || in spite of short-circuited.

@devsnek
Copy link

devsnek commented Dec 12, 2019

@mysticatea that would allow an empty array, which is not a valid tree. i realize this already exists for comma op, but imo a good AST can not represent invalid trees.

@mysticatea
Copy link
Contributor Author

@devsnek There are VariableDeclaration#declarations, TemplateLiteral#quasis, and ImportDeclaration#specifiers as well. Not a rare case.

...in that case, how about moving OptionalExpression#object to the first OptionalChain#base?

@devsnek
Copy link

devsnek commented Dec 12, 2019

how about moving OptionalExpression#object to the first OptionalChain#base?

that would be difficult (or even impossible) to deal with for ast evaluators. i think it might also break the associativity of expressions of multiple chains.

@mysticatea
Copy link
Contributor Author

@devsnek Thank you for your answers.

Hmm. I still like an array for chain because the base property is incomplete to represent object/callee. ESTree has several arrays that need an element at least, so I don't think it's a big problem.


In my first impression, I thought that the OptionalExpression#chain property contains all short-circuited parts, but it doesn't look so. Therefore, for example, obj?.a?.b and (obj?.a)?.b has the same tree. I think that the two have different semantics (whether the second member access is skipped or not), but it seems no problem because the results of those are the same. (really?)

@mysticatea
Copy link
Contributor Author

After I considered, I lean toward to keeping this PR as-is.

I like devsnek's design, too. However, I have two concerns on it. (1) It's a severe concern that non-optional member accesses and function calls are getting two different representations. For example, obj?.foo() is not CallExpression. The last dot of obj?.foo.bar is not MemberExpression. (2) It's a small concern that the AST cannot distinguish obj?.a?.b and (obj?.a)?.b that have different semantics.

@devsnek
Copy link

devsnek commented Dec 14, 2019

I don't care if you specifically use my design or not, but please do not overload the existing AST types. perhaps introduce an OptionalMemberExpression or something.

I'd also like to mention that the current design is basically impossible for analysis based on evaluation or control flow, since the break in the chain is at the bottom instead of the top, which is what my design fixes. The design babel chose is basically only good if you're trying to generate branches (jumps) as a byproduct of the AST, not use the AST itself.

Also as far as I understand, obj?.a?.b and (obj?.a)?.b are operationally equivalent (and would be literally the same in estree, since it lacks parenthesized expressions).

@mysticatea
Copy link
Contributor Author

please do not overload the existing AST types. perhaps introduce an OptionalMemberExpression or something.

In ESTree, it's very common cases that the flags of existing nodes represent additive semantics. For example,

  • MemberExpression#computed represents computed member accesses.
  • FunctionDeclaration#generator represents generator functions.
  • FunctionDeclaration#async represents async functions.
  • ForOfStatement#await represents for-await-of loops.
  • Property#shorthand and Property#method represent shorthand property notations.

Because the optional member access is still member access, I think proper that MemberExpression represents it with a flag.

I think that we need breaking changes around MemberExpression and CallExpression to represent optional chaining properly. However, ESTree's philosophy doesn't allow it. To fork the representation of member accesses and function calls at ?. is a solution, but I'm not sure if it's acceptable. I really want to ask the decision of the ESTree participating members.

I'd also like to mention that the current design is basically impossible for analysis based on evaluation or control flow, since the break in the chain is at the bottom instead of the top, which is what my design fixes.

It needs to skip the evaluation of RHS until shortCircuited property is false if short-circuited happened. I guess that it needs a state in the evaluator.

Also as far as I understand, obj?.a?.b and (obj?.a)?.b are operationally equivalent (and would be literally the same in estree, since it lacks parenthesized expressions).

As this section. In obj?.a?.b case, if obj is nullish then all of ?.a?.b part are skipped. On the other hand, in (obj?.a)?.b case, if obj is nullish then only ?.a part is skipped and the second ?. is evaluated regardless of obj value. As you mentioned, the result is probably the same, so it's a small concern. The gap might raise problem in the future.

@devsnek
Copy link

devsnek commented Dec 14, 2019

well in any case, i'd said what i hope the design is. if i could only choose thing, it would be that the chains are split at the top of the ast, not from the bottom.

@mysticatea
Copy link
Contributor Author

I thought a modified version of devsnek's design: https://gist.github.com/mysticatea/f3a87f3e02632797ec59d9b447fdf05e. It should resolve my second concern (obj?.a?.b vs. (obj?.a)?.b). The ChainingExpression can replace MemberExpression and CallExpression but doesn't replace those in the left of ?. for backward compatibility.

@julianjensen
Copy link

It looks to me that this commit doesn't quite follow the final functionality of the proposal in this line:

extend interface CallExpression <: OptionalChainLink {}

since that would appear to add optional chaining to a new expression, which the proposal as written in stage 4, explicitly does not support: Proposal Optional Chaining # Not supported

Maybe changing the line to

extend interface SimpleCallExpression <: OptionalChainLink {}

would bring it in line with the proposal.

@mysticatea
Copy link
Contributor Author

@julianjensen I'm guessing you are talking about another spec than ESTree because SimpleCallExpression doesn't exist in ESTree.

@mysticatea
Copy link
Contributor Author

@dherman @RReverser @adrianheine @nzakas @ariya @michaelficarra @hzoo @loganfsmyth @danez Would you review this PR? Especially, there are two proposals about optional chaining. I'd like to ask for the direction we should go.

  1. New optional and shortCircuited properties.
  2. New ChainingExpression node.

The former is minimal changes, but it represents the short-circuit behavior in an ugly way. We have to traverse ancestor nodes to know the place of the end of the short-circuiting. (In other words, the tree doesn't represent the short-circuit behavior.)

The latter represents the short-circuit behavior in a better way, but it forks the representation of member accesses and function calls. Those two semantics have two kinds of representations in the left/right of ?. (MemberExpressionMemberChain and CallExpressionCallChain).

@mysticatea
Copy link
Contributor Author

@bradzacher
Copy link

One thing I would like clarification on here: what's the problem with going with babel's representation?
In typescript-eslint, we've implemented the same AST, and have been using it since October. I haven't run into any issues with the usage of the AST in rules, and I think it's pretty clear and self explanatory.

You mention:

the optional chaining has short-circuit behavior for the right side and parentheses can cancel the short-circuit behavior. Therefore, the related nodes must have the properties of optional chaining regardless of optional or not.

In particular

Therefore, the related nodes must have the properties of optional chaining regardless of optional or not.

What do you mean by this? Why does a **Expression need to have an optional property?
If it's part of an optional chain, then it's an Optional**Expression node. Otherwise it's not. The non-optional types do not have an optional property.

Correct me if I'm wrong, but isn't this (a?.b).c the same as ab = a?.b; ab.c? I.e. an optional chain wrapped in parens is treated as if it were resolved separately.
If that's right, then I don't understand why a parent should be given extra properties in this case - it's not actually part of an optional chain, so why is it influenced by it?

Similarly, from the POV of an AST, why does the "optional: true, shortCircuited: true" need to be encoded in the AST?


Current solution has 5 different permutations:

  • **Expression, optional: true, shortCircuited: true
    • "has a ?., and there is a ?. higher up the chain"
  • **Expression, optional: true, shortCircuited: false
    • "has a ?., but there is no ?. higher up the chain"
  • **Expression, optional: false, shortCircuited: true
    • "there is a ?. higher up the chain"
  • **Expression, optional: false, shortCircuited: false
    • "there is a ?. higher up the chain, but it is surrounded with parentheses"
  • **Expression, optional: undefined, shortCircuited: undefined
    • "not part of an optional chain"

babel's(/typescript-eslint's) solution has 3:

  • *Expression (in typescript-estree, also always has optional: false)
    • "not part of an optional chain"
  • Optional**Expression, optional: true
    • "has a ?." (and there may be a ?. higher up the chain)
  • Optional**Expression, optional: false
    • "there is a ?. higher up the chain"

@mysticatea
Copy link
Contributor Author

What do you mean by this? Why does a **Expression need to have an optional property?
If it's part of an optional chain, then it's an Optional**Expression node. Otherwise it's not. The non-optional types do not have an optional property.

Correct me if I'm wrong, but isn't this (a?.b).c the same as ab = a?.b; ab.c? I.e. an optional chain wrapped in parens is treated as if it were resolved separately.
If that's right, then I don't understand why a parent should be given extra properties in this case - it's not actually part of an optional chain, so why is it influenced by it?

See those four examples. In the "New optional and shortCircuited properties" version, the AST cannot represent short-circuit behavior by binary tree: the RHS of the operators (. and ?.) is always only a property name regardless of short-circuit behavior. Therefore, **Expression nodes must have a flag property for short-circuit behavior. See also the "Context" section of the "New ChainingExpression node" version. This means that Optional**Expression nodes cannot represent ?. syntax properly. It needs the help of the flag property of **Expression nodes.

Also, the ?. syntax just provides an additive behavior along with . syntax. For such situations, ESTree has used flag properties. For example:

  • FunctionDeclaration#generator represents generator functions.
  • FunctionDeclaration#async represents async functions.
  • ForOfStatement#await represents for-await-of loops.
  • Property#computed represents computed property.

Therefore, I believe that two flag properties (optional and shortCircuited) are more proper than new AST types (Optional**Expression).

Similarly, from the POV of an AST, why does the "optional: true, shortCircuited: true" need to be encoded in the AST?

Current solution has 5 different permutations:

I don't think there are 5 different permutations. There are 4 different permutations because two boolean flags have 2 bits information.

  • optional: false means the node throws a TypeError if the object (or callee) is nullish.
  • optional: true means the node doesn't throw a TypeError even if the object (or callee) is nullish.
  • shortCircuited: false means the node isn't short-circuited by the subtree of AST.
  • shortCircuited: true means the node is short-circuited by the subtree of AST.

It doesn't need other patterns.

@mysticatea
Copy link
Contributor Author

I updated this PR for the latest discussion.

Copy link
Contributor

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Let's ship this.

Copy link
Member

@RReverser RReverser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me too, as already said above :) Waiting on response from Babel side.

@JLHwung
Copy link
Contributor

JLHwung commented Jun 11, 2020

We have discussed it in today's meeting. We, the Babel team, backs the current version.

We are committed to support current version on babel-eslint. However, we are not going to revise Babel AST given that both AST design correctly covers the syntax and the cost of such breaking change outweighs the benefit.

Note that in current approach, to determine whether .bbb is an optional chain element in obj?.aaa.bbb.ccc, we have to traverse to the root of the optional chain and see if it is a ChainExpression, unlike Babel AST where we can determine by the type of .bbb. This may introduce performance overhead.

I think we have reached consensus and it should be merged as-is. Thanks for all the fish.

@RReverser
Copy link
Member

We have discussed it in today's meeting. We, the Babel team, backs the current version.

Looks like you linked to the document without optional chaining - is that a mistake?

However, we are not going to revise Babel AST given that both AST design correctly covers the syntax and the cost of such breaking change outweighs the benefit.

Oh, that makes sense, I don't think anyone suggested to modify Babel AST. The consensus and changes in this repo is specifically for ESTree; whether Babel can and/or wants to modify Babel AST as well in response to any such changes, is entirely up to the Babel team.


Thanks for the approval!

@mysticatea
Copy link
Contributor Author

Thank you for the review and approval. I updated this PR to fix "where are" → "that are".

@nzakas
Copy link
Contributor

nzakas commented Jun 11, 2020

It looks like consensus has been reached, so merging. Thanks everyone!

@jridgewell
Copy link

Note that in current approach, to determine whether .bbb is an optional chain element in obj?.aaa.bbb.ccc, we have to traverse to the root of the optional chain and see if it is a ChainExpression, unlike Babel AST where we can determine by the type of .bbb. This may introduce performance overhead.

When Babel originally implemented this, one of the suggestions was to use a trinary for the optional field. This would allow us to detect whether we're in an optional chain or a regular member chain with a single check, instead of depth-1 checks.

// obj.aaa.bbb
{
  "type": "MemberExpression",
  "optional": null, // regular member expression
  "object": {
    "type": "MemberExpression",
    "optional": null, // regular member expression
    "object": { "type": "Identifier", "name": "obj" },
    "property": { "type": "Identifier", "name": "aaa" }
  },
  "property": { "type": "Identifier", "name": "bbb" }
}
// obj?.aaa.bbb
{
  "type": "ChainExpression",
  "expression": {
    "type": "MemberExpression",
    "optional": false, // part of a optional chain
    "object": {
      "type": "MemberExpression",
      "optional": true,
      "object": { "type": "Identifier", "name": "obj" },
      "property": { "type": "Identifier", "name": "aaa" }
    },
    "property": { "type": "Identifier", "name": "bbb" }
  }
}
// (obj?.aaa).bbb
{
  "type": "MemberExpression",
  "optional": null, // not part of the chain
  "object": {
    "type": "ChainExpression",
    "expression": {
      "type": "MemberExpression",
      "optional": true,
      "object": { "type": "Identifier", "name": "obj" },
      "property": { "type": "Identifier", "name": "aaa" }
    }
  },
  "property": { "type": "Identifier", "name": "bbb" }
}

@RReverser
Copy link
Member

I just want to say special thanks to @mysticatea who explored and patiently adjusted a lot of variations for this proposal in response to the feedback. Thanks a lot!

@sevor005
Copy link

Good afternoon! Have you added support for optional chaining to eslint? If not added, which version will support it? Thanks!

@TheOptimisticFactory
Copy link

@sevor005 It landed in eslint v7.5.0 - July 18, 2020 through eslint/eslint#13416

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Nullish coalescing operator ?? Optional Chaining Operator