Optional Chaining Operator (Stage 1) #5813

Merged
merged 33 commits into from Jun 27, 2017

Conversation

@jridgewell
Member

jridgewell commented Jun 2, 2017

Example: https://babeljs.io/repl/build/master#?babili=false&browsers=&build=&builtIns=false&code_lz=IYfgdARg3AUDqTAYzAE1vcFnnXBA2gOQREC6shEFmYAFAJRRA&debug=false&circleciRepo=&evaluate=false&lineWrap=false&presets=stage-0&prettier=false&targets=&version=7.0.0-beta.2


Q A
Patch: Bug Fix? No
Major: Breaking Change? No
Minor: New Feature? Yes
Deprecations?
Spec Compliancy?
Tests Added/Pass? Yes
Fixed Tickets Closes #5786
License MIT
Doc PR
Dependency Changes

This is another implementation of Null Propagation Operators, competing with #5786. This one takes a different, top-down approach to the transform that avoids the WeakSet state. It also implements optional CallExpressions and NewExpressions.

This has a few todos:

  • Split transform into two plugins
    1. Syntax enabler (the manipulateOptions stuff)
    2. The transform
  • There's a "fixer" for the WIP parser.
    • Optional CallExpressions don't have a callee
    • Optional NewExpressions have a callee that's a CallExpression. That callee's callee and arguments should be the NewExpressions callee and arguments
  • Update babel-generator printer

@jridgewell jridgewell changed the title from Pr/5786 to Null Propagation Operators Jun 2, 2017

@hzoo
  • readme update with example and link to proposal
  • does babel-types need changing?
  • do we want some simple exec tests? (could be basis for test262 as well)
@jridgewell

This comment has been minimized.

Show comment
Hide comment
@jridgewell

jridgewell Jun 4, 2017

Member

@hzoo: done. Please advise whether I have all the necessary LHS parents we need to guard.

Member

jridgewell commented Jun 4, 2017

@hzoo: done. Please advise whether I have all the necessary LHS parents we need to guard.

@xtuc

This comment has been minimized.

Show comment
Hide comment
@xtuc

xtuc Jun 5, 2017

Member

Nice job @jridgewell!

  • In the spec, they are talking about a "special Reference" called nil. Could we explicitly use it in the plugin's code? Like in my plugin here.

I'm ok with closing my PR.

Link to parser PR babel/babylon#545.

Member

xtuc commented Jun 5, 2017

Nice job @jridgewell!

  • In the spec, they are talking about a "special Reference" called nil. Could we explicitly use it in the plugin's code? Like in my plugin here.

I'm ok with closing my PR.

Link to parser PR babel/babylon#545.

@hzoo hzoo requested review from gisenberg and xtuc Jun 5, 2017

@jridgewell

This comment has been minimized.

Show comment
Hide comment
@jridgewell

jridgewell Jun 5, 2017

Member

Done.

Sorry about the duplicate PRs. I usually open one against the original PR (like the parser one), but this implementation was significantly different.

Member

jridgewell commented Jun 5, 2017

Done.

Sorry about the duplicate PRs. I usually open one against the original PR (like the parser one), but this implementation was significantly different.

@hzoo hzoo referenced this pull request Jun 5, 2017

Closed

Null Propagation Operator #5786

0 of 2 tasks complete

@hzoo hzoo changed the title from Null Propagation Operators to Optional Chaining Operator (Stage 1) Jun 5, 2017

@jridgewell jridgewell referenced this pull request in babel/babylon Jun 5, 2017

Merged

Optional Chaining: Stage 1 plugin #545

1 of 1 task complete

@hzoo hzoo referenced this pull request in babel/babylon Jun 5, 2017

Closed

Optional Chaining: Stage 1 #328

+const baz = obj?.foo?.bar?.baz(); // 42
+
+const safe = obj?.qux?.baz(); // undefined
+const safe2 = obj?.foo.bar.qux?.(); // undefined

This comment has been minimized.

@ljharb

ljharb Jun 5, 2017

can we add an example like obj?.foo.bar.bazz() that throws?

@ljharb

ljharb Jun 5, 2017

can we add an example like obj?.foo.bar.bazz() that throws?

This comment has been minimized.

@jridgewell

jridgewell Jun 6, 2017

Member

Done.

+
+const baz = new obj?.foo?.bar?.baz(); // baz instance
+
+const safe = new obj?.qux?.baz(); // undefined

This comment has been minimized.

@ljharb

ljharb Jun 5, 2017

I don't think this should return undefined (obv that depends on the proposal itself).

I think that if you want the new not to throw when it gets a non-function, either the new or the () must have the conditional operator attached to it.

