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

Implement transform for nullish-coalescing operator #6483

Merged
merged 10 commits into from
Oct 18, 2017

Conversation

azz
Copy link
Member

@azz azz commented Oct 14, 2017

Q                       A
Fixed Issues?
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature? 👍
Tests Added + Pass? 👍 (more needed)
Documentation PR
Any Dependency Changes? 👍 depends on babylon beta.29

Adds the babel transform for the nullish-coalescing operator proposal, currently at stage 1.

Adds two new packages:

  • babel-plugin-syntax-nullish-coalescing-operator
    Just turns on the "nullishCoalescingOperator" plugin.
  • babel-plugin-transform-nullish-coalescing-operator
    Transforms:
    var x = obj.foo ?? "bar";
    Into:
    var _ref;
    var x = (_ref = obj.foo, _ref != null ? _ref : "bar");

Adds transform-nullish-coalescing-operator to babel-preset-stage-1.

A couple of tests are added but more are needed. Please comment with any tests cases you can think of!

Babylon PR: babel/babylon#761
Ref: babel/proposals#14
Proposal: https://github.com/gisenberg/proposal-null-coalescing
Spec Text: https://littledan.github.io/proposal-nullary-coalescing/

return;
}

const scope = path.scope.parent || path.scope;
Copy link
Member

Choose a reason for hiding this comment

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

Just path.scope.

Copy link
Member Author

@azz azz Oct 15, 2017

Choose a reason for hiding this comment

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

I did this to fix an issue with function default arguments.

function foo(a, b = a ?? 0) {}

Would otherwise transform to

function foo(a, b = (_ref = a, ref != null ? _ref : 0)) {
  var _ref;
}

Which leaks a global. Is there a better way to handle this?

}

const scope = path.scope.parent || path.scope;
const ref = scope.generateUidIdentifier("ref");
Copy link
Member

Choose a reason for hiding this comment

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

We can use #generateUidIdentifierBasedOnNode, which'll create a better identifier name.

