From e31bbe329a55adbdd37a953597e297c0da279f07 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 14 May 2022 05:15:54 +0100 Subject: [PATCH] improve compatibility with `use strict` (#5440) --- lib/ast.js | 21 ++-- lib/compress.js | 36 +++++- lib/parse.js | 11 +- test/compress/classes.js | 176 +++++++++++++++++++++++++++-- test/compress/functions.js | 225 +++++++++++++++++++++++++++++++++++++ test/ufuzz/index.js | 9 -- 6 files changed, 441 insertions(+), 37 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index e826cec6..bc4741fe 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -827,6 +827,9 @@ var AST_Class = DEFNODE("Class", "extends name properties", { extends: "[AST_Node?] the super class, or null if not specified", properties: "[AST_ClassProperty*] array of class properties", }, + resolve: function(def_class) { + return def_class ? this : this.parent_scope.resolve(); + }, walk: function(visitor) { var node = this; visitor.visit(node, function() { @@ -851,9 +854,6 @@ var AST_DefClass = DEFNODE("DefClass", null, { $propdoc: { name: "[AST_SymbolDefClass] the name of this class", }, - resolve: function(def_class) { - return def_class ? this : this.parent_scope.resolve(); - }, _validate: function() { if (!(this.name instanceof AST_SymbolDefClass)) throw new Error("name must be AST_SymbolDefClass"); }, @@ -1837,7 +1837,7 @@ var AST_Label = DEFNODE("Label", "references", { initialize: function() { this.references = []; this.thedef = this; - } + }, }, AST_Symbol); var AST_SymbolRef = DEFNODE("SymbolRef", "fixed in_arg redef", { @@ -2039,16 +2039,21 @@ TreeWalker.prototype = { return this.stack[this.stack.length - 2 - (n || 0)]; }, push: function(node) { - if (node instanceof AST_Lambda) { + var value; + if (node instanceof AST_Class) { + this.directives = Object.create(this.directives); + value = "use strict"; + } else if (node instanceof AST_Directive) { + value = node.value; + } else if (node instanceof AST_Lambda) { this.directives = Object.create(this.directives); - } else if (node instanceof AST_Directive && !this.directives[node.value]) { - this.directives[node.value] = node; } + if (value && !this.directives[value]) this.directives[value] = node; this.stack.push(node); }, pop: function() { var node = this.stack.pop(); - if (node instanceof AST_Lambda) { + if (node instanceof AST_Class || node instanceof AST_Lambda) { this.directives = Object.getPrototypeOf(this.directives); } }, diff --git a/lib/compress.js b/lib/compress.js index 8ab33cfe..42bb6a77 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2552,8 +2552,7 @@ Compressor.prototype.compress = function(node) { && all(iife.args, function(arg) { return !(arg instanceof AST_Spread); })) { - var fn_strict = compressor.has_directive("use strict"); - if (fn_strict && !member(fn_strict, fn.body)) fn_strict = false; + var fn_strict = fn.in_strict_mode() && !fn.parent_scope.resolve(true).in_strict_mode(); var has_await = is_async(fn) ? function(node) { return node instanceof AST_Symbol && node.name == "await"; } : function(node) { @@ -4120,6 +4119,25 @@ Compressor.prototype.compress = function(node) { && !(compressor && node.expression.has_side_effects(compressor)); } + // in_strict_mode() + // return true if scope executes in Strict Mode + (function(def) { + def(AST_Class, return_this); + def(AST_Scope, function() { + var body = this.body; + for (var i = 0; i < body.length; i++) { + var stat = body[i]; + if (!(stat instanceof AST_Directive)) break; + if (stat.value == "use strict") return true; + } + var parent = this.parent_scope; + if (!parent) return false; + return parent.resolve(true).in_strict_mode(); + }); + })(function(node, func) { + node.DEFMETHOD("in_strict_mode", func); + }); + // is_truthy() // return true if `!!node === true` (function(def) { @@ -9882,6 +9900,10 @@ Compressor.prototype.compress = function(node) { return safe; } + function safe_from_strict_mode(fn, compressor) { + return fn.in_strict_mode() || !compressor.has_directive("use strict"); + } + OPT(AST_Call, function(self, compressor) { var exp = self.expression; var terminated = trim_optional_chain(self, compressor); @@ -10206,7 +10228,10 @@ Compressor.prototype.compress = function(node) { } return true; }) && !(fn.rest instanceof AST_Destructured && has_arg_refs(fn, fn.rest)); - var can_inline = can_drop && compressor.option("inline") && !self.is_expr_pure(compressor); + var can_inline = can_drop + && compressor.option("inline") + && !self.is_expr_pure(compressor) + && (exp === fn || safe_from_strict_mode(fn, compressor)); if (can_inline && stat instanceof AST_Return) { var value = stat.value; if (exp === fn @@ -11813,8 +11838,9 @@ Compressor.prototype.compress = function(node) { } else if (fixed.name && fixed.name.definition() !== def) { single_use = false; } else if (fixed.parent_scope !== self.scope || is_funarg(def)) { - single_use = fixed.is_constant_expression(self.scope); - if (single_use == "f") { + if (!safe_from_strict_mode(fixed, compressor)) { + single_use = false; + } else if ((single_use = fixed.is_constant_expression(self.scope)) == "f") { var scope = self.scope; do { if (scope instanceof AST_LambdaDefinition || scope instanceof AST_LambdaExpression) { diff --git a/lib/parse.js b/lib/parse.js index 60710292..a03300e9 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -636,8 +636,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { next_token.add_directive = function(directive) { S.directive_stack[S.directive_stack.length - 1].push(directive); - if (S.directives[directive]) S.directives[directive]++; - else S.directives[directive] = 1; + S.directives[directive] = (S.directives[directive] || 0) + 1; } next_token.push_directives_stack = function() { @@ -1340,8 +1339,6 @@ function parse($TEXT, options) { var loop = S.in_loop; var labels = S.labels; ++S.in_function; - S.in_directives = true; - S.input.push_directives_stack(); S.in_loop = 0; S.labels = []; if (is("punc", "{")) { @@ -1352,8 +1349,6 @@ function parse($TEXT, options) { handle_regexp(); value = maybe_assign(); } - var is_strict = S.input.has_directive("use strict"); - S.input.pop_directives_stack(); --S.in_function; S.in_loop = loop; S.labels = labels; @@ -1367,7 +1362,7 @@ function parse($TEXT, options) { value: value, end: prev(), }); - if (is_strict) node.each_argname(strict_verify_symbol); + if (S.input.has_directive("use strict")) node.each_argname(strict_verify_symbol); return node; } @@ -1412,7 +1407,7 @@ function parse($TEXT, options) { name: name, argnames: argnames, rest: argnames.rest || null, - body: body + body: body, }); if (is_strict) { if (name) strict_verify_symbol(name); diff --git a/test/compress/classes.js b/test/compress/classes.js index aec8c015..63b3eb9f 100644 --- a/test/compress/classes.js +++ b/test/compress/classes.js @@ -742,6 +742,38 @@ collapse_rhs_static: { node_version: ">=12" } +inline_non_strict: { + options = { + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f(a) { + return a.p = "PASS"; + } + class A { + g() { + return f(42); + } + } + console.log(new A().g()); + } + expect: { + function f(a) { + return a.p = "PASS"; + } + console.log(new class { + g() { + return f(42); + } + }().g()); + } + expect_stdout: "PASS" + node_version: ">=6" +} + self_comparison: { options = { booleans: true, @@ -1758,12 +1790,14 @@ issue_4962_1: { })(function g() {}); } expect: { - (function g() {}), - void class { - static c = function() { + (function() { + function f() { while (console.log(typeof g)); - }(); - }; + } + (class { + static c = f(); + }); + })(function g() {}); } expect_stdout: "undefined" node_version: ">=12" @@ -1796,6 +1830,37 @@ issue_4962_1_strict: { node_version: ">=12" } +issue_4962_1_strict_direct: { + options = { + ie: true, + inline: true, + reduce_vars: true, + unused: true, + } + input: { + (function() { + function f() { + "use strict"; + while (console.log(typeof g)); + } + class A { + static p = f(); + } + })(function g() {}); + } + expect: { + (function g() {}), + void class { + static c = function() { + "use strict"; + while (console.log(typeof g)); + }(); + }; + } + expect_stdout: "undefined" + node_version: ">=12" +} + issue_4962_2: { options = { ie: true, @@ -1815,8 +1880,11 @@ issue_4962_2: { } expect: { console.log(function f() {}(function g() { + function h() { + f; + } (class { - static c = f; + static c = h(); }); })); } @@ -1852,6 +1920,69 @@ issue_4962_2_strict: { node_version: ">=12" } +issue_4962_2_strict_direct: { + options = { + ie: true, + inline: true, + reduce_vars: true, + unused: true, + } + input: { + console.log(function f() {}(function g() { + function h() { + "use strict"; + f; + } + class A { + static p = h(); + } + }, typeof g)); + } + expect: { + console.log(function f() {}(function g() { + (class { + static c = function() { + "use strict"; + f; + }(); + }); + })); + } + expect_stdout: "undefined" + node_version: ">=12" +} + +issue_4962_2_strict_direct_inline: { + options = { + directives: true, + ie: true, + inline: true, + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + console.log(function f() {}(function g() { + function h() { + "use strict"; + f; + } + class A { + static p = h(); + } + }, typeof g)); + } + expect: { + console.log(function f() {}(function g() { + (class { + static c = f; + }); + })); + } + expect_stdout: "undefined" + node_version: ">=12" +} + issue_4982_1: { options = { dead_code: true, @@ -2541,7 +2672,7 @@ issue_5387: { node_version: ">=4" } -issue_5389: { +issue_5389_1: { options = { collapse_vars: true, toplevel: true, @@ -2572,6 +2703,37 @@ issue_5389: { node_version: ">=12" } +issue_5389_2: { + options = { + collapse_vars: true, + toplevel: true, + } + input: { + function log(m, n) { + console.log(m, n); + } + var a = log; + var A = class { + [a = "FAIL"] = a = "PASS"; + }; + var b = new A(); + log(a, b.FAIL); + } + expect: { + function log(m, n) { + console.log(m, n); + } + var a = log; + var A; + var b = new class { + [a = "FAIL"] = a = "PASS"; + }(); + log(a, b.FAIL); + } + expect_stdout: "PASS PASS" + node_version: ">=12" +} + issue_5436: { options = { merge_vars: true, diff --git a/test/compress/functions.js b/test/compress/functions.js index 0db59da5..d73a219b 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -8398,3 +8398,228 @@ issue_5409: { } expect_stdout: "undefined" } + +mixed_mode_inline_1: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + return this; + } + console.log(function() { + return f(); + }() ? "PASS" : "FAIL"); + } + expect: { + console.log(function() { + return this; + }() ? "PASS" : "FAIL"); + } + expect_stdout: "PASS" +} + +mixed_mode_inline_1_strict: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + "use strict"; + function f() { + return this; + } + console.log(function() { + return f(); + }() ? "FAIL" : "PASS"); + } + expect: { + "use strict"; + console.log(function() { + return this; + }() ? "FAIL" : "PASS"); + } + expect_stdout: "PASS" +} + +mixed_mode_inline_2: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + "use strict"; + return this; + } + console.log(function() { + return f(); + }() ? "FAIL" : "PASS"); + } + expect: { + console.log(function() { + "use strict"; + return this; + }() ? "FAIL" : "PASS"); + } + expect_stdout: "PASS" +} + +mixed_mode_inline_2_strict: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + "use strict"; + function f() { + "use strict"; + return this; + } + console.log(function() { + return f(); + }() ? "FAIL" : "PASS"); + } + expect: { + "use strict"; + console.log(function() { + return this; + }() ? "FAIL" : "PASS"); + } + expect_stdout: "PASS" +} + +mixed_mode_inline_3: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + return this; + } + console.log(function() { + "use strict"; + return f(); + }() ? "PASS" : "FAIL"); + } + expect: { + function f() { + return this; + } + console.log(function() { + "use strict"; + return f(); + }() ? "PASS" : "FAIL"); + } + expect_stdout: "PASS" +} + +mixed_mode_inline_3_strict: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + "use strict"; + function f() { + return this; + } + console.log(function() { + "use strict"; + return f(); + }() ? "FAIL" : "PASS"); + } + expect: { + "use strict"; + console.log(function() { + return this; + }() ? "FAIL" : "PASS"); + } + expect_stdout: "PASS" +} + +mixed_mode_inline_4: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + "use strict"; + return this; + } + console.log(function() { + "use strict"; + return f(); + }() ? "FAIL" : "PASS"); + } + expect: { + console.log(function() { + "use strict"; + return function() { + "use strict"; + return this; + }(); + }() ? "FAIL" : "PASS"); + } + expect_stdout: "PASS" +} + +mixed_mode_inline_4_strict: { + options = { + directives: true, + inline: true, + reduce_funcs: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + "use strict"; + function f() { + "use strict"; + return this; + } + console.log(function() { + "use strict"; + return f(); + }() ? "FAIL" : "PASS"); + } + expect: { + "use strict"; + console.log(function() { + return this; + }() ? "FAIL" : "PASS"); + } + expect_stdout: "PASS" +} diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index 9635b1d1..75de87d5 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -2534,15 +2534,6 @@ for (var round = 1; round <= num_iterations; round++) { } // ignore difference in error message caused by Temporal Dead Zone if (!ok && original_erred && uglify_erred && original_result.name == "ReferenceError" && uglify_result.name == "ReferenceError") ok = true; - // ignore difference due to implicit strict-mode in `class` - if (!ok && /\bclass\b/.test(original_code)) { - var original_strict = run_code('"use strict";\n' + original_code, toplevel); - if (uglify_erred && /^(Syntax|Type)Error$/.test(uglify_result.name)) { - ok = sandbox.is_error(original_strict); - } else { - ok = sandbox.same_stdout(original_strict, uglify_result); - } - } // ignore difference in error message caused by `import` symbol redeclaration if (!ok && original_erred && uglify_erred && /\bimport\b/.test(original_code)) { if (is_error_redeclaration(original_result) && is_error_redeclaration(uglify_result)) ok = true;