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
Spec compatibility for template literals. #5791
Conversation
@yavorsky, thanks for your PR! By analyzing the history of the files in this pull request, we identified @jridgewell, @hzoo and @existentialism to be potential reviewers. |
Codecov Report
@@ Coverage Diff @@
## 7.0 #5791 +/- ##
==========================================
- Coverage 84.81% 84.81% -0.01%
==========================================
Files 282 282
Lines 9874 9887 +13
Branches 2776 2780 +4
==========================================
+ Hits 8375 8386 +11
Misses 990 990
- Partials 509 511 +2
Continue to review full report at Codecov.
|
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.
Overall, this looks good.
nodes.unshift(t.stringLiteral("")); | ||
} | ||
let root = nodes.shift(); |
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 just got rid of this #shift
😛. Let's do:
let root = nodes[0];
if (spec && nodes.length > 1) {
root = t.callExpression(t.memberExpression(target, t.identifier("concat")), nodes.slice(1));
} else {
for (;;) {
// ...
}
Whether you use the helper function is up to you.
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 saw you had refactored this previously.
I'm ok with a bit of immutability 👍
This still isn't quite right, if we're being really pedantic: `
${
console.log(1),
{
[Symbol.toPrimitive](){
console.log(2);
return 'foo';
}
}
}
${
console.log(3),
{
[Symbol.toPrimitive](){
console.log(4);
return 'bar';
}
}
}
` Per spec, this will log You could use a series of concat calls instead, which would have the right semantics. Alternatively you could define a custom |
@bakkot Also, what do you think about wrapping expressions into arrays instead of String. |
Strongly prefer |
Hmm. Purely to minimize output, we might be able to group strings into the last concat call: const calls = nodes.reduce((acc, node) => {
if (t.isStringLiteral(node)) {
last(acc).push(node);
} else {
nodes.push([node]);
}
});
for (let i = 0; i < calls.length; i++) {
root = buildConcat(root, calls[i]);
} |
Ok, I've updated some stuff to ensure the toPrimitive calls order. @jridgewell I took your proposition as a basis and enhanced it to fill arguments of the last So, it looks like: Also, it's good to add a test case to check toPrimitive calls order. The problem is that |
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.
@@ -1,7 +1,28 @@ | |||
export default function ({ types: t }) { | |||
|
|||
function buildConcatCallExressions(items) { | |||
return items.reduce(function(left, right) { |
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 avoid the _withIdentifier
, can't we rewrite to:
let first = true;
return items.reduce((left, right) => {
// Use isLiteral instead, 3 and null, and etc.
let canInsert = t.isLiteral(right);
if (!canInsert && first) {
canInsert = true;
first = false;
}
if (canInsert) {
left.arguments.push(right);
return left;
}
return t.callExpression(
t.memberExpression(left, t.identifier('concat')),
[right]
);
});
@jridgewell Updated. |
We can use minNodeVersion to restrict that test. |
canBeInserted = true; | ||
avail = false; | ||
} | ||
if (t.isCallExpression(left) && canBeInserted) { |
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: we can flip the LogicalExpression to save a function call in some cases.
@@ -75,7 +75,7 @@ In loose mode, tagged template literal objects aren't frozen. | |||
|
|||
`boolean`, defaults to `false`. | |||
|
|||
This option wraps all template literal expressions with `String`. See [babel/babel#1065](https://github.com/babel/babel/issues/1065) for more info. |
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 did we remove the link? We should add a reason for why this is necessary or a good idea so people understand why they would use this option (if it's just old then we can add a new sentence 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.
@hzoo I think we can add a link to this PR in README. Also going to add a few sentences about spec option and why we are using String.prototype.concat
.
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.
nice work! Just wondering about the docs for this in the readme (why use this, an example) - maybe loose needs that too
Awesome work @yavorsky 🎉 |
The behavior before is `"".concat(part1, part2, ...)`, which is useless and makes the minified file larger (about 600 bytes). The benefit of `concat` is just supporting `Symbol.toPrimitive`. Therefore, we should replace it with simpler `+`. Doc: * https://babeljs.io/docs/en/babel-plugin-transform-template-literals * babel/babel#5791
The behavior before is `"".concat(part1, part2, ...)`, which is useless and makes the minified file larger (about 600 bytes). The benefit of `concat` is just supporting `Symbol.toPrimitive`. Therefore, we should replace it with simpler `+`. Doc: * https://babeljs.io/docs/en/babel-plugin-transform-template-literals * babel/babel#5791
The behavior before is `"".concat(part1, part2, ...)`, which is useless and makes the minified file larger (about 600 bytes). The benefit of `concat` is just supporting `Symbol.toPrimitive`. Therefore, we should replace it with simpler `+`. Doc: * https://babeljs.io/docs/en/babel-plugin-transform-template-literals * babel/babel#5791
The behavior before is `"".concat(part1, part2, ...)`, which is useless and makes the minified file larger (about 600 bytes). The benefit of `concat` is just supporting `Symbol.toPrimitive`. Therefore, we should replace it with simpler `+`. Doc: * https://babeljs.io/docs/en/babel-plugin-transform-template-literals * babel/babel#5791
Without
spec
option there is spec compatibility problem:+
always passes "default" hint, but not "string".For ex:
It was resolved and described under babel/babel#1065, but the solution isn't the best and could be optimized.
For now, with
spec
option we just wrap expressions withString
:"a" + String(1) + "b" + String("c")
But for ex, if one of the template literal expressions is a
Symbol()
it won't throw.String
constructor special-cases symbols and return description string instead of throwing.So, proposed solution is to use
String.prototype.concat
which handle this cases according to the spec:in
`a${1}${"b"}${"c"}`
out
`"a".concat(1, "b", "c")`
thanks @shvaikalesh for pointing me to this issue and help!