In other words, new obj?.qux?.baz() should be identical to const foo = obj?.qux?.baz; new foo().

@ljharb

ljharb Jun 5, 2017

I don't think this should return undefined (obv that depends on the proposal itself).

I think that if you want the new not to throw when it gets a non-function, either the new or the () must have the conditional operator attached to it.

In other words, new obj?.qux?.baz() should be identical to const foo = obj?.qux?.baz; new foo().

This comment has been minimized.

+ optionals.push(node);
+ }
+
+ objectPath = objectPath.get("object");

This comment has been minimized.

@Kovensky

Kovensky Jun 6, 2017

Member
for (let objectPath = path.get(key); objectPath.isMemberExpression(); objectPath = objectPath.get("object"))
@Kovensky

Kovensky Jun 6, 2017

Member
for (let objectPath = path.get(key); objectPath.isMemberExpression(); objectPath = objectPath.get("object"))

This comment has been minimized.

@jridgewell

jridgewell Jun 6, 2017

Member

Done.

+ objectPath = objectPath.get("object");
+ }
+
+ for (let i = optionals.length - 1; i >= 0; i--) {

This comment has been minimized.

@Kovensky

Kovensky Jun 6, 2017

Member
for (const [i, node] of optionals.entries())

or

optionals.forEach((node, i) => {})
@Kovensky

Kovensky Jun 6, 2017

Member
for (const [i, node] of optionals.entries())

or

optionals.forEach((node, i) => {})

This comment has been minimized.

@jridgewell

jridgewell Jun 6, 2017

Member

Can't, I have to reverse iterate.

@jridgewell

jridgewell Jun 6, 2017

Member

Can't, I have to reverse iterate.

This comment has been minimized.

@Kovensky

Kovensky Jun 6, 2017

Member

Oh, right. .reverse() could do the trick, but the i would have to be subtracted from the length 🤔

@Kovensky

Kovensky Jun 6, 2017

Member

Oh, right. .reverse() could do the trick, but the i would have to be subtracted from the length 🤔

+ if (atCall && t.isMemberExpression(chain)) {
+ if (loose) {
+ // To avoid a Function#call, we can instead re-grab the property from the context object.
+ // `a.?b.?()` translates roughly to `_a.b != null && _a.b()`

This comment has been minimized.

@Kovensky

Kovensky Jun 6, 2017

Member

That would invoke a getter twice though...

@Kovensky

Kovensky Jun 6, 2017

Member

That would invoke a getter twice though...

This comment has been minimized.

@jridgewell

jridgewell Jun 6, 2017

Member

Yup, that's why it's loose. 😉

@jridgewell

jridgewell Jun 6, 2017

Member

Yup, that's why it's loose. 😉

+};
+
+let ab = new obj?.a?.b(1);
+assert(ab instanceof obj.a.b);

This comment has been minimized.

@Kovensky

Kovensky Jun 6, 2017

Member

Hm... if this was an optional path, this could result in instanceof void 0, which is a runtime error. Maybe something to support in the spec, or just leave it as an error? cc/ @gisenberg

@Kovensky

Kovensky Jun 6, 2017

Member

Hm... if this was an optional path, this could result in instanceof void 0, which is a runtime error. Maybe something to support in the spec, or just leave it as an error? cc/ @gisenberg

@@ -416,7 +416,7 @@ defineType("LogicalExpression", {
});
defineType("MemberExpression", {
- builder: ["object", "property", "computed"],
+ builder: ["object", "property", "computed", "optional"],

This comment has been minimized.

@Kovensky

Kovensky Jun 6, 2017

Member

Does this need to be added to NewExpression / CallExpression as well?

@Kovensky

Kovensky Jun 6, 2017

Member

Does this need to be added to NewExpression / CallExpression as well?

This comment has been minimized.

@jridgewell

jridgewell Jun 6, 2017

Member

Damn, missed that.

@jridgewell

jridgewell Jun 6, 2017

Member

Damn, missed that.

@hzoo

This comment has been minimized.

Show comment
Hide comment
@hzoo

hzoo Jun 6, 2017

Member
Member

hzoo commented Jun 6, 2017

@codecov

This comment has been minimized.

Show comment
Hide comment
@codecov

codecov bot Jun 7, 2017

Codecov Report

Merging #5813 into 7.0 will increase coverage by 0.12%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##              7.0    #5813      +/-   ##
==========================================
+ Coverage   85.09%   85.22%   +0.12%     
==========================================
  Files         284      285       +1     
  Lines        9911     9975      +64     
  Branches     2768     2790      +22     
==========================================
+ Hits         8434     8501      +67     
+ Misses        976      974       -2     
+ Partials      501      500       -1
Impacted Files Coverage Δ
packages/babel-types/src/definitions/core.js 98.52% <ø> (ø) ⬆️
...el-plugin-transform-optional-chaining/src/index.js 100% <100%> (ø)
...ages/babel-generator/src/generators/expressions.js 97.84% <100%> (+0.14%) ⬆️
...bel-plugin-transform-es2015-classes/src/vanilla.js 90.21% <0%> (-0.43%) ⬇️
packages/babel-traverse/src/path/context.js 86.2% <0%> (+0.86%) ⬆️
packages/babel-traverse/src/path/modification.js 73.07% <0%> (+0.96%) ⬆️
packages/babel-generator/src/node/index.js 97.82% <0%> (+2.17%) ⬆️
packages/babel-helper-call-delegate/src/index.js 68% <0%> (+4%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5cc1cbf...0740e61. Read the comment docs.

codecov bot commented Jun 7, 2017

Codecov Report

Merging #5813 into 7.0 will increase coverage by 0.12%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##              7.0    #5813      +/-   ##
==========================================
+ Coverage   85.09%   85.22%   +0.12%     
==========================================
  Files         284      285       +1     
  Lines        9911     9975      +64     
  Branches     2768     2790      +22     
==========================================
+ Hits         8434     8501      +67     
+ Misses        976      974       -2     
+ Partials      501      500       -1
Impacted Files Coverage Δ
packages/babel-types/src/definitions/core.js 98.52% <ø> (ø) ⬆️
...el-plugin-transform-optional-chaining/src/index.js 100% <100%> (ø)
...ages/babel-generator/src/generators/expressions.js 97.84% <100%> (+0.14%) ⬆️
...bel-plugin-transform-es2015-classes/src/vanilla.js 90.21% <0%> (-0.43%) ⬇️
packages/babel-traverse/src/path/context.js 86.2% <0%> (+0.86%) ⬆️
packages/babel-traverse/src/path/modification.js 73.07% <0%> (+0.96%) ⬆️
packages/babel-generator/src/node/index.js 97.82% <0%> (+2.17%) ⬆️
packages/babel-helper-call-delegate/src/index.js 68% <0%> (+4%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5cc1cbf...0740e61. Read the comment docs.

@jridgewell

This comment has been minimized.

Show comment
Hide comment
@jridgewell

jridgewell Jun 7, 2017

Member

@hzoo: done.

Member

jridgewell commented Jun 7, 2017

@hzoo: done.

jridgewell added some commits Jun 7, 2017

+ function findReplacementPath(path) {
+ return path.find((path) => {
+ const { parentPath } = path;
+

This comment has been minimized.

@rattrayalex

rattrayalex Jun 9, 2017

Contributor

My impression is that you were not interested in supporting (x?.y).z -> (x == null ? null : x.y).z syntax (which CoffeeScript supports), but if you are, I believe that returning true here if the parentPath is parenthesized would accomplish it.

@rattrayalex

rattrayalex Jun 9, 2017

Contributor

My impression is that you were not interested in supporting (x?.y).z -> (x == null ? null : x.y).z syntax (which CoffeeScript supports), but if you are, I believe that returning true here if the parentPath is parenthesized would accomplish it.

This comment has been minimized.

@jridgewell

jridgewell Jun 9, 2017

Member

The spec explicitly says not to do this (see the "Free grouping?" section, which sadly I can't directly link to). Anyway, I don't think there's a way to tell in Babel.

@jridgewell

jridgewell Jun 9, 2017

Member

The spec explicitly says not to do this (see the "Free grouping?" section, which sadly I can't directly link to). Anyway, I don't think there's a way to tell in Babel.

This comment has been minimized.

@rattrayalex

rattrayalex Jun 9, 2017

Contributor

Thanks, quoting here for convenience:

Free grouping? As currently specced, use of parentheses for mere grouping does not stop short-circuiting. However that semantics is debatable and may be changed.

You can tell in Babel by checking node.extra && node.extra.parenthesized === true on a MemberExpresssion (or any node).

@rattrayalex

rattrayalex Jun 9, 2017

Contributor

Thanks, quoting here for convenience:

Free grouping? As currently specced, use of parentheses for mere grouping does not stop short-circuiting. However that semantics is debatable and may be changed.

You can tell in Babel by checking node.extra && node.extra.parenthesized === true on a MemberExpresssion (or any node).

@chicoxyzzy chicoxyzzy referenced this pull request in claudepache/es-optional-chaining Jun 10, 2017

Closed

Do you have a babel plugin? #6

@anasinnyk

This comment has been minimized.

Show comment
Hide comment
@anasinnyk

anasinnyk Jun 13, 2017

When are you plan merge this pull request? Thanks (:

When are you plan merge this pull request? Thanks (:

@jridgewell

This comment has been minimized.

Show comment
Hide comment
@jridgewell

jridgewell Jun 13, 2017

Member

No one's approved it yet.

Member

jridgewell commented Jun 13, 2017

No one's approved it yet.

@nickmessing nickmessing referenced this pull request in vuejs/vuex Jun 14, 2017

Open

add hasModule API #833

@gisenberg

Looks good to me, thanks so much for the contribution! I'll work towards updating the TC39 proposal to cover some of the comments in this PR.

@Mouvedia

This comment has been minimized.

Show comment
Hide comment
@Mouvedia

Mouvedia Jun 21, 2017

Does this mean that it has been added to the preset-stage-1?

Does this mean that it has been added to the preset-stage-1?

@alangpierce alangpierce referenced this pull request in decaffeinate/decaffeinate Jun 22, 2017

Closed

Option to use lodash' `_.get()` instead of `__guard` functions #1103

@jridgewell

This comment has been minimized.

Show comment
Hide comment
@jridgewell

jridgewell Jun 27, 2017

Member

It's in there now.

Member

jridgewell commented Jun 27, 2017

It's in there now.

+ "babel-plugin-syntax-optional-chaining": "7.0.0-alpha.13"
+ },
+ "keywords": [
+ "babel-plugin"

This comment has been minimized.

@xtuc

xtuc Jun 27, 2017

Member

We can more keyword here:

  • optional chaining
  • null propagator
  • elvis
@xtuc

xtuc Jun 27, 2017

Member

We can more keyword here:

  • optional chaining
  • null propagator
  • elvis

This comment has been minimized.

This comment has been minimized.

@mblarsen

mblarsen Feb 25, 2018

Cyclops-Elvis operator

@mblarsen

mblarsen Feb 25, 2018

Cyclops-Elvis operator

@xtuc

xtuc approved these changes Jun 27, 2017

Nice work guys 👍

@hzoo hzoo merged commit 89d8f70 into babel:7.0 Jun 27, 2017

3 checks passed

ci/circleci Your tests passed on CircleCI!
Details
codecov/project 85.25% (target 80%)
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@hzoo

This comment has been minimized.

Show comment
Hide comment
@hzoo

hzoo Jun 27, 2017

Member

Great job @jridgewell and @xtuc, thanks for the review @gisenberg!!

Member

hzoo commented Jun 27, 2017

Great job @jridgewell and @xtuc, thanks for the review @gisenberg!!

@STRML STRML referenced this pull request in Microsoft/TypeScript Jun 27, 2017

Open

Suggestion: "safe navigation operator", i.e. x?.y #16

@fernahh fernahh referenced this pull request in braziljs/weekly Jun 27, 2017

Closed

Edição 200 - 29/06/2017 #161

@hzoo hzoo referenced this pull request Jun 28, 2017

Closed

Elvis/existential operator? #855

@pronebird

This comment has been minimized.

Show comment
Hide comment
@pronebird

pronebird Jun 30, 2017

This is awesome! Can this be released on NPM please? I was not able to install syntax-optional-chaining plugin :/

This is awesome! Can this be released on NPM please? I was not able to install syntax-optional-chaining plugin :/

@mastilver

This comment has been minimized.

Show comment
Hide comment

@pronebird see #5905

@xtuc xtuc referenced this pull request in babel/babel-standalone Jul 2, 2017

Closed

Include "idx" plugin #89

@hzoo

This comment has been minimized.

Show comment
Hide comment
@hzoo

hzoo Jul 12, 2017

Member

You want transform not syntax - https://github.com/babel/babel/releases/tag/v7.0.0-alpha.15, it's part of stage-1 too

https://babeljs.io/7/

Member

hzoo commented Jul 12, 2017

You want transform not syntax - https://github.com/babel/babel/releases/tag/v7.0.0-alpha.15, it's part of stage-1 too

https://babeljs.io/7/

@pronebird pronebird referenced this pull request in facebook/flow Jul 21, 2017

Closed

Support for optional chaining #4434

@keithkml keithkml referenced this pull request in facebook/flow Jul 24, 2017

Closed

Syntax for handling nullable variables #607

@hzoo hzoo referenced this pull request in babel/proposals Jul 27, 2017

Open

Optional Chaining Operator (Stage 1) #18

@wtgtybhertgeghgtwtg wtgtybhertgeghgtwtg referenced this pull request in facebook/create-react-app Jan 13, 2018

Closed

⚛️Add idx in babel-preset #1874

@vjpr vjpr referenced this pull request in tc39/proposal-optional-chaining Jan 18, 2018

Open

Tooling implementation status #44

3 of 5 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment