-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
Conversation
return; | ||
} | ||
|
||
const scope = path.scope.parent || path.scope; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just path.scope
.
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 = [ | |||
"+", | |||
"??", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Just inline it.
21dccff
to
fae540f
Compare
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"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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? 😉
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(reminder)
We should probably wait for #6495 before merging and renaming it in the mean time. |
There was a problem hiding this 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 👍
Ah yeah I merged the scoped packages rename so will need to make that change too 😛 and rebase |
return; | ||
} | ||
|
||
const scope = path.scope.parent || path.scope; |
There was a problem hiding this comment.
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()), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
- Use generateUidIdentifierBasedOnNode - Inline "??" - Clone ref node - Move "??" to LogicalExpression in babel-types
fae540f
to
dea0733
Compare
|
||
```javascript | ||
var _ref; | ||
var foo = (_ref = object.foo, _ref != null ? _ref : "default"); |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(reminder)
Happy to go either way with It will be difficult to write a test for it either way without exposing natives syntax in v8. assert.equal(%GetUndetectable() ?? "foo", %GetUndetectable()); |
If I recall correctly, we explicitly discussed I wouldn't worry too much about a test for |
An inline comment is probably most useful. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, LGTM!
There was a problem hiding this 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() { |
There was a problem hiding this comment.
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". |
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, thanks.
There was a problem hiding this 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; |
There was a problem hiding this comment.
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;
}
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
Oh, and we need to update optional chaining to match the |
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:
Adds
transform-nullish-coalescing-operator
tobabel-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/