enhance `templates` (#5131)
authorAlex Lam S.L <alexlamsl@gmail.com>
Thu, 23 Sep 2021 10:26:26 +0000 (11:26 +0100)
committerGitHub <noreply@github.com>
Thu, 23 Sep 2021 10:26:26 +0000 (18:26 +0800)
closes #5125

lib/compress.js
lib/parse.js
test/compress/templates.js
test/mocha/templates.js

index 399bfdc..1cd6c77 100644 (file)
@@ -4922,11 +4922,9 @@ merge(Compressor.prototype, {
             return this;
 
             function decode(str) {
-                return str.replace(/\\(u\{[^}]*\}?|u[\s\S]{0,4}|x[\s\S]{0,2}|[0-9]+|[\s\S])/g, function(match, seq) {
-                    var s = decode_escape_sequence(seq);
-                    if (typeof s != "string") malformed = true;
-                    return s;
-                });
+                str = decode_template(str);
+                if (typeof str != "string") malformed = true;
+                return str;
             }
         });
     })(function(node, func) {
@@ -11336,42 +11334,84 @@ merge(Compressor.prototype, {
             && tag.expression.name == "String";
     }
 
+    function decode_template(str) {
+        var malformed = false;
+        str = str.replace(/\\(u\{[^}]*\}?|u[\s\S]{0,4}|x[\s\S]{0,2}|[0-9]+|[\s\S])/g, function(match, seq) {
+            var ch = decode_escape_sequence(seq);
+            if (typeof ch == "string") return ch;
+            malformed = true;
+        });
+        if (!malformed) return str;
+    }
+
     OPT(AST_Template, function(self, compressor) {
         if (!compressor.option("templates")) return self;
         var tag = self.tag;
         if (!tag || is_raw_tag(compressor, tag)) {
-            var exprs = self.expressions.slice();
-            var strs = self.strings.slice();
-            var CHANGED = false;
-            for (var i = exprs.length; --i >= 0;) {
-                var node = exprs[i];
-                var ev = node.evaluate(compressor);
-                if (ev === node) continue;
-                if (tag && /\r|\\|`/.test(ev)) continue;
-                ev = ("" + ev).replace(/\r|\\|`/g, function(s) {
-                    return "\\" + (s == "\r" ? "r" : s);
-                });
-                if (ev.length > node.print_to_string().length + 3) continue;
-                var combined = strs[i] + ev + strs[i + 1];
-                if (typeof make_node(AST_Template, self, {
-                    expressions: [],
-                    strings: [ combined ],
-                    tag: tag,
-                }).evaluate(compressor) != typeof make_node(AST_Template, self, {
-                    expressions: [ node ],
-                    strings: strs.slice(i, i + 2),
-                    tag: tag,
-                }).evaluate(compressor)) continue;
-                exprs.splice(i, 1);
-                strs.splice(i, 2, combined);
-                CHANGED = true;
+            var exprs = [];
+            var strs = [];
+            for (var i = 0, status; i < self.strings.length; i++) {
+                var str = self.strings[i];
+                if (!tag) {
+                    var trimmed = decode_template(str);
+                    if (trimmed) str = escape_literal(trimmed);
+                }
+                if (i > 0) {
+                    var node = self.expressions[i - 1];
+                    var value = should_join(node);
+                    if (value) {
+                        var prev = strs[strs.length - 1];
+                        var joined = prev + value + str;
+                        var decoded;
+                        if (tag || typeof (decoded = decode_template(joined)) == status) {
+                            strs[strs.length - 1] = decoded ? escape_literal(decoded) : joined;
+                            continue;
+                        }
+                    }
+                    exprs.push(node);
+                }
+                strs.push(str);
+                if (!tag) status = typeof trimmed;
             }
-            if (CHANGED) {
-                self.expressions = exprs;
-                self.strings = strs;
+            if (!tag && strs.length > 1) {
+                if (strs[0] == "") return make_node(AST_Binary, self, {
+                    operator: "+",
+                    left: exprs[0],
+                    right: make_node(AST_Template, self, {
+                        expressions: exprs.slice(1),
+                        strings: strs.slice(1),
+                        tag: tag,
+                    }).transform(compressor),
+                }).optimize(compressor);
+                if (strs[strs.length - 1] == "") return make_node(AST_Binary, self, {
+                    operator: "+",
+                    left: make_node(AST_Template, self, {
+                        expressions: exprs.slice(0, -1),
+                        strings: strs.slice(0, -1),
+                        tag: tag,
+                    }).transform(compressor),
+                    right: exprs[exprs.length - 1],
+                }).optimize(compressor);
             }
+            self.expressions = exprs;
+            self.strings = strs;
         }
         return try_evaluate(compressor, self);
+
+        function escape_literal(str) {
+            return str.replace(/\r|\\|`|\${/g, function(s) {
+                return "\\" + (s == "\r" ? "r" : s);
+            });
+        }
+
+        function should_join(node) {
+            var ev = node.evaluate(compressor);
+            if (ev === node) return;
+            if (tag && /\r|\\|`/.test(ev)) return;
+            ev = escape_literal("" + ev);
+            if (ev.length > node.print_to_string().length + "${}".length) return;
+            return ev;
+        }
     });
 
     function is_atomic(lhs, self) {
index a80fcaf..16ac7dd 100644 (file)
@@ -242,10 +242,10 @@ function tokenizer($TEXT, filename, html5_comments, shebang) {
         read_template   : with_eof_error("Unterminated template literal", function(strings) {
             var s = "";
             for (;;) {
-                var ch = next(true, true);
+                var ch = read();
                 switch (ch) {
                   case "\\":
-                    ch += next(true, true);
+                    ch += read();
                     break;
                   case "`":
                     strings.push(s);
@@ -260,6 +260,11 @@ function tokenizer($TEXT, filename, html5_comments, shebang) {
                 }
                 s += ch;
             }
+
+            function read() {
+                var ch = next(true, true);
+                return ch == "\r" ? "\n" : ch;
+            }
         }),
     };
     var prev_was_dot = false;
index 3c1661f..81bc861 100644 (file)
@@ -131,6 +131,105 @@ malformed_escape: {
     node_version: ">=4"
 }
 
+booleans: {
+    options = {
+        booleans: true,
+        evaluate: true,
+        templates: true,
+    }
+    input: {
+        var a;
+        console.log(`$${a}${a}` ? "PASS" : "FAIL");
+    }
+    expect: {
+        var a;
+        console.log("$" + a + a ? "PASS" : "FAIL");
+    }
+    expect_stdout: "PASS"
+    node_version: ">=4"
+}
+
+escape_placeholder_1: {
+    options = {
+        templates: true,
+    }
+    input: {
+        console.log(`\${\n`);
+    }
+    expect: {
+        console.log(`\${
+`);
+    }
+    expect_stdout: [
+        "${",
+        "",
+    ]
+    node_version: ">=4"
+}
+
+escape_placeholder_2: {
+    options = {
+        evaluate: true,
+        templates: true,
+    }
+    input: {
+        console.log(`\n${"${"}\n`);
+    }
+    expect: {
+        console.log(`
+\${
+`);
+    }
+    expect_stdout: [
+        "",
+        "${",
+        "",
+    ]
+    node_version: ">=4"
+}
+
+escape_placeholder_3: {
+    options = {
+        evaluate: true,
+        templates: true,
+    }
+    input: {
+        console.log(`\n$${"{"}\n`);
+    }
+    expect: {
+        console.log(`
+\${
+`);
+    }
+    expect_stdout: [
+        "",
+        "${",
+        "",
+    ]
+    node_version: ">=4"
+}
+
+escape_placeholder_4: {
+    options = {
+        evaluate: true,
+        templates: true,
+    }
+    input: {
+        console.log(`\n${"$"}${"{"}\n`);
+    }
+    expect: {
+        console.log(`
+\${
+`);
+    }
+    expect_stdout: [
+        "",
+        "${",
+        "",
+    ]
+    node_version: ">=4"
+}
+
 evaluate: {
     options = {
         evaluate: true,
@@ -174,7 +273,7 @@ partial_evaluate: {
         console.log(`${6 * 7} foo ${console ? `PA` + "SS" : `FA` + `IL`}`);
     }
     expect: {
-        console.log(`42 foo ${console ? "PASS" : "FAIL"}`);
+        console.log("42 foo " + (console ? "PASS" : "FAIL"));
     }
     expect_stdout: "42 foo PASS"
     node_version: ">=4"
@@ -204,7 +303,7 @@ malformed_evaluate_2: {
         console.log(`\u0${0}b${5}`);
     }
     expect: {
-        console.log(`\u0${0}b5`);
+        console.log(`\u00b` + 5);
     }
     expect_stdout: true
     node_version: ">=4"
@@ -357,13 +456,14 @@ issue_4604: {
 issue_4606: {
     options = {
         evaluate: true,
+        strings: true,
         templates: true,
     }
     input: {
         console.log(`${typeof A} ${"\r"} ${"\\"} ${"`"}`);
     }
     expect: {
-        console.log(`${typeof A} \r \\ \``);
+        console.log(typeof A + " \r \\ `");
     }
     expect_stdout: "undefined \r \\ `"
     node_version: ">=4"
@@ -434,3 +534,154 @@ issue_4931: {
     ]
     node_version: ">=4"
 }
+
+issue_5125_1: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`PASS ${typeof A}`);
+    }
+    expect: {
+        console.log("PASS " + typeof A);
+    }
+    expect_stdout: "PASS undefined"
+    node_version: ">=4"
+}
+
+issue_5125_2: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`PASS
+${typeof A}`);
+    }
+    expect: {
+        console.log(`PASS
+` + typeof A);
+    }
+    expect_stdout: [
+        "PASS",
+        "undefined",
+    ]
+    node_version: ">=4"
+}
+
+issue_5125_3: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`PASS\n${typeof A}`);
+    }
+    expect: {
+        console.log(`PASS
+` + typeof A);
+    }
+    expect_stdout: [
+        "PASS",
+        "undefined",
+    ]
+    node_version: ">=4"
+}
+
+issue_5125_4: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`PASS
+
+${typeof A}`);
+    }
+    expect: {
+        console.log(`PASS
+
+` + typeof A);
+    }
+    expect_stdout: [
+        "PASS",
+        "",
+        "undefined",
+    ]
+    node_version: ">=4"
+}
+
+issue_5125_5: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`PASS\n\n${typeof A}`);
+    }
+    expect: {
+        console.log(`PASS
+
+` + typeof A);
+    }
+    expect_stdout: [
+        "PASS",
+        "",
+        "undefined",
+    ]
+    node_version: ">=4"
+}
+
+issue_5125_6: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`${typeof A} ${typeof B} PASS`);
+    }
+    expect: {
+        console.log(typeof A + ` ${typeof B} PASS`);
+    }
+    expect_stdout: "undefined undefined PASS"
+    node_version: ">=4"
+}
+
+issue_5125_7: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`${typeof A} ${typeof B} ${typeof C} PASS`);
+    }
+    expect: {
+        console.log(typeof A + ` ${typeof B} ${typeof C} PASS`);
+    }
+    expect_stdout: "undefined undefined undefined PASS"
+    node_version: ">=4"
+}
+
+issue_5125_8: {
+    options = {
+        evaluate: true,
+        strings: true,
+        templates: true,
+    }
+    input: {
+        console.log(`${typeof A}${typeof B}${typeof C} PASS`);
+    }
+    expect: {
+        console.log(typeof A + typeof B + typeof C + " PASS");
+    }
+    expect_stdout: "undefinedundefinedundefined PASS"
+    node_version: ">=4"
+}
index 169e791..3e5134a 100644 (file)
@@ -34,20 +34,20 @@ describe("Template literals", function() {
         [
             // native line breaks
             [ "`foo\nbar`", "`foo\nbar`" ],
-            [ "`foo\rbar`", "`foo\rbar`" ],
+            [ "`foo\rbar`", "`foo\nbar`" ],
             [ "`foo\r\nbar`", "`foo\nbar`" ],
-            [ "`foo\r\n\rbar`", "`foo\n\rbar`" ],
+            [ "`foo\r\n\rbar`", "`foo\n\nbar`" ],
             // escaped line breaks
             [ "`foo\\nbar`", "`foo\\nbar`" ],
             [ "`foo\\rbar`", "`foo\\rbar`" ],
-            [ "`foo\r\\nbar`", "`foo\r\\nbar`" ],
+            [ "`foo\r\\nbar`", "`foo\n\\nbar`" ],
             [ "`foo\\r\nbar`", "`foo\\r\nbar`" ],
             [ "`foo\\r\\nbar`", "`foo\\r\\nbar`" ],
             // continuation
             [ "`foo\\\nbar`", "`foo\\\nbar`" ],
-            [ "`foo\\\rbar`", "`foo\\\rbar`" ],
+            [ "`foo\\\rbar`", "`foo\\\nbar`" ],
             [ "`foo\\\r\nbar`", "`foo\\\nbar`" ],
-            [ "`foo\\\r\n\rbar`", "`foo\\\n\rbar`" ],
+            [ "`foo\\\r\n\rbar`", "`foo\\\n\nbar`" ],
             [ "`foo\\\\nbar`", "`foo\\\\nbar`" ],
             [ "`foo\\\\rbar`", "`foo\\\\rbar`" ],
             [ "`foo\\\\r\nbar`", "`foo\\\\r\nbar`" ],