t.assignmentExpression("=", ref, node.left),
t.conditionalExpression(
t.binaryExpression("!=", ref, t.nullLiteral()),
ref,
Copy link
Member

Choose a reason for hiding this comment

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

Need to clone this node.

@@ -38,6 +38,7 @@ export const NUMBER_BINARY_OPERATORS = [
];
export const BINARY_OPERATORS = [
"+",
"??",
Copy link
Member

Choose a reason for hiding this comment

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

This belongs in LOGICAL_OPERATORS, not BINARY_OPERATORS

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, forgot to move it. Thanks.

visitor: {
LogicalExpression(path) {
const { node } = path;
if (node.operator !== OPERATOR) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Just inline it.

@azz azz force-pushed the nullish-coalescing-operator branch 2 times, most recently from 21dccff to fae540f Compare October 17, 2017 02:49
@babel-bot
Copy link
Collaborator

babel-bot commented Oct 17, 2017

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/5383/


```javascript
var _ref;
var foo = (_ref = object.foo, _ref != null ? _ref : "default");
Copy link
Contributor

@bakkot bakkot Oct 17, 2017

Choose a reason for hiding this comment

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

Depending on how concerned we are about being exactly right... this isn't. document.all ?? "default" should give you document.all, not "default", even though document.all is == null.

(Not familiar with the HTML Document Dot All object? document.all is a value which is == null and == void 0 but not === either; it has typeof document.all === 'undefined' but can be called without throwing. Why? Historical reasons. Welcome to the web platform, where everything is terrible.)

If we care about this, it should instead be ref !== null && ref !== void 0 (or however babel outputs undefined). Alternatively, we could choose to not care. Basically no modern code has any legitimate reason to interact with document.all anyway.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should maybe have two options here; the default, spec, should indeed do ref === null && ref === undefined, but loose could do == null if there's a performance impact.

Copy link
Contributor

Choose a reason for hiding this comment

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

It is plausible that the former is faster, actually, depending on the engine. x != null has to end up the same as x !== null && x !== undefined && x !== document.all, which is more expensive than x !== null && x !== undefined.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we just forget that document.all exists? 😉

Choose a reason for hiding this comment

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

There was actual confusion at the TC39 meeting about whether document.all is included, so it'd be nice if Babel got this right, at least in spec mode. It would be good to get strong user feedback here.

Copy link
Member

Choose a reason for hiding this comment

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

Get it right, as in pretend it doesn't exist? That's my preferred solution. Do we have any other syntax that explicitly accounts for document.all?

Copy link
Member

Choose a reason for hiding this comment

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

_ref !== null && _ref !== undefined would account for it properly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to ref !== null && _ref !== void 0 for compliance.

Copy link
Contributor

@bakkot bakkot left a comment

Choose a reason for hiding this comment

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

LGTM otherwise.

.gitignore Outdated
@@ -1,4 +1,5 @@
.DS_Store
.vscode
Copy link
Member

Choose a reason for hiding this comment

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

(not a big deal, but this seems like the sort of thing to put in your global gitconfig, rather than adding specific editor knowledge to individual repos)

Copy link
Member

Choose a reason for hiding this comment

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

(reminder)

@@ -0,0 +1,13 @@
{
"name": "babel-plugin-syntax-nullish-coalescing-operator",
"version": "7.0.0-beta.3",
Copy link
Member

Choose a reason for hiding this comment

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

any reason this can't just start at 1.0.0? I thought babel 7 was moving away from pegged version numbers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just following precedent. @hzoo should this be changed?

Copy link
Member

@hzoo hzoo Oct 17, 2017

Choose a reason for hiding this comment

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

Yeah we need to move all proposals/stage-x presets into the new experimental folder and then version those separately (separate pr)

@@ -0,0 +1,7 @@
export default function() {
Copy link
Member

Choose a reason for hiding this comment

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

should this be an arrow, since it doesn't need this?

Copy link
Member

Choose a reason for hiding this comment

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

function is ok, since we use it in every other plugin.

Copy link
Member

Choose a reason for hiding this comment

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

I'd argue for it to be an arrow because it's not supposed to be constructible -- but since all other plugins are written like this, changing it would be outside the scope of this PR. Best to keep to the pattern and then later we can codemod everything to arrows if we decide to.

@@ -0,0 +1,17 @@
{
"name": "babel-plugin-transform-nullish-coalescing-operator",
"version": "7.0.0-beta.3",
Copy link
Member

Choose a reason for hiding this comment

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

same question here

"description": "Remove nullish coalescing operator",
"repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-nullish-coalescing-opearator",
"license": "MIT",
"main": "lib/index.js",
Copy link
Member

Choose a reason for hiding this comment

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

could this just be lib?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. I just copied it from a similar package.

Copy link
Member

Choose a reason for hiding this comment

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

(reminder)

@xtuc
Copy link
Member

xtuc commented Oct 17, 2017

We should probably wait for #6495 before merging and renaming it in the mean time.

Copy link
Member

@xtuc xtuc left a comment

Choose a reason for hiding this comment

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

I didn't realize it was your first contribution @azz. Nice job 👍

@hzoo
Copy link
Member

hzoo commented Oct 17, 2017

Ah yeah I merged the scoped packages rename so will need to make that change too 😛 and rebase

@hzoo hzoo added the PR: New Feature 🚀 A type of pull request used for our changelog categories label Oct 17, 2017
return;
}

const scope = path.scope.parent || path.scope;
Copy link
Member

Choose a reason for hiding this comment

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

Still needs to be just path.scope. The default parameter memoization affects other transforms, and we'll need to figure out a way to deal with them all together.

t.sequenceExpression([
t.assignmentExpression("=", ref, node.left),
t.conditionalExpression(
t.binaryExpression("!=", ref, t.nullLiteral()),
Copy link
Member

Choose a reason for hiding this comment

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

ref needs to be cloned here.

Copy link
Member Author

Choose a reason for hiding this comment

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

What's the rule for when a node needs to be cloned? When it is passed to multiple builders?

Copy link
Member

Choose a reason for hiding this comment

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

Whenever it's reusued in multiple places in an AST. We're just findings bugs with reused nodes, so they're still a bit hard to spot.

@azz azz force-pushed the nullish-coalescing-operator branch from fae540f to dea0733 Compare October 17, 2017 23:05

```javascript
var _ref;
var foo = (_ref = object.foo, _ref != null ? _ref : "default");
Copy link
Member

Choose a reason for hiding this comment

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

to clarify; i think that using != null by default should be a blocker, since it's not in compliance with the spec

"description": "Remove nullish coalescing operator",
"repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-nullish-coalescing-opearator",
"license": "MIT",
"main": "lib/index.js",
Copy link
Member

Choose a reason for hiding this comment

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

(reminder)

.gitignore Outdated
@@ -1,4 +1,5 @@
.DS_Store
.vscode
Copy link
Member

Choose a reason for hiding this comment

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

(reminder)

@azz
Copy link
Member Author

azz commented Oct 18, 2017

Happy to go either way with document.all. Do we change the transform or change the proposal?

It will be difficult to write a test for it either way without exposing natives syntax in v8.

assert.equal(%GetUndetectable() ?? "foo", %GetUndetectable());

@bakkot
Copy link
Contributor

bakkot commented Oct 18, 2017

If I recall correctly, we explicitly discussed document.all in committee and decided it was not "nullish", for the purposes of this proposal. So I think the right thing to do is change the transform.

I wouldn't worry too much about a test for document.all, personally, though other people might disagree. Maybe a comment in the transform (and/or readme?) explaining why you aren't using != null.

@ljharb
Copy link
Member

ljharb commented Oct 18, 2017

An inline comment is probably most useful.

Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

Thanks, LGTM!

Copy link
Member

@Jessidhia Jessidhia left a comment

Choose a reason for hiding this comment

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

LGTM

I kinda want to add a test for document.all if typeof document !== 'undefined' but I don't want browsers to feel like they can't remove document.all because they would break babel's tests ;)

(I don't even know if we can execute our tests in a browser)

@@ -0,0 +1,7 @@
export default function() {
Copy link
Member

Choose a reason for hiding this comment

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

I'd argue for it to be an arrow because it's not supposed to be constructible -- but since all other plugins are written like this, changing it would be outside the scope of this PR. Best to keep to the pattern and then later we can codemod everything to arrows if we decide to.

```

> **NOTE:** We cannot use `!= null` here because `document.all` is `!= null` and
> `document.all` has been deemed not "nullish".
Copy link
Member

Choose a reason for hiding this comment

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

This also has a runtime performance benefit, even if it happens to be slightly bigger. See nodejs/node#15178 for example.

}

const ref = scope.generateUidIdentifierBasedOnNode(node.left);
scope.push({ id: ref });
Copy link
Member

Choose a reason for hiding this comment

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

A possible future optimization is to try to reuse the ref if they are in the same scope and the ref does not escape.

I don't know if the scope API supports escape analysis, though.

var foo = (_object$foo = object.foo, _object$foo !== null && _object$foo !== void 0 ? _object$foo : "default");
```

> **NOTE:** We cannot use `!= null` here because `document.all` is `!= null` and
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: 'document.all is != null' should be 'document.all is == null'

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, thanks.

Copy link
Member

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

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

We don't have a way to test in browsers currently, but this is fine. Let's make a "good first issue" to transform into != null in loose mode.

@@ -0,0 +1,3 @@
function foo(foo, bar = (_foo = foo, _foo !== null && _foo !== void 0 ? _foo : "bar")) {
var _foo;
Copy link
Member

Choose a reason for hiding this comment

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

This is definitely incorrect, but we can't do better at the moment. It'll take a try-finally to get right, which is kinda sad.

var _foo;

function foo(foo, bar = (_foo = foo, _foo !== null && _foo !== void 0 ? _foo : "bar")) {
  try {
    // function body
  } finally {
    _foo = void 0;
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

It's probably better to use an IIFE, no?

function foo(foo, bar = function(){let _foo = foo; return _foo !== null && _foo !== void 0 ? _foo : "bar";}()) {}

Avoids leaking anything to the outer scope, if that's the concern.

Copy link
Member

Choose a reason for hiding this comment

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

😄 Oh man, I totally overlooked that this value can be anything, including a IIFE.

Copy link
Member Author

Choose a reason for hiding this comment

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

Kids, don't try this at home!

@jridgewell jridgewell merged commit 99be60b into babel:master Oct 18, 2017
@jridgewell
Copy link
Member

Oh, and we need to update optional chaining to match the document.all semantics.

@azz azz deleted the nullish-coalescing-operator branch October 18, 2017 23:12
@lock lock bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Oct 5, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 5, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: New Feature 🚀 A type of pull request used for our changelog categories Spec: Nullish Coalescing Operator
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants