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
fix(rest-spread): Do not require Symbol.iterator
for strings
#9794
fix(rest-spread): Do not require Symbol.iterator
for strings
#9794
Conversation
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/10682/ |
@zloirock Do you have reference as to how it would produce an issue? Do you mean the conversion with corejs2? I did find other pieces of helper use I did have another idea to solving this and that's ditching In the end, it's just going to call I was only targeting the regression specifically, but the alternative of According to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Polyfill the only thing that |
@clshortfuse this line will add tens of kilobytes to the bundle and it's a serious regression. It could be fixed on
I propose to use this approach in the |
I'm willing to dive in a bit more into the issue. I know sometimes solutions to problems tend to get shelved for lack of priority. I think finding the mechanics through which an Object can use Symbol.iterator can be a good goal. Plainly stated, all the current code checks is whether the object is a candidate for Symbol iterator. We don't have to replicate the entirely Symbol functionality, but, instead, only it's check it's candidacy. (ie: if it can get an iterator, then it can receive an Array.from call). Also, I think it's important to note the that the only purpose for the entire check is to simply cast (of sorts) an Object to an Array. As far as I've seen, there's no reference to this function outside of ToArray, so maybe taking a step back and looking at the original purpose may prove useful. As for the mention of bloat, I'm assuming the issue lies with the importing of _Symbol from an external library. This is my first time playing around with a PR for Babel, so I wasn't aware of what consequences some functions would have. Still, I'll try to see what alternatives I can come up with. I'll probably need to dive deep into the ECMA spec and review a handful of popular Symbol polyfills just to ensure we don't cause any conflicts, and ultimately, hopefully it can be used outside of loose mode. |
cc952fe
to
ded2532
Compare
I took another stab at this. I took another approach based on how babel/packages/babel-helpers/src/helpers.js Line 891 in 3aaafae
Basically, it's wrapped in a try block. If it fails, then it discards the error and tries the prototype. If that doesn't success, babel/packages/babel-helpers/src/helpers.js Line 924 in 3aaafae
Doing it this way instead of how v6 was doing it (checking prototype first) avoids the performance penalty of the string comparison in contexts that support Symbol.iterator. I broke out the prototype whitelist into an array, so we can change it in the future to:
Out of curiosity, and not exactly related to the issue, but if we call |
'[object Arguments]', | ||
]; | ||
let hasSymbol = false; | ||
try { |
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.
Why is a try/catch needed here instead of just checking if Symbol.iterator exists or not?
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.
You'd have to check if Symbol
exists before checking if Symbol.iterator
exists. I squashed the previous commit, but checking if Symbol exists would bloat the code up, as explained the one of the previous comments.
Doing it this way removes the need for typeof
checks and lets the browser pass the Symbol check failure to catch
. Since we're not actually using Symbol.iterator
the result would be same. If it can't check the object to see if it has it, then it wouldn't have it 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.
try/catch has a performance penalty that's far worse than a few extra gzippable bytes.
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.
As I mentioned in the other comment, this is the fourth try
in the same helper file. I don't see why this specific one should be treated any differently.
Edit: 4th try, 2nd specific to Symbol
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.
because in that case, it's being used to catch errors thrown by user code (the iterator method). In this case it's used to catch errors in your code itself (attempting unchecked member access on a maybe-not-object).
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, in the next()
function. Okay, back to the drawing board.
According to ES6 spec, the following prototypes use
Since I'm only targeting fixing the V6 regression with this PR, I resorted to leaving it out. |
// ES6 prototypes that use @@iterator | ||
const prototypeWhiteList = [ | ||
String.prototype, | ||
Array.prototype, |
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.
Array.isArray
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.
Array.isArray
isn't the same a prototype check. For example, Array.isArray
would return true
when checked against Array.prototype
where this wouldn't. Also, Array.isArray
is already checked prior to this function.
} | ||
|
||
// Check for custom iterator outside ES6 spec, iterable WeakMap, or iterable TypedArray | ||
if (Symbol.iterator in iterObject) { |
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.
Wasn't the point of this PR to do a typeof Symbol !== 'undefined'
check?
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.
No. It's to fix the spread regression specifically.
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.
But none of your changes will help spreading the HTMLCollection
from the issue: [...document.getElementsByTagName('div')]
. I don't understand what you're looking for then.
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.
The input code shows the error, but doesn't accurately detail the regession. It's in the description, which is more accurate. Basically, all spread
functions fail on IE11 because it's checking for Symbol before checking the prototype. It's a regression from v6. I believe it's caused by 4da3f3b
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.
The code in the OP goes through arrayWithHoles
's Array.isArray
check, so this change does nothing still. I need an explicit "this fails in IE 11, doesn't fail with this change" case so that I can review the PR.
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 you're missing the point of the [object Arguments]
check. That block never executes on environments without Symbol
. Therefore any function with spread arguments would fail.
function func1(...args) {
console.log(args[0]);
// expected output: 1
console.log(args[1]);
// expected output: 2
console.log(args[2]);
// expected output: 3
}
func1(1, 2, 3);
Edit: Forgot the spread!
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.
Looking back at the issue from 3 months ago, the request was a partial fix for the regression while the more complicated issue of completely eliminating Symbol is explored. That PR has been stuck for a couple of months now.
I think that's why I had the mindset of swapping the order of execution, at least to minimize the issue.
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 executable test case we can add is:
function sum(x, y, z) {
return x + y + z;
}
function test() {
var args = arguments;
return sum(...args);
}
assert.equal(test(1, 2, 3), 6);
All that's necessary for this was to change the order of the ||
expression in the old code.
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.
Yes, and wrap them in parentheses. Also, any object that is a prototype of Array
and String
would align with the ES6 spec.
I'm not seeing much more we can do that wouldn't trigger extra corejs2 imports. I have no way of checking for the internal TypedArray
type. Also, NodeList
, HTMLCollection
and others aren't part of the ES6 spec. I realized that this function, at least as it's called, is not checking for array-likes. It's checking for iterables. Therefore, something like this:
[...({ 0: 'a', 1: 'b', 2: 'c', length: 3})]
would throw an error on an ES6 supporting browser, and therefore should throw an error on Babel. Support for array-likes would have to be loose
. I think this the best we can do without violating spec.
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 add a string check, that's fine. The array check is already covered by arrayWithHoles
(the difference between Array.prototype
passing Array.isArray
and not Array.prototype.isPrototypeOf(Array.prototype)
doesn't matter). Let's do the string check, then arguments check, then isIterable check.
Okay, I've made the changes to this code. I also changed the commit and topic to better reflect the limited scope of this fix. I'm only targeting the rest parameter regression here. From my research, I've hit a bit of a wall getting a catch-all solution. I've come to agree with @zloirock that the
would throw an non-iterable error. That means babel should as well. It would be bad if babel would run something fine when transpiled to ES5, and then if somebody makes a ES6 version without babel, realizes that it doesn't work. That's because There's more that can be done to target Lastly, I want to apologize to the devs for the possibly annoying back and forth with the changes due to not fundamentally understanding the true underlying nature of Note to @jridgewell : The reason why I didn't put the String check outside |
If I add
to Then I can add the following without forcing imports:
Edit: Though taking another look, that would cause a false positive. The reality is that we probably should let the compiler throw an error saying Symbol is not found. We wouldn't want to incorrectly let the function pass |
Please add an |
@jridgewell Let me know when you want me to squash the commits. |
No need, we can squash merge them on GitHub. |
We will fix this to also work with Map/Set without loading the polyfills from |
I don't see any sense in this PR in the current form since without |
@zloirock I don't understand. You rather it be in a more broken state? It's a regression. There is no catch-all fix that I've seen without polyfilling Symbol. If we can at least partially resolve the regression, then people don't have to change perfectly working V6 code around. Edit:
I'm pretty sure the function that is called before this one checks |
@clshortfuse I'm not against of adding support of work spread without |
Ok, my vision of this helper for function iterableToArray(it) {
switch (Object.prototype.toString.call(it).slice(8, -1)) {
// As an option, they will work even without `Array.from` polyfill,
// however, it could cause problems with custom iterators:
case "Arguments": // It will not work in old IE without `core-js`
case "HTMLCollection": // 2 most popular iterable DOM collections,
case "NodeList": // for work with other you should load `core-js`
return Array.prototype.slice.call(it);
case "String": // Since of code units, we should use `Array.from`
case "Set": // It will work with `es6-shim` if they will add
case "Map": // internal @@toStringTag support
return Array.from(it);
}
if (it == null) return;
// It's more correct by the spec than `in` check
// However, it will not work with `@babel/runtime-corejs2` -/
const iterator = it[Symbol.iterator];
if (typeof iterator !== 'function') return;
return Array.from({ [Symbol.iterator]: iterator.bind(it) });
} |
@zloirock, I originally had an array prototype check but we agreed to remove it since the previous calling function is going to call I have my old commits here. I did have a list of every ES6 type in spec. I decided against the typeof check, before calling Set, Map or Symbol because it would lead to the wrong error. The user should get notified they don't exist and not a TypeError. That would be a spec violation. It can be confusing to give the user the incorrect error and we don't want people to think the object they are using spread against is an invalid one. I opted out of non ES6 prototypes like HTMLCollection because they're not part of the actual spec. It's stricter, but whitelisting would be too loose, IMO, and go against the goal of being to spec. I'm being pretty strict to limiting to just what ES6 has with String and rest arguments. Anything else should throw the correct error if it's not available. If users don't want it to be so strict, we can improve the loose mode. |
Ok, seems I missed a moment about |
This applies the same logic as other runtime helpers by checking if
typeof Symbol === 'function'
before using Symbol.iterator as seen here:babel/packages/babel-helpers/src/helpers.js
Line 178 in 3aaafae
The CoreJS2 version of this example would be compiled as:
You can also see this at
babel/packages/babel-helpers/src/helpers.js
Line 215 in 3aaafae
So I can confirm the syntax is correct. I've tested on IE11 and spread now works properly (after adding a polyfill for Array.from).
Tested with:
The only actual code change in
packages/babel-helpers/src/helpers.js
and everything else is auto generated bymake bootstrap
.