Skip to content

Commit 7d05b10

Browse files
spkapustcopybara-github
authored andcommitted
This CL addresses [Issue #4148](#4257) "Concatenated template literals are not merged in ADVANCED mode"
Upon further investigation it appears that template literals (specifically those that can't be optimized to strings) are not add folded when transpiling to ES6 or above What’s in this CL: - ONLY addressing case of template literal + template literal (added TODO for other cases) in PeepholeFoldConstants. - added associated unit tests PiperOrigin-RevId: 833853572
1 parent acae9cb commit 7d05b10

File tree

2 files changed

+240
-1
lines changed

2 files changed

+240
-1
lines changed

src/com/google/javascript/jscomp/PeepholeFoldConstants.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -992,11 +992,46 @@ private Node tryFoldLeftChildOp(Node n, Node left, Node right) {
992992
return n;
993993
}
994994

995+
/**
996+
* Folds additions of template literals with placeholders/substitutions like `${foo} and ${bar}` +
997+
* `${baz}`.
998+
*
999+
* <p>This code path is only hit if the left and right are both template literals WITH
1000+
* placeholders/substitutions. If one of them did not have placeholders/substitutions, it would
1001+
* have already have been optimized to a string.
1002+
*/
1003+
private Node foldAddTemplateLiterals(Node oldNode, Node left, Node right) {
1004+
// All template literal nodes with placeholders/substitutions will have their first child
1005+
// and last child as a string node.
1006+
// Merge the last left child string node with the first right child node.
1007+
Node lastLeft = left.getLastChild();
1008+
Node firstRight = right.getFirstChild();
1009+
Node mergeNode =
1010+
IR.templateLiteralString(
1011+
lastLeft.getCookedString() + firstRight.getCookedString(),
1012+
lastLeft.getRawString() + firstRight.getRawString());
1013+
mergeNode.srcrefTreeIfMissing(lastLeft);
1014+
1015+
// Detach the lastLeft and firstRight children and append all right children to the left node.
1016+
lastLeft.detach();
1017+
firstRight.detach();
1018+
left.addChildToBack(mergeNode);
1019+
left.addChildrenToBack(right.removeChildren());
1020+
1021+
// Detach the updated left node and replace the old node with it.
1022+
left.detach();
1023+
return replace(oldNode, left);
1024+
}
1025+
9951026
private Node tryFoldAdd(Node node, Node left, Node right) {
9961027
checkArgument(node.isAdd());
9971028

9981029
if (NodeUtil.mayBeString(node, shouldUseTypes)) {
999-
if (NodeUtil.isLiteralValue(left, false) && NodeUtil.isLiteralValue(right, false)) {
1030+
if (left.isTemplateLit() && right.isTemplateLit()) {
1031+
// TODO: account for cases of template literal + other data type (any order)
1032+
// (e.g. string + template literal, template literal + number)
1033+
return foldAddTemplateLiterals(node, left, right);
1034+
} else if (NodeUtil.isLiteralValue(left, false) && NodeUtil.isLiteralValue(right, false)) {
10001035
// '6' + 7
10011036
return tryFoldAddConstantString(node, left, right);
10021037
} else if (left.isStringLit() && left.getString().isEmpty() && isStringTyped(right)) {

test/com/google/javascript/jscomp/PeepholeFoldConstantsTest.java

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.javascript.jscomp;
1818

19+
import static com.google.common.truth.Truth.assertThat;
1920
import static com.google.common.truth.Truth.assertWithMessage;
2021

2122
import com.google.common.base.Joiner;
@@ -2249,6 +2250,209 @@ public void testAssociativeFoldConstantsWithVariables() {
22492250
test("alert(12 & x & 20);", "alert(x & 4);");
22502251
}
22512252

2253+
@Test
2254+
public void testTemplateLiteralConcat() {
2255+
2256+
// join at variables
2257+
test(
2258+
"""
2259+
const a = `${x}` + `${x}`;
2260+
""",
2261+
"""
2262+
const a = `${x}${x}`;
2263+
""");
2264+
test(
2265+
"""
2266+
const a = `${x}${y}` + `${x}${y}`;
2267+
""",
2268+
"""
2269+
const a = `${x}${y}${x}${y}`;
2270+
""");
2271+
2272+
// join at variable and string
2273+
test(
2274+
"""
2275+
const a = `${x}` + `a${x}`;
2276+
""",
2277+
"""
2278+
const a =`${x}a${x}`;
2279+
""");
2280+
test(
2281+
"""
2282+
const a = `${x}` + ` ${x}`;
2283+
""",
2284+
"""
2285+
const a =`${x} ${x}`;
2286+
""");
2287+
2288+
// join at string and variable
2289+
test(
2290+
"""
2291+
const a = `a${x}b` + `${x}c`;
2292+
""",
2293+
"""
2294+
const a = `a${x}b${x}c`;
2295+
""");
2296+
test(
2297+
"""
2298+
const a = `a${x} ` + `${x}b`;
2299+
""",
2300+
"""
2301+
const a = `a${x} ${x}b`;
2302+
""");
2303+
2304+
// join at strings
2305+
test(
2306+
"""
2307+
const x = 'X';
2308+
console.log(`a${x}b` + `c${x}d`);
2309+
""",
2310+
"""
2311+
const x = 'X';
2312+
console.log(`a${x}bc${x}d`);
2313+
""");
2314+
2315+
// complex joins
2316+
test(
2317+
"""
2318+
const a = `${foo() + bar()}` + `${baz()}`;
2319+
""",
2320+
"""
2321+
const a =`${foo() + bar()}${baz()}`;
2322+
""");
2323+
test(
2324+
"""
2325+
console.log(`<h1>${url}</h1>` + `<p>The URL is ${url}.</p>`);
2326+
""",
2327+
"""
2328+
console.log(`<h1>${url}</h1><p>The URL is ${url}.</p>`);
2329+
""");
2330+
test(
2331+
"""
2332+
console.log(`${(() => {return url;})()}</h1>` + `<p>The URL is ${url}.</p>`);
2333+
""",
2334+
"""
2335+
console.log(`${(() => {return url;})()}</h1><p>The URL is ${url}.</p>`);
2336+
""");
2337+
2338+
// don't fold tagged template literals
2339+
testSame(
2340+
"""
2341+
const a = foo`${b}` + `${b}`;
2342+
""");
2343+
testSame(
2344+
"""
2345+
const a = foo`${b}` + bar`${b}`;
2346+
""");
2347+
}
2348+
2349+
@Test
2350+
public void
2351+
testFoldAddTemplateLiterals_validateRawAndCookedStringForSpecialChars_andValidateChildren() {
2352+
// This is a bit tricky because there are two layers of escaping.
2353+
// Java sees "\\n" as "(\\)n" so it becomes "\n" for the javascript compiler.
2354+
// The compiler then interprets this as the special character "\n"
2355+
test("var x = `${a}\\n` + `\\t${b}`", "var x = `${a}\\n\\t${b}`");
2356+
2357+
// getJsRoot() returns the ROOT node for inputs only.
2358+
// The first child is the SCRIPT node containing the input code.
2359+
Node script = getLastCompiler().getJsRoot().getFirstChild();
2360+
Node var = script.getFirstChild();
2361+
Node name = var.getFirstChild();
2362+
Node templateLit = name.getFirstChild();
2363+
assertThat(templateLit.isTemplateLit()).isTrue();
2364+
2365+
// TEMPLATELIT should have 5 total children
2366+
assertThat(templateLit.getChildCount()).isEqualTo(5);
2367+
2368+
// Index 0: TEMPLATELIT_STRING ""
2369+
@SuppressWarnings({"RhinoNodeGetFirstChild", "Simplification"})
2370+
Node child0 = templateLit.getChildAtIndex(0);
2371+
assertThat(child0.isTemplateLitString()).isTrue();
2372+
assertThat(child0.getCookedString()).isEmpty();
2373+
assertThat(child0.getRawString()).isEmpty();
2374+
2375+
// Index 1: SUB a
2376+
@SuppressWarnings({"RhinoNodeGetSecondChild", "Simplification"})
2377+
Node child1 = templateLit.getChildAtIndex(1);
2378+
assertThat(child1.isTemplateLitSub()).isTrue();
2379+
assertThat(child1.getOnlyChild().isName()).isTrue();
2380+
assertThat(child1.getOnlyChild().getString()).isEqualTo("a");
2381+
2382+
// Index 2: TEMPLATELIT_STRING "\n\t"
2383+
Node child2 = templateLit.getChildAtIndex(2);
2384+
assertThat(child2.isTemplateLitString()).isTrue();
2385+
assertThat(child2.getCookedString()).isEqualTo("\n\t");
2386+
assertThat(child2.getRawString()).isEqualTo("\\n\\t");
2387+
2388+
// Index 3: SUB b
2389+
Node child3 = templateLit.getChildAtIndex(3);
2390+
assertThat(child3.isTemplateLitSub()).isTrue();
2391+
assertThat(child3.getOnlyChild().isName()).isTrue();
2392+
assertThat(child3.getOnlyChild().getString()).isEqualTo("b");
2393+
2394+
// Index 4: TEMPLATELIT_STRING ""
2395+
Node child4 = templateLit.getChildAtIndex(4);
2396+
assertThat(child4.isTemplateLitString()).isTrue();
2397+
assertThat(child4.getCookedString()).isEmpty();
2398+
assertThat(child4.getRawString()).isEmpty();
2399+
}
2400+
2401+
@Test
2402+
public void
2403+
testFoldAddTemplateLiterals_validateRawAndCookedStringForLiteralBackslash_andValidateChildren() {
2404+
// This is a bit tricky because there are two layers of escaping.
2405+
// Java sees "\\\\n" as "(\\)(\\)n" so it becomes "\\n" for the javascript compiler.
2406+
// The compiler then interprets this as "(\\)n" so it becomes "\" followed by
2407+
// "n" NOT the special character "\n".
2408+
test(
2409+
"var x = `${foo()}\\\\` + `n\\t${()=> {return b;}}`",
2410+
"var x = `${foo()}\\\\n\\t${()=> {return b;}}`");
2411+
2412+
// getJsRoot() returns the ROOT node for inputs only.
2413+
// The first child is the SCRIPT node containing the input code.
2414+
Node script = getLastCompiler().getJsRoot().getFirstChild();
2415+
Node var = script.getFirstChild();
2416+
Node name = var.getFirstChild();
2417+
Node templateLit = name.getFirstChild();
2418+
assertThat(templateLit.isTemplateLit()).isTrue();
2419+
2420+
// TEMPLATELIT should have 5 children:
2421+
assertThat(templateLit.getChildCount()).isEqualTo(5);
2422+
2423+
// Index 0: TEMPLATELIT_STRING ""
2424+
@SuppressWarnings({"RhinoNodeGetFirstChild", "Simplification"})
2425+
Node child0 = templateLit.getChildAtIndex(0);
2426+
assertThat(child0.isTemplateLitString()).isTrue();
2427+
assertThat(child0.getCookedString()).isEmpty();
2428+
assertThat(child0.getRawString()).isEmpty();
2429+
2430+
// Index 1: SUB foo()
2431+
@SuppressWarnings({"RhinoNodeGetSecondChild", "Simplification"})
2432+
Node child1 = templateLit.getChildAtIndex(1);
2433+
assertThat(child1.isTemplateLitSub()).isTrue();
2434+
assertThat(child1.getOnlyChild().isCall()).isTrue();
2435+
assertThat(child1.getOnlyChild().getOnlyChild().getString()).isEqualTo("foo");
2436+
2437+
// Index 2: TEMPLATELIT_STRING "\n\t"
2438+
Node child2 = templateLit.getChildAtIndex(2);
2439+
assertThat(child2.isTemplateLitString()).isTrue();
2440+
assertThat(child2.getCookedString()).isEqualTo("\\n\t");
2441+
assertThat(child2.getRawString()).isEqualTo("\\\\n\\t");
2442+
2443+
// Index 3: SUB ()=> {return b;}
2444+
Node child3 = templateLit.getChildAtIndex(3);
2445+
assertThat(child3.isTemplateLitSub()).isTrue();
2446+
assertThat(child3.getOnlyChild().isFunction()).isTrue();
2447+
assertThat(child3.getOnlyChild().getFirstChild().getString()).isEqualTo("");
2448+
2449+
// Index 4: TEMPLATELIT_STRING ""
2450+
Node child4 = templateLit.getChildAtIndex(4);
2451+
assertThat(child4.isTemplateLitString()).isTrue();
2452+
assertThat(child4.getCookedString()).isEmpty();
2453+
assertThat(child4.getRawString()).isEmpty();
2454+
}
2455+
22522456
private void foldBigIntTypes(String js, String expected) {
22532457
test(
22542458
"function f(/** @type {bigint} */ x) { " + js + " }",

0 commit comments

Comments
 (0)