From e918748d88e39c6f2142b01e71c3e580d790d642 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 02:55:32 +0800 Subject: [PATCH 01/38] improve collapsible value detection (#1638) - #1634 bars variables with cross-scope references in between to collapse - but if assigned value is side-effect-free, no states can be modified, so it is safe to move --- lib/compress.js | 4 +- test/compress/collapse_vars.js | 68 ++++++++++++++++++++++++++++++++++ test/mocha/glob.js | 4 +- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index cfa8f230..e75d7c96 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -592,9 +592,10 @@ merge(Compressor.prototype, { // Restrict var replacement to constants if side effects encountered. if (side_effects_encountered |= lvalues_encountered) continue; + var value_has_side_effects = var_decl.value.has_side_effects(compressor); // Non-constant single use vars can only be replaced in same scope. if (ref.scope !== self) { - side_effects_encountered |= var_decl.value.has_side_effects(compressor); + side_effects_encountered |= value_has_side_effects; continue; } @@ -620,6 +621,7 @@ merge(Compressor.prototype, { || (parent instanceof AST_If && node !== parent.condition) || (parent instanceof AST_Conditional && node !== parent.condition) || (node instanceof AST_SymbolRef + && value_has_side_effects && !are_references_in_scope(node.definition(), self)) || (parent instanceof AST_Binary && (parent.operator == "&&" || parent.operator == "||") diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 6f273b97..2437ca5f 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1522,3 +1522,71 @@ issue_1631_3: { } expect_stdout: "6" } + +var_side_effects_1: { + options = { + collapse_vars: true, + } + input: { + var print = console.log.bind(console); + function foo(x) { + var twice = x * 2; + print('Foo:', twice); + } + foo(10); + } + expect: { + var print = console.log.bind(console); + function foo(x) { + print('Foo:', 2 * x); + } + foo(10); + } + expect_stdout: true +} + +var_side_effects_2: { + options = { + collapse_vars: true, + } + input: { + var print = console.log.bind(console); + function foo(x) { + var twice = x.y * 2; + print('Foo:', twice); + } + foo({ y: 10 }); + } + expect: { + var print = console.log.bind(console); + function foo(x) { + var twice = 2 * x.y; + print('Foo:', twice); + } + foo({ y: 10 }); + } + expect_stdout: true +} + +var_side_effects_3: { + options = { + collapse_vars: true, + pure_getters: true, + } + input: { + var print = console.log.bind(console); + function foo(x) { + var twice = x.y * 2; + print('Foo:', twice); + } + foo({ y: 10 }); + } + expect: { + var print = console.log.bind(console); + function foo(x) { + print('Foo:', 2 * x.y); + } + foo({ y: 10 }); + } + expect_stdout: true +} diff --git a/test/mocha/glob.js b/test/mocha/glob.js index e291efc8..e9555a52 100644 --- a/test/mocha/glob.js +++ b/test/mocha/glob.js @@ -5,7 +5,7 @@ var path = require("path"); describe("minify() with input file globs", function() { it("minify() with one input file glob string.", function() { var result = Uglify.minify("test/input/issue-1242/foo.*"); - assert.strictEqual(result.code, 'function foo(o){var n=2*o;print("Foo:",n)}var print=console.log.bind(console);'); + assert.strictEqual(result.code, 'function foo(o){print("Foo:",2*o)}var print=console.log.bind(console);'); }); it("minify() with an array of one input file glob.", function() { var result = Uglify.minify([ @@ -20,7 +20,7 @@ describe("minify() with input file globs", function() { ], { compress: { toplevel: true } }); - assert.strictEqual(result.code, 'var print=console.log.bind(console),a=function(n){return 3*n}(3),b=function(n){return n/2}(12);print("qux",a,b),function(n){var o=2*n;print("Foo:",o)}(11);'); + assert.strictEqual(result.code, 'var print=console.log.bind(console),a=function(n){return 3*n}(3),b=function(n){return n/2}(12);print("qux",a,b),function(n){print("Foo:",2*n)}(11);'); }); it("should throw with non-matching glob string", function() { var glob = "test/input/issue-1242/blah.*"; From 79334dda105f772b7a5bfe13608a193c22537a7b Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 23 Mar 2017 23:55:03 -0400 Subject: [PATCH 02/38] fix regression: CLI options with hyphens like -b ascii-only (#1640) fixes #1637 --- bin/uglifyjs | 2 +- lib/parse.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/uglifyjs b/bin/uglifyjs index e39a4b4b..635ca365 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -561,7 +561,7 @@ function getOptions(flag, constants) { var ast; try { - ast = UglifyJS.parse(x, { expression: true }); + ast = UglifyJS.parse(x, { cli: true, expression: true }); } catch(ex) { if (ex instanceof UglifyJS.JS_Parse_Error) { print_error("Error parsing arguments for flag `" + flag + "': " + x); diff --git a/lib/parse.js b/lib/parse.js index 4d37b85e..99d7ce09 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -695,6 +695,7 @@ function parse($TEXT, options) { html5_comments : true, bare_returns : false, shebang : true, + cli : false, }); var S = { @@ -1501,6 +1502,7 @@ function parse($TEXT, options) { }; function is_assignable(expr) { + if (options.cli) return true; return expr instanceof AST_PropAccess || expr instanceof AST_SymbolRef; }; From 701035621d708c39f4bdd2022fc02bdb4a967a39 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 13:19:50 +0800 Subject: [PATCH 03/38] fix expect_stdout (#1642) `compress()` may modify input ASTs add tests for #1627 & #1640 --- test/compress/transform.js | 1 - test/input/invalid/assign_1.js | 1 + test/input/invalid/assign_2.js | 1 + test/input/invalid/assign_3.js | 1 + test/mocha/cli.js | 55 ++++++++++++++++++++++++++++++++++ test/run-tests.js | 45 ++++++++++++++-------------- 6 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 test/input/invalid/assign_1.js create mode 100644 test/input/invalid/assign_2.js create mode 100644 test/input/invalid/assign_3.js diff --git a/test/compress/transform.js b/test/compress/transform.js index 1cc72c07..48aa605e 100644 --- a/test/compress/transform.js +++ b/test/compress/transform.js @@ -30,7 +30,6 @@ booleans_global_defs: { expect: { console.log(!0); } - expect_stdout: true } condition_evaluate: { diff --git a/test/input/invalid/assign_1.js b/test/input/invalid/assign_1.js new file mode 100644 index 00000000..6d09d132 --- /dev/null +++ b/test/input/invalid/assign_1.js @@ -0,0 +1 @@ +console.log(1 || 5--); diff --git a/test/input/invalid/assign_2.js b/test/input/invalid/assign_2.js new file mode 100644 index 00000000..197bdc90 --- /dev/null +++ b/test/input/invalid/assign_2.js @@ -0,0 +1 @@ +console.log(2 || (Math.random() /= 2)); diff --git a/test/input/invalid/assign_3.js b/test/input/invalid/assign_3.js new file mode 100644 index 00000000..7c560e4b --- /dev/null +++ b/test/input/invalid/assign_3.js @@ -0,0 +1 @@ +console.log(3 || ++this); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 2b44c901..33749045 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -251,4 +251,59 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should support hyphen as shorthand", function(done) { + var command = uglifyjscmd + ' test/input/issue-1431/sample.js -m keep-fnames=true'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "function f(r){return function(){function n(n){return n*n}return r(n)}}function g(n){return n(1)+n(2)}console.log(f(g)()==5);\n"); + done(); + }); + }); + it("Should throw syntax error (5--)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/assign_1.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/assign_1.js:1,18", + "console.log(1 || 5--);", + " ^", + "SyntaxError: Invalid use of -- operator" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (Math.random() /= 2)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/assign_2.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/assign_2.js:1,32", + "console.log(2 || (Math.random() /= 2));", + " ^", + "SyntaxError: Invalid assignment" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (++this)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/assign_3.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/assign_3.js:1,23", + "console.log(3 || ++this);", + " ^", + "SyntaxError: Invalid use of ++ operator" + ].join("\n")); + done(); + }); + }); }); diff --git a/test/run-tests.js b/test/run-tests.js index 09e70021..3d291416 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -105,6 +105,23 @@ function run_compress_tests() { function test_case(test) { log_test(test.name); U.base54.reset(); + var output_options = test.beautify || {}; + var expect; + if (test.expect) { + expect = make_code(as_toplevel(test.expect, test.mangle), output_options); + } else { + expect = test.expect_exact; + } + var input = as_toplevel(test.input, test.mangle); + var input_code = make_code(input, output_options); + var input_formatted = make_code(test.input, { + beautify: true, + quote_style: 3, + keep_quoted_props: true + }); + if (test.mangle_props) { + input = U.mangle_properties(input, test.mangle_props); + } var options = U.defaults(test.options, { warnings: false }); @@ -117,22 +134,6 @@ function run_compress_tests() { if (!options.warnings) options.warnings = true; } var cmp = new U.Compressor(options, true); - var output_options = test.beautify || {}; - var expect; - if (test.expect) { - expect = make_code(as_toplevel(test.expect, test.mangle), output_options); - } else { - expect = test.expect_exact; - } - var input = as_toplevel(test.input, test.mangle); - var input_code = make_code(test.input, { - beautify: true, - quote_style: 3, - keep_quoted_props: true - }); - if (test.mangle_props) { - input = U.mangle_properties(input, test.mangle_props); - } var output = cmp.compress(input); output.figure_out_scope(test.mangle); if (test.mangle) { @@ -142,7 +143,7 @@ function run_compress_tests() { output = make_code(output, output_options); if (expect != output) { log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n", { - input: input_code, + input: input_formatted, output: output, expected: expect }); @@ -155,7 +156,7 @@ function run_compress_tests() { var reparsed_ast = U.parse(output); } catch (ex) { log("!!! Test matched expected result but cannot parse output\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n--REPARSE ERROR--\n{error}\n\n", { - input: input_code, + input: input_formatted, output: output, error: ex.toString(), }); @@ -174,7 +175,7 @@ function run_compress_tests() { var actual_warnings = JSON.stringify(warnings_emitted); if (expected_warnings != actual_warnings) { log("!!! failed\n---INPUT---\n{input}\n---EXPECTED WARNINGS---\n{expected_warnings}\n---ACTUAL WARNINGS---\n{actual_warnings}\n\n", { - input: input_code, + input: input_formatted, expected_warnings: expected_warnings, actual_warnings: actual_warnings, }); @@ -183,13 +184,13 @@ function run_compress_tests() { } } if (test.expect_stdout) { - var stdout = run_code(make_code(input, output_options)); + var stdout = run_code(input_code); if (test.expect_stdout === true) { test.expect_stdout = stdout; } if (!same_stdout(test.expect_stdout, stdout)) { log("!!! Invalid input or expected stdout\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { - input: input_code, + input: input_formatted, expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", expected: test.expect_stdout, actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", @@ -201,7 +202,7 @@ function run_compress_tests() { stdout = run_code(output); if (!same_stdout(test.expect_stdout, stdout)) { log("!!! failed\n---INPUT---\n{input}\n---EXPECTED {expected_type}---\n{expected}\n---ACTUAL {actual_type}---\n{actual}\n\n", { - input: input_code, + input: input_formatted, expected_type: typeof test.expect_stdout == "string" ? "STDOUT" : "ERROR", expected: test.expect_stdout, actual_type: typeof stdout == "string" ? "STDOUT" : "ERROR", From 2e0dc970037b3a22fb367ab77c5fe506317ee40b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 14:28:40 +0800 Subject: [PATCH 04/38] improve error marker placement (#1644) For AST_UnaryPrefix, points to the operator rather than end of expression. --- lib/parse.js | 2 +- test/mocha/cli.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 99d7ce09..1ccde26a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1457,7 +1457,7 @@ function parse($TEXT, options) { function make_unary(ctor, op, expr) { if ((op == "++" || op == "--") && !is_assignable(expr)) - croak("Invalid use of " + op + " operator"); + croak("Invalid use of " + op + " operator", null, ctor === AST_UnaryPrefix ? expr.start.col - 1 : null); return new ctor({ operator: op, expression: expr }); }; diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 33749045..b956309a 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -298,9 +298,9 @@ describe("bin/uglifyjs", function () { assert.ok(err); assert.strictEqual(stdout, ""); assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ - "Parse error at test/input/invalid/assign_3.js:1,23", + "Parse error at test/input/invalid/assign_3.js:1,18", "console.log(3 || ++this);", - " ^", + " ^", "SyntaxError: Invalid use of ++ operator" ].join("\n")); done(); From f3a1694a4182e1a26d3dd63dd21fcd4b38dafe3a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 14:30:31 +0800 Subject: [PATCH 05/38] fix assignment substitution in sequences (#1643) take side effects of binary boolean operations into account fixes #1639 --- lib/compress.js | 7 ++- test/compress/issue-1639.js | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1639.js diff --git a/lib/compress.js b/lib/compress.js index e75d7c96..b3edb840 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2934,7 +2934,12 @@ merge(Compressor.prototype, { return car; } if (cdr instanceof AST_Binary && !(cdr instanceof AST_Assign)) { - field = cdr.left.is_constant() ? "right" : "left"; + if (cdr.left.is_constant()) { + if (cdr.operator == "||" || cdr.operator == "&&") break; + field = "right"; + } else { + field = "left"; + } } else if (cdr instanceof AST_Call || cdr instanceof AST_Unary && cdr.operator != "++" && cdr.operator != "--") { field = "expression"; diff --git a/test/compress/issue-1639.js b/test/compress/issue-1639.js new file mode 100644 index 00000000..b6a9647f --- /dev/null +++ b/test/compress/issue-1639.js @@ -0,0 +1,88 @@ + +issue_1639_1: { + options = { + booleans: true, + cascade: true, + conditionals: true, + evaluate: true, + join_vars: true, + loops: true, + sequences: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + + var L1 = 5; + while (--L1 > 0) { + if ((--b), false) { + if (b) { + var ignore = 0; + } + } + } + + console.log(a, b); + } + expect: { + for (var a = 100, b = 10, L1 = 5; --L1 > 0;) + if (--b, !1) var ignore = 0; + console.log(a, b); + } + expect_stdout: true +} + +issue_1639_2: { + options = { + booleans: true, + cascade: true, + conditionals: true, + evaluate: true, + join_vars: true, + sequences: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + + function f19() { + if (++a, false) + if (a) + if (++a); + } + f19(); + + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f19() { + ++a, 1; + } + f19(), + console.log(a, b); + } + expect_stdout: true +} + +issue_1639_3: { + options = { + booleans: true, + cascade: true, + conditionals: true, + evaluate: true, + sequences: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + a++ && false && a ? 0 : 0; + console.log(a, b); + } + expect: { + var a = 100, b = 10; + a++, + console.log(a, b); + } + expect_stdout: true +} From 0432a7abb98f3aec871daa88331aa9223979dde3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 18:52:48 +0800 Subject: [PATCH 06/38] fix assignment extraction from conditional (#1651) fixes #1645 fixes #1646 --- lib/compress.js | 16 ++++++-------- test/compress/conditionals.js | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index b3edb840..cbcb7b86 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3550,19 +3550,17 @@ merge(Compressor.prototype, { } var consequent = self.consequent; var alternative = self.alternative; + // if (foo) exp = something; else exp = something_else; + // | + // v + // exp = foo ? something : something_else; if (consequent instanceof AST_Assign && alternative instanceof AST_Assign && consequent.operator == alternative.operator && consequent.left.equivalent_to(alternative.left) - && (!consequent.left.has_side_effects(compressor) - || !self.condition.has_side_effects(compressor)) - ) { - /* - * Stuff like this: - * if (foo) exp = something; else exp = something_else; - * ==> - * exp = foo ? something : something_else; - */ + && (!self.condition.has_side_effects(compressor) + || consequent.operator == "=" + && !consequent.left.has_side_effects(compressor))) { return make_node(AST_Assign, self, { operator: consequent.operator, left: consequent.left, diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 7c81cc80..c5639836 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -893,3 +893,43 @@ equality_conditionals_true: { } expect_stdout: true } + +issue_1645_1: { + options = { + conditionals: true, + } + input: { + var a = 100, b = 10; + (b = a) ? a++ + (b += a) ? b += a : b += a : b ^= a; + console.log(a, b); + } + expect: { + var a = 100, b = 10; + (b = a) ? (a++ + (b += a), b += a) : b ^= a; + console.log(a,b); + } + expect_stdout: true +} + +issue_1645_2: { + options = { + conditionals: true, + } + input: { + var a = 0; + function f() { + return a++; + } + f() ? a += 2 : a += 4; + console.log(a); + } + expect: { + var a = 0; + function f(){ + return a++; + } + f() ? a += 2 : a += 4; + console.log(a); + } + expect_stdout: true +} From ac51d4c5a079dccbc9a6cf69d06f757432c69694 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 19:31:17 +0800 Subject: [PATCH 07/38] fix corner case in `AST_For.init` (#1652) Enforce `null` as value for empty initialisation blocks. fixes #1648 --- lib/compress.js | 1 + lib/output.js | 2 +- test/compress/loops.js | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index cbcb7b86..3804a932 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2327,6 +2327,7 @@ merge(Compressor.prototype, { }; OPT(AST_For, function(self, compressor){ + if (is_empty(self.init)) self.init = null; if (!compressor.option("loops")) return self; if (self.condition) { var cond = self.condition.evaluate(compressor); diff --git a/lib/output.js b/lib/output.js index 767abd4d..c0f10523 100644 --- a/lib/output.js +++ b/lib/output.js @@ -799,7 +799,7 @@ function OutputStream(options) { output.print("for"); output.space(); output.with_parens(function(){ - if (self.init && !(self.init instanceof AST_EmptyStatement)) { + if (self.init) { if (self.init instanceof AST_Definitions) { self.init.print(output); } else { diff --git a/test/compress/loops.js b/test/compress/loops.js index b55c6162..c8d77840 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -440,3 +440,21 @@ issue_186_beautify_bracketize_ie8: { '}', ] } + +issue_1648: { + options = { + join_vars: true, + loops: true, + passes: 2, + sequences: true, + unused: true, + } + input: { + function f() { + x(); + var b = 1; + while (1); + } + } + expect_exact: "function f(){for(x();1;);}" +} From 32283a0def807e0a382d4a6a23cc42e99084e1dd Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 22:09:19 +0800 Subject: [PATCH 08/38] fix cascade of `evaluate` optimisation (#1654) Operator has changed, so break out from rest of the rules. fixes #1649 --- lib/compress.js | 2 ++ test/compress/evaluate.js | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 3804a932..e13985ee 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3292,6 +3292,7 @@ merge(Compressor.prototype, { left: self.left, right: self.right.expression }); + break; } // -a + b => b - a if (self.left instanceof AST_UnaryPrefix @@ -3303,6 +3304,7 @@ merge(Compressor.prototype, { left: self.right, right: self.left.expression }); + break; } case "*": associative = compressor.option("unsafe_math"); diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index e7a6c6b7..7a562055 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -789,3 +789,16 @@ unsafe_charAt_noop: { ); } } + +issue_1649: { + options = { + evaluate: true, + } + input: { + console.log(-1 + -1); + } + expect: { + console.log(-2); + } + expect_stdout: "-2"; +} From b454ce667eb8d9179c74532f308484bdae5392f5 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Mar 2017 23:12:58 +0800 Subject: [PATCH 09/38] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 31ba69d3..44afb511 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,9 @@ -- Bug report or feature request? +- Bug report or feature request? - `uglify-js` version (`uglifyjs -V`) - JavaScript input - ideally as small as possible. - The `uglifyjs` CLI command executed or `minify()` options used. - An example of JavaScript output produced and/or the error or warning. - -Note: the release version of `uglify-js` only supports ES5. Those wishing to minify ES6 should use the experimental [`harmony`](https://github.com/mishoo/UglifyJS2#harmony) branch. + From b1abe92e1aface2ec3d1c3666f8674e120f3b487 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Mar 2017 01:46:12 +0800 Subject: [PATCH 10/38] introduce ufuzz.js (#1655) closes #1647 --- test/ufuzz.js | 225 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 test/ufuzz.js diff --git a/test/ufuzz.js b/test/ufuzz.js new file mode 100644 index 00000000..ac2ded7c --- /dev/null +++ b/test/ufuzz.js @@ -0,0 +1,225 @@ +// ufuzz.js +// derived from https://github.com/qfox/uglyfuzzer by Peter van der Zee +"use strict"; + +// workaround for tty output truncation upon process.exit() +[process.stdout, process.stderr].forEach(function(stream){ + if (stream._handle && stream._handle.setBlocking) + stream._handle.setBlocking(true); +}); + +var vm = require("vm"); +var minify = require("..").minify; + +function run_code(code) { + var stdout = ""; + var original_write = process.stdout.write; + process.stdout.write = function(chunk) { + stdout += chunk; + }; + try { + new vm.Script(code).runInNewContext({ console: console }, { timeout: 5000 }); + return stdout; + } catch (ex) { + return ex; + } finally { + process.stdout.write = original_write; + } +} + +function rng(max) { + return Math.floor(max * Math.random()); +} + +function createFunctionDecls(n, recurmax) { + if (--recurmax < 0) { return ';'; } + var s = ''; + while (--n > 0) { + s += createFunctionDecl(recurmax) + '\n'; + } + return s; +} + +var funcs = 0; +function createFunctionDecl(recurmax) { + if (--recurmax < 0) { return ';'; } + var func = funcs++; + return 'function f' + func + '(){' + createStatements(3, recurmax) + '}\nf' + func + '();'; +} + +function createStatements(n, recurmax) { + if (--recurmax < 0) { return ';'; } + var s = ''; + while (--n > 0) { + s += createStatement(recurmax); + } + return s; +} + +var loops = 0; +function createStatement(recurmax) { + var loop = ++loops; + if (--recurmax < 0) { return ';'; } + switch (rng(7)) { + case 0: + return '{' + createStatement(recurmax) + '}'; + case 1: + return 'if (' + createExpression(recurmax) + ')' + createStatement(recurmax); + case 2: + return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax) + '} while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0);}'; + case 3: + return '{var brake' + loop + ' = 5; while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax) + '}'; + case 4: + return 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax); + case 5: + return ';'; + case 6: + return createExpression() + ';'; + } +} + +function createExpression(recurmax) { + if (--recurmax < 0) { return '0'; } + switch (rng(8)) { + case 0: + return '(' + createUnaryOp() + 'a)'; + case 1: + return '(a' + (Math.random() > 0.5 ? '++' : '--') + ')'; + case 2: + return '(b ' + createAssignment() + ' a)'; + case 3: + return '(' + Math.random() + ' > 0.5 ? a : b)'; + case 4: + return createExpression(recurmax) + createBinaryOp() + createExpression(recurmax); + case 5: + return createValue(); + case 6: + return '(' + createExpression(recurmax) + ')'; + case 7: + return createExpression(recurmax) + '?(' + createExpression(recurmax) + '):(' + createExpression(recurmax) + ')'; + } +} + +function createValue() { + var values = [ + 'true', + 'false', + '22', + '0', + '(-1)', + 'NaN', + 'undefined', + 'null', + '"foo"', + '"bar"' ]; + return values[rng(values.length)]; +} + +function createBinaryOp() { + switch (rng(6)) { + case 0: + return '+'; + case 1: + return '-'; + case 2: + return ','; + case 3: + return '&&'; + case 4: + return '||'; + case 5: + return '^'; + } +} + +function createAssignment() { + switch (rng(4)) { + case 0: + return '='; + case 1: + return '-='; + case 2: + return '^='; + case 3: + return '+='; + } +} + +function createUnaryOp() { + switch (rng(4)) { + case 0: + return '--'; + case 1: + return '++'; + case 2: + return '~'; + case 3: + return '!'; + } +} + +function log() { + console.log("//============================================================="); + console.log("// original code"); + console.log("//"); + console.log(original_code); + console.log(); + console.log(); + console.log("//-------------------------------------------------------------"); + console.log("// original code (beautify'd)"); + console.log("//"); + console.log(beautify_code); + console.log(); + console.log(); + console.log("//-------------------------------------------------------------"); + console.log("// uglified code"); + console.log("//"); + console.log(uglify_code); + console.log(); + console.log(); + console.log("original result:"); + console.log(original_result); + console.log("beautified result:"); + console.log(beautify_result); + console.log("uglified result:"); + console.log(uglify_result); +} + +var num_iterations = +process.argv[2] || 1/0; +var verbose = !!process.argv[3]; +for (var round = 0; round < num_iterations; round++) { + process.stdout.write(round + " of " + num_iterations + "\r"); + var original_code = [ + "var a = 100, b = 10;", + createFunctionDecls(rng(3) + 1, 10), + "console.log(a, b);" + ].join("\n"); + var beautify_code = minify(original_code, { + fromString: true, + mangle: false, + compress: false, + output: { + beautify: true, + bracketize: true, + }, + }).code; + + var uglify_code = minify(beautify_code, { + fromString: true, + mangle: false, + compress: { + passes: 3, + }, + output: { + beautify: true, + bracketize: true, + }, + }).code; + + var original_result = run_code(original_code); + var beautify_result = run_code(beautify_code); + var uglify_result = run_code(uglify_code); + var ok = original_result == beautify_result && original_result == uglify_result; + if (verbose || !ok) log(); + if (!ok) process.exit(1); +} From a30092e20f6a1e23706f87ca998121b8832a57bb Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Mar 2017 03:18:36 +0800 Subject: [PATCH 11/38] fix invalid `AST_For.init` (#1657) Turns out the only place in `Compressor` which can generate invalid `AST_For.init` is within `drop_unused()`, so focus the fix-up efforts. supercedes #1652 fixes #1656 --- lib/compress.js | 4 +++- test/compress/drop-unused.js | 14 +++++++++++ test/compress/issue-1656.js | 45 ++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1656.js diff --git a/lib/compress.js b/lib/compress.js index e13985ee..ab7cca6f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1961,6 +1961,9 @@ merge(Compressor.prototype, { return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { body: body }); + } else if (is_empty(node.init)) { + node.init = null; + return node; } } if (node instanceof AST_Scope && node !== self) @@ -2327,7 +2330,6 @@ merge(Compressor.prototype, { }; OPT(AST_For, function(self, compressor){ - if (is_empty(self.init)) self.init = null; if (!compressor.option("loops")) return self; if (self.condition) { var cond = self.condition.evaluate(compressor); diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 9f3bf77d..fabf8d9f 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -791,3 +791,17 @@ issue_1583: { } } } + +issue_1656: { + options = { + toplevel: true, + unused: true, + } + beautify = { + beautify: true, + } + input: { + for(var a=0;;); + } + expect_exact: "for (;;) ;" +} diff --git a/test/compress/issue-1656.js b/test/compress/issue-1656.js new file mode 100644 index 00000000..8b683a28 --- /dev/null +++ b/test/compress/issue-1656.js @@ -0,0 +1,45 @@ +f7: { + options = { + booleans: true, + cascade: true, + collapse_vars: true, + comparisons: true, + conditionals: true, + dead_code: true, + drop_debugger: true, + evaluate: true, + hoist_funs: true, + if_return: true, + join_vars: true, + loops: true, + negate_iife: true, + passes: 3, + properties: true, + reduce_vars: true, + sequences: true, + side_effects: true, + toplevel: true, + unused: true, + } + beautify = { + beautify: true, + } + input: { + var a = 100, b = 10; + function f22464() { + var brake146670 = 5; + while (((b = a) ? !a : ~a ? null : b += a) && --brake146670 > 0) { + } + } + f22464(); + console.log(a, b); + } + expect_exact: [ + "var a = 100, b = 10;", + "", + "!function() {", + " for (;b = a, !1; ) ;", + "}(), console.log(a, b);", + ] + expect_stdout: true +} From 491f16c766c92e20260b99696b6081f333ceaf0f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Mar 2017 03:21:16 +0800 Subject: [PATCH 12/38] v2.8.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f095c793..ce6f309c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.15", + "version": "2.8.16", "engines": { "node": ">=0.8.0" }, From 8ca2401ebe024287ce1133d2707b1a8ce91f4e6c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Mar 2017 16:21:42 +0800 Subject: [PATCH 13/38] fix `dead_code` on `AST_Switch` (#1667) Need to call `extract_declarations_from_unreachable_code()`. fixes #1663 --- lib/compress.js | 32 ++++++++++++++++++-------------- test/compress/switch.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index ab7cca6f..47eb4d73 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2529,14 +2529,14 @@ merge(Compressor.prototype, { // no need to descend these node types return node; } - else if (node instanceof AST_Switch && node === self) { + else if (node === self) { node = node.clone(); descend(node, this); return ruined ? node : make_node(AST_BlockStatement, node, { - body: node.body.reduce(function(a, branch){ - return a.concat(branch.body); - }, []) - }).transform(compressor); + body: node.body.map(function(stat) { + return stat instanceof AST_SwitchBranch ? make_node(AST_BlockStatement, stat, stat) : stat; + }) + }).optimize(compressor); } else if (node instanceof AST_If || node instanceof AST_Try) { var save = in_if; @@ -2559,10 +2559,10 @@ merge(Compressor.prototype, { } if (in_block) return node; stopped = true; - return in_list ? MAP.skip : make_node(AST_EmptyStatement, node); + return skip(node); } else if (node instanceof AST_SwitchBranch && this.parent() === self) { - if (stopped) return MAP.skip; + if (stopped) return skip(node); if (node instanceof AST_Case) { var exp = node.expression.evaluate(compressor); if (exp === node.expression) { @@ -2572,16 +2572,20 @@ merge(Compressor.prototype, { if (exp === value || started) { started = true; if (aborts(node)) stopped = true; - descend(node, this); - return node; - } - return MAP.skip; + } else return skip(node); } - descend(node, this); - return node; + } + + function skip(node) { + var a = []; + extract_declarations_from_unreachable_code(compressor, node, a); + return in_list ? MAP.splice(a) : make_node(AST_BlockStatement, node, { + body: a + }); } }); - tt.stack = compressor.stack.slice(); // so that's able to see parent nodes + // allow transform() to view the whole AST + tt.stack = compressor.stack.slice(0, -1); self = self.transform(tt); } catch(ex) { if (ex !== self) throw ex; diff --git a/test/compress/switch.js b/test/compress/switch.js index 62e39cf7..01d45f78 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -258,3 +258,35 @@ keep_default: { } } } + +issue_1663: { + options = { + dead_code: true, + evaluate: true, + } + input: { + var a = 100, b = 10; + function f() { + switch (1) { + case 1: + b = a++; + return ++b; + default: + var b; + } + } + f(); + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f() { + b = a++; + return ++b; + var b; + } + f(); + console.log(a, b); + } + expect_stdout: true +} From 6e86ee950d98ebacef7e02515e34d758c4f836a1 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Mar 2017 17:40:18 +0800 Subject: [PATCH 14/38] fix typeof side-effects (#1669) `has_side_effects()` does not take `typeof`'s magical power of not tripping over undeclared variable into account. fixes #1668 --- lib/compress.js | 4 ++-- test/compress/typeof.js | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 47eb4d73..e12c5eb0 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3013,10 +3013,10 @@ merge(Compressor.prototype, { // typeof always returns a non-empty string, thus it's // always true in booleans compressor.warn("Boolean expression always true [{file}:{line},{col}]", self.start); - return make_node(AST_Seq, self, { + return (e instanceof AST_SymbolRef ? make_node(AST_True, self) : make_node(AST_Seq, self, { car: e, cdr: make_node(AST_True, self) - }).optimize(compressor); + })).optimize(compressor); } } // avoids infinite recursion of numerals diff --git a/test/compress/typeof.js b/test/compress/typeof.js index 7bf8e5e3..60f3d1d0 100644 --- a/test/compress/typeof.js +++ b/test/compress/typeof.js @@ -48,3 +48,15 @@ typeof_in_boolean_context: { foo(); } } + +issue_1668: { + options = { + booleans: true, + } + input: { + if (typeof bar); + } + expect: { + if (!0); + } +} From 0a65de89b97731cd83a077303dd0511ba7ff7151 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Mar 2017 21:17:30 +0800 Subject: [PATCH 15/38] fix `reduce_vars` on `AST_Switch` (#1671) Take conditional nature of switch branches into account. fixes #1670 --- lib/compress.js | 2 +- test/compress/reduce_vars.js | 180 +++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index e12c5eb0..bea9c9a4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -341,7 +341,7 @@ merge(Compressor.prototype, { pop(); return true; } - if (node instanceof AST_Catch) { + if (node instanceof AST_Catch || node instanceof AST_SwitchBranch) { push(); descend(); pop(); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 3d5612cf..943dd290 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1364,3 +1364,183 @@ issue_1606: { } } } + +issue_1670_1: { + options = { + comparisons: true, + conditionals: true, + evaluate: true, + dead_code: true, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + default: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + var a; + void 0 === a ? console.log("PASS") : console.log("FAIL"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_2: { + options = { + conditionals: true, + evaluate: true, + dead_code: true, + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + default: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + console.log("PASS"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_3: { + options = { + comparisons: true, + conditionals: true, + evaluate: true, + dead_code: true, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + case 1: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + var a; + void 0 === a ? console.log("PASS") : console.log("FAIL"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_4: { + options = { + conditionals: true, + evaluate: true, + dead_code: true, + passes: 2, + reduce_vars: true, + unused: true, + } + input: { + (function f() { + switch (1) { + case 0: + var a = true; + break; + case 1: + if (typeof a === "undefined") console.log("PASS"); + else console.log("FAIL"); + } + })(); + } + expect: { + (function() { + console.log("PASS"); + })(); + } + expect_stdout: "PASS" +} + +issue_1670_5: { + options = { + dead_code: true, + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + (function(a) { + switch (1) { + case a: + console.log(a); + break; + default: + console.log(2); + break; + } + })(1); + } + expect: { + (function() { + console.log(1); + })(); + } + expect_stdout: "1" +} + +issue_1670_6: { + options = { + dead_code: true, + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + (function(a) { + switch (1) { + case a = 1: + console.log(a); + break; + default: + console.log(2); + break; + } + })(1); + } + expect: { + (function(a) { + switch (1) { + case a = 1: + console.log(a); + break; + default: + console.log(2); + } + })(1); + } + expect_stdout: "1" +} From b19aa58cff73ea59438346db094de4a54463d669 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Mar 2017 23:03:26 +0800 Subject: [PATCH 16/38] fix `has_side_effects()` (#1675) `AST_Try` is an `AST_Block`, so besides try block we also need to inspect catch and finally blocks for possible side effects. Also extend this functionality to handle `AST_If` and `AST_LabeledStatement` while we are at it. fixes #1673 --- lib/compress.js | 35 ++++++---- test/compress/issue-1673.js | 127 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 test/compress/issue-1673.js diff --git a/lib/compress.js b/lib/compress.js index bea9c9a4..805a84a7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1600,14 +1600,29 @@ merge(Compressor.prototype, { return false; }); - def(AST_Block, function(compressor){ - for (var i = this.body.length; --i >= 0;) { - if (this.body[i].has_side_effects(compressor)) + function any(list, compressor) { + for (var i = list.length; --i >= 0;) + if (list[i].has_side_effects(compressor)) return true; - } return false; - }); + } + def(AST_Block, function(compressor){ + return any(this.body, compressor); + }); + def(AST_Try, function(compressor){ + return any(this.body, compressor) + || this.bcatch && this.bcatch.has_side_effects(compressor) + || this.bfinally && this.bfinally.has_side_effects(compressor); + }); + def(AST_If, function(compressor){ + return this.condition.has_side_effects(compressor) + || this.body && this.body.has_side_effects(compressor) + || this.alternative && this.alternative.has_side_effects(compressor); + }); + def(AST_LabeledStatement, function(compressor){ + return this.body.has_side_effects(compressor); + }); def(AST_SimpleStatement, function(compressor){ return this.body.has_side_effects(compressor); }); @@ -1633,19 +1648,13 @@ merge(Compressor.prototype, { return this.global() && this.undeclared(); }); def(AST_Object, function(compressor){ - for (var i = this.properties.length; --i >= 0;) - if (this.properties[i].has_side_effects(compressor)) - return true; - return false; + return any(this.properties, compressor); }); def(AST_ObjectProperty, function(compressor){ return this.value.has_side_effects(compressor); }); def(AST_Array, function(compressor){ - for (var i = this.elements.length; --i >= 0;) - if (this.elements[i].has_side_effects(compressor)) - return true; - return false; + return any(this.elements, compressor); }); def(AST_Dot, function(compressor){ if (!compressor.option("pure_getters")) return true; diff --git a/test/compress/issue-1673.js b/test/compress/issue-1673.js new file mode 100644 index 00000000..59686abf --- /dev/null +++ b/test/compress/issue-1673.js @@ -0,0 +1,127 @@ +side_effects_catch: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f() { + function g() { + try { + throw 0; + } catch (e) { + console.log("PASS"); + } + } + g(); + } + f(); + } + expect: { + function f() { + (function() { + try { + throw 0; + } catch (e) { + console.log("PASS"); + } + })(); + } + f(); + } + expect_stdout: "PASS" +} + +side_effects_else: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f(x) { + function g() { + if (x); + else console.log("PASS"); + } + g(); + } + f(0); + } + expect: { + function f(x) { + (function() { + if (x); + else console.log("PASS"); + })(); + } + f(0); + } + expect_stdout: "PASS" +} + +side_effects_finally: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f() { + function g() { + try { + } catch (e) { + } finally { + console.log("PASS"); + } + } + g(); + } + f(); + } + expect: { + function f() { + (function() { + try { + } catch (e) { + } finally { + console.log("PASS"); + } + })(); + } + f(); + } + expect_stdout: "PASS" +} + +side_effects_label: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f(x) { + function g() { + L: { + console.log("PASS"); + break L; + } + } + g(); + } + f(0); + } + expect: { + function f(x) { + (function() { + L: { + console.log("PASS"); + break L; + } + })(); + } + f(0); + } + expect_stdout: "PASS" +} From f83d370f57c49e5112ac5ce74e27a0573265baf6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Mar 2017 05:15:46 +0800 Subject: [PATCH 17/38] improve switch optimisations (#1677) - correctly determine reachability of (default) branches - gracefully handle multiple default branches - optimise branches with duplicate bodies fixes #376 fixes #441 fixes #1674 --- lib/compress.js | 181 +++++++++++++++++++--------------------- test/compress/switch.js | 146 +++++++++++++++++++++++++++++++- 2 files changed, 232 insertions(+), 95 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 805a84a7..c57287bf 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2504,109 +2504,102 @@ merge(Compressor.prototype, { }); OPT(AST_Switch, function(self, compressor){ - if (self.body.length == 0 && compressor.option("conditionals")) { - return make_node(AST_SimpleStatement, self, { - body: self.expression - }).transform(compressor); + var branch; + var value = self.expression.evaluate(compressor); + if (value !== self.expression) { + var expression = make_node_from_constant(value, self.expression).transform(compressor); + self.expression = best_of_expression(expression, self.expression); } - for(;;) { - var last_branch = self.body[self.body.length - 1]; - if (last_branch) { - var stat = last_branch.body[last_branch.body.length - 1]; // last statement - if (stat instanceof AST_Break && loop_body(compressor.loopcontrol_target(stat.label)) === self) - last_branch.body.pop(); - if (last_branch instanceof AST_Default && last_branch.body.length == 0) { - self.body.pop(); - continue; + if (compressor.option("dead_code")) { + var blocks = Object.create(null); + var decl = []; + var body = []; + var default_branch; + var exact_match; + var fallthrough; + for (var i = 0, len = self.body.length; i < len && !exact_match; i++) { + branch = self.body[i]; + if (branch instanceof AST_Default) { + if (!default_branch) default_branch = branch; + else if (!fallthrough) { + extract_declarations_from_unreachable_code(compressor, branch, decl); + continue; + } + } else if (value !== self.expression) { + var exp = branch.expression.evaluate(compressor); + if (exp === value) { + exact_match = branch; + if (default_branch) { + body.splice(body.indexOf(default_branch), 1); + extract_declarations_from_unreachable_code(compressor, default_branch, decl); + } + } else if (exp !== branch.expression && !fallthrough) { + extract_declarations_from_unreachable_code(compressor, branch, decl); + continue; + } + } + if (aborts(branch)) { + var key = make_node(AST_BlockStatement, branch, branch).print_to_string(); + var block; + if (!fallthrough && (block = blocks[key])) { + block.body = []; + body.splice(body.indexOf(block) + 1, 0, branch); + } else { + body.push(branch); + } + blocks[key] = branch; + fallthrough = false; + } else { + body.push(branch); + fallthrough = true; } } - break; + for (; i < len && fallthrough; i++) { + branch = self.body[i]; + if (branch instanceof AST_Case) { + exact_match.body.push(make_node(AST_SimpleStatement, branch.expression, { + body: branch.expression + })); + } + exact_match.body = exact_match.body.concat(branch.body); + fallthrough = !aborts(exact_match); + } + while (i < len) extract_declarations_from_unreachable_code(compressor, self.body[i++], decl); + if (body.length == 0) return make_node(AST_BlockStatement, self, { + body: decl + }).optimize(compressor); + body[0].body = decl.concat(body[0].body); + self.body = body; } - var value = self.expression.evaluate(compressor); - out: if (value !== self.expression) try { - // constant expression - var expression = make_node_from_constant(value, self.expression); - self.expression = best_of_expression(expression, self.expression); - if (!compressor.option("dead_code")) break out; - var in_if = false; - var in_block = false; - var started = false; - var stopped = false; - var ruined = false; - var tt = new TreeTransformer(function(node, descend, in_list){ - if (node instanceof AST_Lambda || node instanceof AST_SimpleStatement) { - // no need to descend these node types - return node; - } - else if (node === self) { - node = node.clone(); - descend(node, this); - return ruined ? node : make_node(AST_BlockStatement, node, { - body: node.body.map(function(stat) { - return stat instanceof AST_SwitchBranch ? make_node(AST_BlockStatement, stat, stat) : stat; - }) - }).optimize(compressor); - } - else if (node instanceof AST_If || node instanceof AST_Try) { - var save = in_if; - in_if = !in_block; - descend(node, this); - in_if = save; - return node; - } - else if (node instanceof AST_StatementWithBody || node instanceof AST_Switch) { - var save = in_block; - in_block = true; - descend(node, this); - in_block = save; - return node; - } - else if (node instanceof AST_Break && this.loopcontrol_target(node.label) === self) { - if (in_if) { - ruined = true; - return node; - } - if (in_block) return node; - stopped = true; - return skip(node); - } - else if (node instanceof AST_SwitchBranch && this.parent() === self) { - if (stopped) return skip(node); - if (node instanceof AST_Case) { - var exp = node.expression.evaluate(compressor); - if (exp === node.expression) { - // got a case with non-constant expression, baling out - throw self; - } - if (exp === value || started) { - started = true; - if (aborts(node)) stopped = true; - } else return skip(node); - } - } - - function skip(node) { - var a = []; - extract_declarations_from_unreachable_code(compressor, node, a); - return in_list ? MAP.splice(a) : make_node(AST_BlockStatement, node, { - body: a - }); - } + while (branch = self.body[self.body.length - 1]) { + var stat = branch.body[branch.body.length - 1]; + if (stat instanceof AST_Break && compressor.loopcontrol_target(stat.label) === self) + branch.body.pop(); + if (branch.body.length + || branch instanceof AST_Case + && branch.expression.has_side_effects(compressor)) break; + self.body.pop(); + } + if (compressor.option("conditionals") && self.body.length == 0) { + return make_node(AST_SimpleStatement, self, { + body: self.expression + }).optimize(compressor); + } + if (body && body.length == 1 && (body[0] === exact_match || body[0] === default_branch)) { + var has_break = false; + var tw = new TreeWalker(function(node) { + if (has_break + || node instanceof AST_Lambda + || node instanceof AST_SimpleStatement) return true; + if (node instanceof AST_Break && tw.loopcontrol_target(node.label) === self) + has_break = true; }); - // allow transform() to view the whole AST - tt.stack = compressor.stack.slice(0, -1); - self = self.transform(tt); - } catch(ex) { - if (ex !== self) throw ex; + self.walk(tw); + if (!has_break) return make_node(AST_BlockStatement, self, body[0]).optimize(compressor); } return self; }); - OPT(AST_Case, function(self, compressor){ - self.body = tighten_body(self.body, compressor); - return self; - }); - OPT(AST_Try, function(self, compressor){ self.body = tighten_body(self.body, compressor); return self; diff --git a/test/compress/switch.js b/test/compress/switch.js index 01d45f78..7d3d7d13 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -23,6 +23,7 @@ constant_switch_2: { } expect: { foo(); + 2; bar(); } } @@ -117,6 +118,7 @@ constant_switch_6: { x(); if (foo) break OUT; y(); + 2; bar(); } } @@ -155,6 +157,7 @@ constant_switch_7: { console.log(x); } y(); + 2; bar(); } } @@ -203,6 +206,7 @@ constant_switch_9: { x(); for (;;) if (foo) break OUT; y(); + 2; bar(); def(); } @@ -281,12 +285,152 @@ issue_1663: { expect: { var a = 100, b = 10; function f() { + var b; b = a++; return ++b; - var b; } f(); console.log(a, b); } expect_stdout: true } + +drop_case: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case 'bar': baz(); break; + case 'moo': + break; + } + } + expect: { + switch (foo) { + case 'bar': baz(); + } + } +} + +keep_case: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case 'bar': baz(); break; + case moo: + break; + } + } + expect: { + switch (foo) { + case 'bar': baz(); break; + case moo: + } + } +} + +issue_376: { + options = { + dead_code: true, + evaluate: true, + } + input: { + switch (true) { + case boolCondition: + console.log(1); + break; + case false: + console.log(2); + break; + } + } + expect: { + switch (true) { + case boolCondition: + console.log(1); + } + } +} + +issue_441_1: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case bar: + qux(); + break; + case baz: + qux(); + break; + default: + qux(); + break; + } + } + expect: { + switch (foo) { + case bar: + case baz: + default: + qux(); + } + } +} + +issue_441_2: { + options = { + dead_code: true, + } + input: { + switch (foo) { + case bar: + // TODO: Fold into the case below + qux(); + break; + case fall: + case baz: + qux(); + break; + default: + qux(); + break; + } + } + expect: { + switch (foo) { + case bar: + qux(); + break; + case fall: + case baz: + default: + qux(); + } + } +} + +issue_1674: { + options = { + dead_code: true, + evaluate: true, + } + input: { + switch (0) { + default: + console.log("FAIL"); + break; + case 0: + console.log("PASS"); + break; + } + } + expect: { + console.log("PASS"); + } + expect_stdout: "PASS" +} From adb0e882e926249eada4f8f5afaae01aa469face Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Sun, 26 Mar 2017 06:04:50 +0200 Subject: [PATCH 18/38] Improve fuzzer. :) (#1665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @qfox Put value constants in a global constant 74c0fb9 @qfox And the other string based values as well a5033c5 @qfox Be more strict about parameters, allow max to be optional 9c7ce70 @qfox Support a `V` (capital) flag to only log out at intervals 2d822c7 @qfox Fewer magic variables a6a9a7c @qfox Fix decrement such that a function is created when n=1 7e4b017 @qfox Add more values 64e596e @qfox Make `b` appear more often d33191a @qfox Add functions that contain (only..) functions 29a86e3 @qfox Allow the block statement to contain multiple statements 7570484 @qfox Make the interval count a constant d587ad8 @qfox Enable mangling, disable post-processing … 4dc8d35 @qfox Add more simple value that may trigger syntactic errors 8496d58 @qfox Add `else` to some `if` statements a4aed65 @qfox Move iife to expr generator, fix missing recursion arg e453159 @qfox Improve output on error where it wasnt printing the last code properly 4565a1a @qfox Add switch statement to generator ceafa76 @qfox Add var statement, support optional comma for expr generator b83921b @qfox Expression generator should use a simple value instead of `0` as recu… … 9d1a5c7 @qfox const -> var to keep things es5... 0143099 @qfox Add more simple values that may trigger edge cases 5e124f1 @qfox Add central name generator, take special care for global functions aeb7682 @qfox Add some `return` and function declaration cases to statement generator 6c9c3cc @qfox Exclude switches from generator for now 91124b2 Put value constants in a global constant And the other string based values as well Be more strict about parameters, allow max to be optional Support a `V` (capital) flag to only log out at intervals Fewer magic variables Fix decrement such that a function is created when n=1 Add more values Make `b` appear more often Add functions that contain (only..) functions Allow the block statement to contain multiple statements Make the interval count a constant Enable mangling, disable post-processing Mangling is kind of the whole point... Similarly, to beautify the minified code afterwards may supress bugs so it's probably best not to beautify the code prematurely. And there's no point anyways since you won't see it most of the time and only care about the main input anyways. Add more simple value that may trigger syntactic errors Add `else` to some `if` statements Move iife to expr generator, fix missing recursion arg Improve output on error where it wasnt printing the last code properly Add switch statement to generator Add var statement, support optional comma for expr generator Expression generator should use a simple value instead of `0` as recursion default const -> var to keep things es5... Add more simple values that may trigger edge cases Add central name generator, take special care for global functions Add some `return` and function declaration cases to statement generator Exclude switches from generator for now Enable switch generation because #1667 was merged Add typeof generator Add some elision tests Add a new edge case that returns an object explicitly Add all binary ops to try and cover more paths Forgot four binops and added `Math` to var name pool Harden the incremental pre/postfix tests Improve switch generator, allow `default` to appear at any clause index Add try/catch/finally generation Prevent function statements being generated Add edge case with decremental op and a group Disable switch generation until #1679 and #1680 are solved Only allow `default` clause as last clause for now Tentatively enable `throw`, `break` and `continue` statements when in valid contexts --- test/ufuzz.js | 426 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 328 insertions(+), 98 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index ac2ded7c..c56c6224 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -11,6 +11,116 @@ var vm = require("vm"); var minify = require("..").minify; +var MAX_GENERATED_FUNCTIONS_PER_RUN = 1; +var MAX_GENERATION_RECURSION_DEPTH = 15; +var INTERVAL_COUNT = 100; + +var VALUES = [ + 'true', + 'false', + '22', + '0', + '-0', // 0/-0 !== 0 + '23..toString()', + '24 .toString()', + '25. ', + '0x26.toString()', + '(-1)', + 'NaN', + 'undefined', + 'Infinity', + 'null', + '[]', + '[,0][1]', // an array with elisions... but this is always false + '([,0].length === 2)', // an array with elisions... this is always true + '({})', // wrapped the object causes too many syntax errors in statements + '"foo"', + '"bar"' ]; + +var BINARY_OPS_NO_COMMA = [ + ' + ', // spaces needed to disambiguate with ++ cases (could otherwise cause syntax errors) + ' - ', + '/', + '*', + '&', + '|', + '^', + '<<', + '>>', + '>>>', + '%', + '&&', + '||', + '^' ]; + +var BINARY_OPS = [','].concat(BINARY_OPS_NO_COMMA); + +var ASSIGNMENTS = [ + '=', + '=', + '=', + '=', + '=', + '=', + + '==', + '!=', + '===', + '!==', + '+=', + '-=', + '*=', + '/=', + '&=', + '|=', + '^=', + '<<=', + '>>=', + '>>>=', + '%=' ]; + +var UNARY_OPS = [ + '--', + '++', + '~', + '!', + 'void ', + 'delete ', // should be safe, even `delete foo` and `delete f()` shouldn't crash + ' - ', + ' + ' ]; + +var NO_COMMA = true; +var MAYBE = true; +var NESTED = true; +var CAN_THROW = true; +var CANNOT_THROW = false; +var CAN_BREAK = true; +var CAN_CONTINUE = true; + +var VAR_NAMES = [ + 'foo', + 'bar', + 'a', + 'b', + 'undefined', // fun! + 'eval', // mmmm, ok, also fun! + 'NaN', // mmmm, ok, also fun! + 'Infinity', // the fun never ends! + 'arguments', // this one is just creepy + 'Math', // since Math is assumed to be a non-constructor/function it may trip certain cases + 'let' ]; // maybe omit this, it's more a parser problem than minifier + +var TYPEOF_OUTCOMES = [ + 'undefined', + 'string', + 'number', + 'object', + 'boolean', + 'special', + 'unknown', + 'symbol', + 'crap' ]; + function run_code(code) { var stdout = ""; var original_write = process.stdout.write; @@ -31,135 +141,241 @@ function rng(max) { return Math.floor(max * Math.random()); } -function createFunctionDecls(n, recurmax) { +function createFunctionDecls(n, recurmax, nested) { if (--recurmax < 0) { return ';'; } var s = ''; - while (--n > 0) { - s += createFunctionDecl(recurmax) + '\n'; + while (n-- > 0) { + s += createFunctionDecl(recurmax, nested) + '\n'; } return s; } var funcs = 0; -function createFunctionDecl(recurmax) { +function createFunctionDecl(recurmax, nested) { if (--recurmax < 0) { return ';'; } var func = funcs++; - return 'function f' + func + '(){' + createStatements(3, recurmax) + '}\nf' + func + '();'; + var name = rng(5) > 0 ? 'f' + func : createVarName(); + if (name === 'a' || name === 'b') name = 'f' + func; // quick hack to prevent assignment to func names of being called + if (!nested && name === 'undefined' || name === 'NaN' || name === 'Infinity') name = 'f' + func; // cant redefine these in global space + var s = ''; + if (rng(5) === 1) { + // functions with functions. lower the recursion to prevent a mess. + s = 'function ' + name + '(){' + createFunctionDecls(rng(5) + 1, Math.ceil(recurmax / 2), NESTED) + '}\n'; + } else { + // functions with statements + s = 'function ' + name + '(){' + createStatements(3, recurmax) + '}\n'; + } + + if (nested) s = '!' + nested; // avoid "function statements" (decl inside statements) + else s += name + '();' + + return s; } -function createStatements(n, recurmax) { +function createStatements(n, recurmax, canThrow, canBreak, canContinue) { if (--recurmax < 0) { return ';'; } var s = ''; while (--n > 0) { - s += createStatement(recurmax); + s += createStatement(recurmax, canThrow, canBreak, canContinue); } return s; } var loops = 0; -function createStatement(recurmax) { +function createStatement(recurmax, canThrow, canBreak, canContinue) { var loop = ++loops; if (--recurmax < 0) { return ';'; } - switch (rng(7)) { + switch (rng(16)) { case 0: - return '{' + createStatement(recurmax) + '}'; + return '{' + createStatements(rng(5) + 1, recurmax, canThrow, canBreak, canContinue) + '}'; case 1: - return 'if (' + createExpression(recurmax) + ')' + createStatement(recurmax); + return 'if (' + createExpression(recurmax) + ')' + createStatement(recurmax, canThrow, canBreak, canContinue) + (rng(2) === 1 ? ' else ' + createStatement(recurmax, canThrow, canBreak, canContinue) : ''); case 2: - return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax) + '} while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0);}'; + return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE) + '} while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0);}'; case 3: - return '{var brake' + loop + ' = 5; while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax) + '}'; + return '{var brake' + loop + ' = 5; while ((' + createExpression(recurmax) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE) + '}'; case 4: - return 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax); + return 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE); case 5: return ';'; case 6: - return createExpression() + ';'; + return createExpression(recurmax) + ';'; + case 7: + return ';'; // TODO: disabled until some switch issues are resolved + // note: case args are actual expressions + // note: default does not _need_ to be last + return 'switch (' + createExpression(recurmax) + ') { ' + createSwitchParts(recurmax, 4) + '}'; + case 8: + return 'var ' + createVarName() + ';'; + case 9: + // initializer can only have one expression + return 'var ' + createVarName() + ' = ' + createExpression(recurmax, NO_COMMA) + ';'; + case 10: + // initializer can only have one expression + return 'var ' + createVarName() + ' = ' + createExpression(recurmax, NO_COMMA) + ', ' + createVarName() + ' = ' + createExpression(recurmax, NO_COMMA) + ';'; + case 11: + if (canBreak && rng(5) === 0) return 'break;'; + if (canContinue && rng(5) === 0) return 'continue;'; + return 'return;'; + case 12: + // must wrap in curlies to prevent orphaned `else` statement + if (canThrow && rng(5) === 0) return '{ throw ' + createExpression(recurmax) + '}'; + return '{ return ' + createExpression(recurmax) + '}'; + case 13: + // this is actually more like a parser test, but perhaps it hits some dead code elimination traps + // must wrap in curlies to prevent orphaned `else` statement + if (canThrow && rng(5) === 0) return '{ throw\n' + createExpression(recurmax) + '}'; + return '{ return\n' + createExpression(recurmax) + '}'; + case 14: + // "In non-strict mode code, functions can only be declared at top level, inside a block, or ..." + // (dont both with func decls in `if`; it's only a parser thing because you cant call them without a block) + return '{' + createFunctionDecl(recurmax, NESTED) + '}'; + case 15: + return ';'; + // catch var could cause some problems + // note: the "blocks" are syntactically mandatory for try/catch/finally + var s = 'try {' + createStatement(recurmax, CAN_THROW, canBreak, canContinue) + ' }'; + var n = rng(3); // 0=only catch, 1=only finally, 2=catch+finally + if (n !== 1) s += ' catch (' + createVarName() + ') { ' + createStatements(3, recurmax, canBreak, canContinue) + ' }'; + if (n !== 0) s += ' finally { ' + createStatements(3, recurmax, canBreak, canContinue) + ' }'; + return s; } } -function createExpression(recurmax) { - if (--recurmax < 0) { return '0'; } - switch (rng(8)) { +function createSwitchParts(recurmax, n) { + var hadDefault = false; + var s = ''; + while (n-- > 0) { + hadDefault = n > 0; + if (hadDefault || rng(4) > 0) { + s += '' + + 'case ' + createExpression(recurmax) + ':\n' + + createStatements(rng(3) + 1, recurmax, CANNOT_THROW, CAN_BREAK) + + '\n' + + (rng(10) > 0 ? ' break;' : '/* fall-through */') + + '\n'; + } else { + hadDefault = true; + s += '' + + 'default:\n' + + createStatements(rng(3) + 1, recurmax, CANNOT_THROW, CAN_BREAK) + + '\n'; + } + } + return s; +} + +function createExpression(recurmax, noComma) { + if (--recurmax < 0) { + return createValue(); // note: should return a simple non-recursing expression value! + } + switch (rng(12)) { case 0: - return '(' + createUnaryOp() + 'a)'; + return '(' + createUnaryOp() + (rng(2) === 1 ? 'a' : 'b') + ')'; case 1: - return '(a' + (Math.random() > 0.5 ? '++' : '--') + ')'; + return '(a' + (rng(2) == 1 ? '++' : '--') + ')'; case 2: return '(b ' + createAssignment() + ' a)'; case 3: - return '(' + Math.random() + ' > 0.5 ? a : b)'; + return '(' + rng(2) + ' === 1 ? a : b)'; case 4: - return createExpression(recurmax) + createBinaryOp() + createExpression(recurmax); + return createExpression(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma); case 5: return createValue(); case 6: return '(' + createExpression(recurmax) + ')'; case 7: - return createExpression(recurmax) + '?(' + createExpression(recurmax) + '):(' + createExpression(recurmax) + ')'; + return createExpression(recurmax, noComma) + '?(' + createExpression(recurmax) + '):(' + createExpression(recurmax) + ')'; + case 8: + switch(rng(4)) { + case 0: + return '(function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '})()'; + case 1: + return '+function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + case 2: + return '!function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + case 3: + return 'void function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + default: + return 'void function ' + createVarName(MAYBE) + '(){' + createStatements(rng(5) + 1, recurmax) + '}'; + } + case 9: + return createTypeofExpr(recurmax); + case 10: + // you could statically infer that this is just `Math`, regardless of the other expression + // I don't think Uglify does this at this time... + return ''+ + 'new function(){ \n' + + (rng(2) === 1 ? createExpression(recurmax) + '\n' : '') + + 'return Math;\n' + + '}'; + case 11: + // more like a parser test but perhaps comment nodes mess up the analysis? + switch (rng(5)) { + case 0: + return '(a/* ignore */++)'; + case 1: + return '(b/* ignore */--)'; + case 2: + return '(++/* ignore */a)'; + case 3: + return '(--/* ignore */b)'; + case 4: + // only groups that wrap a single variable return a "Reference", so this is still valid. + // may just be a parser edge case that is invisible to uglify... + return '(--(b))'; + default: + return '(--/* ignore */b)'; + } + } +} + +function createTypeofExpr(recurmax) { + if (--recurmax < 0) { + return 'typeof undefined === "undefined"'; + } + + switch (rng(5)) { + case 0: + return '(typeof ' + createVarName() + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; + case 1: + return '(typeof ' + createVarName() + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; + case 2: + return '(typeof ' + createVarName() + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; + case 3: + return '(typeof ' + createVarName() + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; + case 4: + return '(typeof ' + createVarName() + ')'; } } function createValue() { - var values = [ - 'true', - 'false', - '22', - '0', - '(-1)', - 'NaN', - 'undefined', - 'null', - '"foo"', - '"bar"' ]; - return values[rng(values.length)]; + return VALUES[rng(VALUES.length)]; } -function createBinaryOp() { - switch (rng(6)) { - case 0: - return '+'; - case 1: - return '-'; - case 2: - return ','; - case 3: - return '&&'; - case 4: - return '||'; - case 5: - return '^'; - } +function createBinaryOp(noComma) { + if (noComma) return BINARY_OPS_NO_COMMA[rng(BINARY_OPS_NO_COMMA.length)]; + return BINARY_OPS[rng(BINARY_OPS.length)]; } function createAssignment() { - switch (rng(4)) { - case 0: - return '='; - case 1: - return '-='; - case 2: - return '^='; - case 3: - return '+='; - } + return ASSIGNMENTS[rng(ASSIGNMENTS.length)]; } function createUnaryOp() { - switch (rng(4)) { - case 0: - return '--'; - case 1: - return '++'; - case 2: - return '~'; - case 3: - return '!'; - } + return UNARY_OPS[rng(UNARY_OPS.length)]; } -function log() { +function createVarName(maybe) { + if (!maybe || rng(2) === 1) { + return VAR_NAMES[rng(VAR_NAMES.length)] + (rng(5) > 0 ? ++loops : ''); + } + return ''; +} + +function log(ok) { console.log("//============================================================="); + if (!ok) console.log("// !!!!!! Failed..."); console.log("// original code"); console.log("//"); console.log(original_code); @@ -183,43 +399,57 @@ function log() { console.log(beautify_result); console.log("uglified result:"); console.log(uglify_result); + if (!ok) console.log("!!!!!! Failed..."); } var num_iterations = +process.argv[2] || 1/0; -var verbose = !!process.argv[3]; +var verbose = process.argv[3] === 'v' || process.argv[2] === 'v'; +var verbose_interval = process.argv[3] === 'V' || process.argv[2] === 'V'; for (var round = 0; round < num_iterations; round++) { + var parse_error = false; process.stdout.write(round + " of " + num_iterations + "\r"); var original_code = [ "var a = 100, b = 10;", - createFunctionDecls(rng(3) + 1, 10), + createFunctionDecls(rng(MAX_GENERATED_FUNCTIONS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH), "console.log(a, b);" ].join("\n"); - var beautify_code = minify(original_code, { - fromString: true, - mangle: false, - compress: false, - output: { - beautify: true, - bracketize: true, - }, - }).code; - - var uglify_code = minify(beautify_code, { - fromString: true, - mangle: false, - compress: { - passes: 3, - }, - output: { - beautify: true, - bracketize: true, - }, - }).code; - var original_result = run_code(original_code); + + try { + var beautify_code = minify(original_code, { + fromString: true, + mangle: false, + compress: false, + output: { + beautify: true, + bracketize: true, + }, + }).code; + } catch(e) { + parse_error = 1; + } var beautify_result = run_code(beautify_code); + + try { + var uglify_code = minify(beautify_code, { + fromString: true, + mangle: true, + compress: { + passes: 3, + }, + output: { + //beautify: true, + //bracketize: true, + }, + }).code; + } catch(e) { + parse_error = 2; + } var uglify_result = run_code(uglify_code); - var ok = original_result == beautify_result && original_result == uglify_result; - if (verbose || !ok) log(); - if (!ok) process.exit(1); + + var ok = !parse_error && original_result == beautify_result && original_result == uglify_result; + if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(ok); + if (parse_error === 1) console.log('Parse error while beautifying'); + if (parse_error === 2) console.log('Parse error while uglifying'); + if (!ok) break; } From 8a4f86528f5b5c3a0ee0a709ed3a6b908706a5c3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Mar 2017 12:05:44 +0800 Subject: [PATCH 19/38] fix side-effects detection on switch statements (#1678) extension of #1675 --- lib/compress.js | 4 ++++ test/compress/issue-1673.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index c57287bf..8d8387f2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1610,6 +1610,10 @@ merge(Compressor.prototype, { def(AST_Block, function(compressor){ return any(this.body, compressor); }); + def(AST_Case, function(compressor){ + return any(this.body, compressor) + || this.expression.has_side_effects(compressor); + }); def(AST_Try, function(compressor){ return any(this.body, compressor) || this.bcatch && this.bcatch.has_side_effects(compressor) diff --git a/test/compress/issue-1673.js b/test/compress/issue-1673.js index 59686abf..4628e37c 100644 --- a/test/compress/issue-1673.js +++ b/test/compress/issue-1673.js @@ -125,3 +125,35 @@ side_effects_label: { } expect_stdout: "PASS" } + +side_effects_switch: { + options = { + reduce_vars: true, + side_effects: true, + unused: true, + } + input: { + function f() { + function g() { + switch (0) { + default: + case console.log("PASS"): + } + } + g(); + } + f(); + } + expect: { + function f() { + (function() { + switch (0) { + default: + case console.log("PASS"): + } + })(); + } + f(); + } + expect_stdout: "PASS" +} From 94f84727ce454c3ecf5206ac79dba4a21ec6deb6 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Mar 2017 13:32:43 +0800 Subject: [PATCH 20/38] suppress switch branch de-duplication upon side effects (#1682) fixes #1679 --- lib/compress.js | 4 +++- test/compress/switch.js | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 8d8387f2..8d1ef8ac 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2551,12 +2551,14 @@ merge(Compressor.prototype, { } else { body.push(branch); } - blocks[key] = branch; fallthrough = false; } else { body.push(branch); fallthrough = true; } + if (branch instanceof AST_Case && branch.expression.has_side_effects(compressor)) + blocks = Object.create(null); + if (!fallthrough) blocks[key] = branch; } for (; i < len && fallthrough; i++) { branch = self.body[i]; diff --git a/test/compress/switch.js b/test/compress/switch.js index 7d3d7d13..481257aa 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -434,3 +434,50 @@ issue_1674: { } expect_stdout: "PASS" } + +issue_1679: { + options = { + dead_code: true, + evaluate: true, + } + input: { + var a = 100, b = 10; + function f() { + switch (--b) { + default: + case !function x() {}: + break; + case b--: + switch (0) { + default: + case a--: + } + break; + case (a++): + break; + } + } + f(); + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f() { + switch (--b) { + default: + case !function x() {}: + break; + case b--: + switch (0) { + default: + case a--: + } + break; + case (a++): + } + } + f(); + console.log(a, b); + } + expect_stdout: true +} From 5509e51098274b28b8574246011767ba0be66edd Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 26 Mar 2017 04:36:33 -0400 Subject: [PATCH 21/38] optimize conditional when condition symbol matches consequent (#1684) --- lib/compress.js | 11 +++++++++++ test/compress/conditionals.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 8d1ef8ac..70bbb7fc 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3565,8 +3565,19 @@ merge(Compressor.prototype, { alternative: self.consequent }); } + var condition = self.condition; var consequent = self.consequent; var alternative = self.alternative; + // x?x:y --> x||y + if (condition instanceof AST_SymbolRef + && consequent instanceof AST_SymbolRef + && condition.definition() === consequent.definition()) { + return make_node(AST_Binary, self, { + operator: "||", + left: condition, + right: alternative + }); + } // if (foo) exp = something; else exp = something_else; // | // v diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index c5639836..e7ea2bb2 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -933,3 +933,32 @@ issue_1645_2: { } expect_stdout: true } + +condition_symbol_matches_consequent: { + options = { + conditionals: true, + } + input: { + function foo(x, y) { + return x ? x : y; + } + function bar() { + return g ? g : h; + } + var g = 4; + var h = 5; + console.log(foo(3, null), foo(0, 7), foo(true, false), bar()); + } + expect: { + function foo(x, y) { + return x || y; + } + function bar() { + return g || h; + } + var g = 4; + var h = 5; + console.log(foo(3, null), foo(0, 7), foo(true, false), bar()); + } + expect_stdout: "3 7 true 4" +} From 3276740779077f2ee7b686c4aa7f1bd46fbc1c66 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Mar 2017 16:52:38 +0800 Subject: [PATCH 22/38] fallthrough should not execute case expression (#1683) - de-duplicate trailing cases only, avoid all potential side-effects - enable switch statement fuzzing fixes #1680 --- lib/compress.js | 24 ++++--------- test/compress/switch.js | 80 ++++++++++++++++++++++++++++++++++++++--- test/ufuzz.js | 1 - 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 70bbb7fc..8467f96c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2515,7 +2515,7 @@ merge(Compressor.prototype, { self.expression = best_of_expression(expression, self.expression); } if (compressor.option("dead_code")) { - var blocks = Object.create(null); + var prev_block; var decl = []; var body = []; var default_branch; @@ -2542,31 +2542,21 @@ merge(Compressor.prototype, { continue; } } + var case_side_effects = branch instanceof AST_Case && branch.expression.has_side_effects(compressor); if (aborts(branch)) { - var key = make_node(AST_BlockStatement, branch, branch).print_to_string(); - var block; - if (!fallthrough && (block = blocks[key])) { - block.body = []; - body.splice(body.indexOf(block) + 1, 0, branch); - } else { - body.push(branch); - } + var block = make_node(AST_BlockStatement, branch, branch).print_to_string(); + if (!fallthrough && prev_block === block) body[body.length - 1].body = []; + body.push(branch); + prev_block = block; fallthrough = false; } else { body.push(branch); + prev_block = null; fallthrough = true; } - if (branch instanceof AST_Case && branch.expression.has_side_effects(compressor)) - blocks = Object.create(null); - if (!fallthrough) blocks[key] = branch; } for (; i < len && fallthrough; i++) { branch = self.body[i]; - if (branch instanceof AST_Case) { - exact_match.body.push(make_node(AST_SimpleStatement, branch.expression, { - body: branch.expression - })); - } exact_match.body = exact_match.body.concat(branch.body); fallthrough = !aborts(exact_match); } diff --git a/test/compress/switch.js b/test/compress/switch.js index 481257aa..2025d91b 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -23,7 +23,6 @@ constant_switch_2: { } expect: { foo(); - 2; bar(); } } @@ -118,7 +117,6 @@ constant_switch_6: { x(); if (foo) break OUT; y(); - 2; bar(); } } @@ -157,7 +155,6 @@ constant_switch_7: { console.log(x); } y(); - 2; bar(); } } @@ -206,7 +203,6 @@ constant_switch_9: { x(); for (;;) if (foo) break OUT; y(); - 2; bar(); def(); } @@ -481,3 +477,79 @@ issue_1679: { } expect_stdout: true } + +issue_1680_1: { + options = { + dead_code: true, + evaluate: true, + } + input: { + function f(x) { + console.log(x); + return x + 1; + } + switch (2) { + case f(0): + case f(1): + f(2); + case 2: + case f(3): + case f(4): + f(5); + } + } + expect: { + function f(x) { + console.log(x); + return x + 1; + } + switch (2) { + case f(0): + case f(1): + f(2); + case 2: + f(5); + } + } + expect_stdout: [ + "0", + "1", + "2", + "5", + ] +} + +issue_1680_2: { + options = { + dead_code: true, + } + input: { + var a = 100, b = 10; + switch (b) { + case a--: + break; + case b: + var c; + break; + case a: + break; + case a--: + break; + } + console.log(a, b); + } + expect: { + var a = 100, b = 10; + switch (b) { + case a--: + break; + case b: + var c; + break; + case a: + case a--: + } + console.log(a, b); + } + expect_stdout: true +} diff --git a/test/ufuzz.js b/test/ufuzz.js index c56c6224..49236253 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -201,7 +201,6 @@ function createStatement(recurmax, canThrow, canBreak, canContinue) { case 6: return createExpression(recurmax) + ';'; case 7: - return ';'; // TODO: disabled until some switch issues are resolved // note: case args are actual expressions // note: default does not _need_ to be last return 'switch (' + createExpression(recurmax) + ') { ' + createSwitchParts(recurmax, 4) + '}'; From e76fb354eb62d8e7b6968f1f77cfbb219814cea3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Mar 2017 18:08:44 +0800 Subject: [PATCH 23/38] fix `cascade` on `delete` operator (#1687) Conditions including strict mode would make `delete` return `true` or `false`, and are too complex to be evaluated by the compressor. Suppress assignment folding into said operator. fixes #1685 --- lib/compress.js | 6 ++++-- test/compress/sequences.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 8467f96c..a617adbe 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2542,7 +2542,6 @@ merge(Compressor.prototype, { continue; } } - var case_side_effects = branch instanceof AST_Case && branch.expression.has_side_effects(compressor); if (aborts(branch)) { var block = make_node(AST_BlockStatement, branch, branch).print_to_string(); if (!fallthrough && prev_block === block) body[body.length - 1].body = []; @@ -2946,7 +2945,10 @@ merge(Compressor.prototype, { field = "left"; } } else if (cdr instanceof AST_Call - || cdr instanceof AST_Unary && cdr.operator != "++" && cdr.operator != "--") { + || cdr instanceof AST_Unary + && cdr.operator != "delete" + && cdr.operator != "++" + && cdr.operator != "--") { field = "expression"; } else break; parent = cdr; diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 49b61ae0..af6e0c36 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -306,3 +306,27 @@ unsafe_undefined: { } } } + +issue_1685: { + options = { + cascade: true, + side_effects: true, + } + input: { + var a = 100, b = 10; + function f() { + var a = (a--, delete a && --b); + } + f(); + console.log(a, b); + } + expect: { + var a = 100, b = 10; + function f() { + var a = (a--, delete a && --b); + } + f(); + console.log(a, b); + } + expect_stdout: true +} From 00996afd2ce1f05fa973016e793495f69b524d5b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Mar 2017 18:18:44 +0800 Subject: [PATCH 24/38] ufuzz: workaround function name and toString() (#1688) fixes #1686 --- test/ufuzz.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index 49236253..491526d7 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -121,6 +121,8 @@ var TYPEOF_OUTCOMES = [ 'symbol', 'crap' ]; +var FUNC_TOSTRING = 'Function.prototype.toString=function(){return"function(){}"};'; + function run_code(code) { var stdout = ""; var original_write = process.stdout.write; @@ -128,7 +130,15 @@ function run_code(code) { stdout += chunk; }; try { - new vm.Script(code).runInNewContext({ console: console }, { timeout: 5000 }); + new vm.Script(FUNC_TOSTRING + code).runInNewContext({ + console: { + log: function() { + return console.log.apply(console, [].map.call(arguments, function(arg) { + return typeof arg == "function" ? "[Function]" : arg; + })); + } + } + }, { timeout: 5000 }); return stdout; } catch (ex) { return ex; From 861a79ac9fdb2cdbb54054306eb896e2c134af73 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Mar 2017 19:14:30 +0800 Subject: [PATCH 25/38] fix `delete` related issues in `collapse_vars` and `reduce_vars` (#1689) --- lib/compress.js | 44 +++++++++++++--------------------- test/compress/collapse_vars.js | 3 ++- test/compress/reduce_vars.js | 27 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index a617adbe..83486b61 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1207,8 +1207,10 @@ merge(Compressor.prototype, { node.DEFMETHOD("is_string", func); }); + var unary_side_effects = makePredicate("delete ++ --"); + function isLHS(node, parent) { - return parent instanceof AST_Unary && (parent.operator == "++" || parent.operator == "--") + return parent instanceof AST_Unary && unary_side_effects(parent.operator) || parent instanceof AST_Assign && parent.left === node; } @@ -1643,9 +1645,7 @@ merge(Compressor.prototype, { || this.alternative.has_side_effects(compressor); }); def(AST_Unary, function(compressor){ - return this.operator == "delete" - || this.operator == "++" - || this.operator == "--" + return unary_side_effects(this.operator) || this.expression.has_side_effects(compressor); }); def(AST_SymbolRef, function(compressor){ @@ -2196,26 +2196,19 @@ merge(Compressor.prototype, { return node; }); def(AST_Unary, function(compressor, first_in_statement){ - switch (this.operator) { - case "delete": - case "++": - case "--": - return this; - case "typeof": - if (this.expression instanceof AST_SymbolRef) return null; - default: - var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); - if (first_in_statement - && this instanceof AST_UnaryPrefix - && is_iife_call(expression)) { - if (expression === this.expression && this.operator.length === 1) return this; - return make_node(AST_UnaryPrefix, this, { - operator: this.operator.length === 1 ? this.operator : "!", - expression: expression - }); - } - return expression; + if (unary_side_effects(this.operator)) return this; + if (this.operator == "typeof" && this.expression instanceof AST_SymbolRef) return null; + var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); + if (first_in_statement + && this instanceof AST_UnaryPrefix + && is_iife_call(expression)) { + if (expression === this.expression && this.operator.length === 1) return this; + return make_node(AST_UnaryPrefix, this, { + operator: this.operator.length === 1 ? this.operator : "!", + expression: expression + }); } + return expression; }); def(AST_SymbolRef, function() { return this.undeclared() ? this : null; @@ -2945,10 +2938,7 @@ merge(Compressor.prototype, { field = "left"; } } else if (cdr instanceof AST_Call - || cdr instanceof AST_Unary - && cdr.operator != "delete" - && cdr.operator != "++" - && cdr.operator != "--") { + || cdr instanceof AST_Unary && !unary_side_effects(cdr.operator)) { field = "expression"; } else break; parent = cdr; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 2437ca5f..4107707b 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -894,7 +894,8 @@ collapse_vars_unary: { } expect: { function f0(o, p) { - delete o[p]; + var x = o[p]; + delete x; } function f1(n) { return n > +!!n diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 943dd290..f4dd68d2 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1544,3 +1544,30 @@ issue_1670_6: { } expect_stdout: "1" } + +unary_delete: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + var b = 10; + function f() { + var a; + if (delete a) b--; + } + f(); + console.log(b); + } + expect: { + var b = 10; + function f() { + var a; + if (delete a) b--; + } + f(); + console.log(b); + } + expect_stdout: true +} From 57ce5bd9e085546a5c1cb8dd4a3ea71ab6c56f26 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Mar 2017 01:30:21 +0800 Subject: [PATCH 26/38] handle overlapped variable definitions (#1691) Process variable definitions with or without assigned values against: - `arguments` - named function arguments - multiple definitions within same scope Essentially demote variable declarations with no value assignments. Also fixed invalid use of `AST_VarDef` over `arguments` - should use a member of `AST_SymbolDeclaration` instead. --- lib/compress.js | 17 ++- lib/scope.js | 32 ++-- test/compress/reduce_vars.js | 289 ++++++++++++++++++++++++++++++++++- 3 files changed, 314 insertions(+), 24 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 83486b61..590015ff 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -260,7 +260,7 @@ merge(Compressor.prototype, { if (node instanceof AST_SymbolRef) { var d = node.definition(); d.references.push(node); - if (!d.fixed || !is_safe(d) + if (d.fixed === undefined || !is_safe(d) || is_modified(node, 0, d.fixed instanceof AST_Lambda)) { d.fixed = false; } @@ -270,10 +270,10 @@ merge(Compressor.prototype, { } if (node instanceof AST_VarDef) { var d = node.name.definition(); - if (d.fixed === undefined) { - d.fixed = node.value || make_node(AST_Undefined, node); + if (d.fixed == null) { + d.fixed = node.value; mark_as_safe(d); - } else { + } else if (node.value) { d.fixed = false; } } @@ -357,7 +357,14 @@ merge(Compressor.prototype, { function is_safe(def) { for (var i = safe_ids.length, id = def.id; --i >= 0;) { - if (safe_ids[i][id]) return true; + if (safe_ids[i][id]) { + if (def.fixed == null) { + var orig = def.orig[0]; + if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; + def.fixed = make_node(AST_Undefined, orig); + } + return true; + } } } diff --git a/lib/scope.js b/lib/scope.js index 483503ee..b255032c 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -100,15 +100,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ if (node instanceof AST_Catch) { var save_scope = scope; scope = new AST_Scope(node); - scope.init_scope_vars(); - scope.parent_scope = save_scope; + scope.init_scope_vars(save_scope); descend(); scope = save_scope; return true; } if (node instanceof AST_Scope) { - node.init_scope_vars(); - var save_scope = node.parent_scope = scope; + node.init_scope_vars(scope); + var save_scope = scope; var save_defun = defun; var save_labels = labels; defun = scope = node; @@ -243,23 +242,24 @@ AST_Toplevel.DEFMETHOD("def_global", function(node){ } }); -AST_Scope.DEFMETHOD("init_scope_vars", function(){ - this.variables = new Dictionary(); // map name to AST_SymbolVar (variables defined in this scope; includes functions) - this.functions = new Dictionary(); // map name to AST_SymbolDefun (functions defined in this scope) - this.uses_with = false; // will be set to true if this or some nested scope uses the `with` statement - this.uses_eval = false; // will be set to true if this or nested scope uses the global `eval` - this.parent_scope = null; // the parent scope - this.enclosed = []; // a list of variables from this or outer scope(s) that are referenced from this or inner scopes - this.cname = -1; // the current index for mangling functions/variables +AST_Scope.DEFMETHOD("init_scope_vars", function(parent_scope){ + this.variables = new Dictionary(); // map name to AST_SymbolVar (variables defined in this scope; includes functions) + this.functions = new Dictionary(); // map name to AST_SymbolDefun (functions defined in this scope) + this.uses_with = false; // will be set to true if this or some nested scope uses the `with` statement + this.uses_eval = false; // will be set to true if this or nested scope uses the global `eval` + this.parent_scope = parent_scope; // the parent scope + this.enclosed = []; // a list of variables from this or outer scope(s) that are referenced from this or inner scopes + this.cname = -1; // the current index for mangling functions/variables }); AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; - - var symbol = new AST_VarDef({ name: "arguments", start: this.start, end: this.end }); - var def = new SymbolDef(this, this.variables.size(), symbol); - this.variables.set(symbol.name, def); + this.def_variable(new AST_SymbolVar({ + name: "arguments", + start: this.start, + end: this.end + })); }); AST_SymbolRef.DEFMETHOD("reference", function(options) { diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index f4dd68d2..87942ab9 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -425,7 +425,7 @@ iife_new: { expect_stdout: true } -multi_def: { +multi_def_1: { options = { evaluate: true, reduce_vars: true, @@ -435,7 +435,7 @@ multi_def: { if (a) var b = 1; else - var b = 2 + var b = 2; console.log(b + 1); } } @@ -444,7 +444,7 @@ multi_def: { if (a) var b = 1; else - var b = 2 + var b = 2; console.log(b + 1); } } @@ -479,6 +479,33 @@ multi_def_2: { } } +multi_def_3: { + options = { + evaluate: true, + reduce_vars: true, + } + input: { + function f(a) { + var b = 2; + if (a) + var b; + else + var b; + console.log(b + 1); + } + } + expect: { + function f(a) { + var b = 2; + if (a) + var b; + else + var b; + console.log(3); + } + } +} + use_before_var: { options = { evaluate: true, @@ -1571,3 +1598,259 @@ unary_delete: { } expect_stdout: true } + +redefine_arguments_1: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + var arguments = 42; + return typeof arguments; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + return"number"; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect_stdout: "object number undefined" +} + +redefine_arguments_2: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + var arguments = 42; + return typeof arguments; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect: { + console.log(function() { + var arguments; + return typeof arguments; + }(), function() { + return"number"; + }(), function(x) { + var arguments = x; + return typeof arguments; + }()); + } + expect_stdout: "object number undefined" +} + +redefine_arguments_3: { + options = { + evaluate: true, + keep_fargs: false, + passes: 3, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f() { + var arguments; + return typeof arguments; + } + function g() { + var arguments = 42; + return typeof arguments; + } + function h(x) { + var arguments = x; + return typeof arguments; + } + console.log(f(), g(), h()); + } + expect: { + console.log(function() { + var arguments; + return typeof arguments; + }(), "number", "undefined"); + } + expect_stdout: "object number undefined" +} + +redefine_farg_1: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + unused: true, + } + input: { + function f(a) { + var a; + return typeof a; + } + function g(a) { + var a = 42; + return typeof a; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect: { + function f(a) { + var a; + return typeof a; + } + function g() { + return"number"; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect_stdout: "object number undefined" +} + +redefine_farg_2: { + options = { + evaluate: true, + keep_fargs: false, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f(a) { + var a; + return typeof a; + } + function g(a) { + var a = 42; + return typeof a; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect: { + console.log(function(a) { + var a; + return typeof a; + }([]), function() { + return "number"; + }(),function(a, b) { + var a = b; + return typeof a; + }([])); + } + expect_stdout: "object number undefined" +} + +redefine_farg_3: { + options = { + evaluate: true, + keep_fargs: false, + passes: 3, + reduce_vars: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + function f(a) { + var a; + return typeof a; + } + function g(a) { + var a = 42; + return typeof a; + } + function h(a, b) { + var a = b; + return typeof a; + } + console.log(f([]), g([]), h([])); + } + expect: { + console.log(function(a) { + var a; + return typeof a; + }([]), "number", function(a) { + var a = void 0; + return typeof a; + }([])); + } + expect_stdout: "object number undefined" +} + +delay_def: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + function f() { + return a; + var a; + } + function g() { + return a; + var a = 1; + } + console.log(f(), g()); + } + expect: { + function f() { + return a; + var a; + } + function g() { + return a; + var a = 1; + } + console.log(f(), g()); + } + expect_stdout: true +} From f001e4cb9d5bfe56e25db552bb7ab0951a142a99 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Mar 2017 01:58:21 +0800 Subject: [PATCH 27/38] fix `cascade` on anonymous function reference (#1693) Unlike normal variables and even function definitions, these cannot be reassigned, even though assignment expressions would "leak" the assigned value as normal. --- lib/compress.js | 4 +- test/compress/sequences.js | 110 +++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 590015ff..8350ba25 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2922,7 +2922,9 @@ merge(Compressor.prototype, { && (self.car.operator == "++" || self.car.operator == "--")) { left = self.car.expression; } - if (left) { + if (left + && !(left instanceof AST_SymbolRef + && left.definition().orig[0] instanceof AST_SymbolLambda)) { var parent, field; var cdr = self.cdr; while (true) { diff --git a/test/compress/sequences.js b/test/compress/sequences.js index af6e0c36..f1fa0e87 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -330,3 +330,113 @@ issue_1685: { } expect_stdout: true } + +func_def_1: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + return f = 0, !!f; + } + console.log(f()); + } + expect: { + function f() { + return !!(f = 0); + } + console.log(f()); + } + expect_stdout: "false" +} + +func_def_2: { + options = { + cascade: true, + side_effects: true, + } + input: { + console.log(function f() { + return f = 0, !!f; + }()); + } + expect: { + console.log(function f() { + return f = 0, !!f; + }()); + } + expect_stdout: "true" +} + +func_def_3: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + function g() {} + return g = 0, !!g; + } + console.log(f()); + } + expect: { + function f() { + function g() {} + return !!(g = 0); + } + console.log(f()); + } + expect_stdout: "false" +} + +func_def_4: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + function g() { + return g = 0, !!g; + } + return g(); + } + console.log(f()); + } + expect: { + function f() { + function g() { + return !!(g = 0); + } + return g(); + } + console.log(f()); + } + expect_stdout: "false" +} + +func_def_5: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f() { + return function g(){ + return g = 0, !!g; + }(); + } + console.log(f()); + } + expect: { + function f() { + return function g(){ + return g = 0, !!g; + }(); + } + console.log(f()); + } + expect_stdout: "true" +} From f5952933a00bac8c9d794d5b0e3d5f8d6173c4a9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Mar 2017 02:32:46 +0800 Subject: [PATCH 28/38] preserve side effects in switch expression (#1694) fixes #1690 --- lib/compress.js | 15 ++++++++------- test/compress/switch.js | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 8350ba25..9ed368c7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2560,10 +2560,9 @@ merge(Compressor.prototype, { fallthrough = !aborts(exact_match); } while (i < len) extract_declarations_from_unreachable_code(compressor, self.body[i++], decl); - if (body.length == 0) return make_node(AST_BlockStatement, self, { - body: decl - }).optimize(compressor); - body[0].body = decl.concat(body[0].body); + if (body.length > 0) { + body[0].body = decl.concat(body[0].body); + } self.body = body; } while (branch = self.body[self.body.length - 1]) { @@ -2575,9 +2574,11 @@ merge(Compressor.prototype, { && branch.expression.has_side_effects(compressor)) break; self.body.pop(); } - if (compressor.option("conditionals") && self.body.length == 0) { - return make_node(AST_SimpleStatement, self, { - body: self.expression + if (decl && self.body.length == 0) { + return make_node(AST_BlockStatement, self, { + body: decl.concat(make_node(AST_SimpleStatement, self.expression, { + body: self.expression + })) }).optimize(compressor); } if (body && body.length == 1 && (body[0] === exact_match || body[0] === default_branch)) { diff --git a/test/compress/switch.js b/test/compress/switch.js index 2025d91b..654a838b 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -553,3 +553,29 @@ issue_1680_2: { } expect_stdout: true } + +issue_1690_1: { + options = { + dead_code: true, + } + input: { + switch (console.log("PASS")) {} + } + expect: { + console.log("PASS"); + } + expect_stdout: "PASS" +} + +issue_1690_2: { + options = { + dead_code: false, + } + input: { + switch (console.log("PASS")) {} + } + expect: { + switch (console.log("PASS")) {} + } + expect_stdout: "PASS" +} From 581630e0a71cffeadd4887a757889d6a3502275c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Mar 2017 04:37:42 +0800 Subject: [PATCH 29/38] fix typeof side effects (#1696) `statement_to_expression()` drops `typeof` even if it operates on undeclared variables. Since we now have `drop_side_effect_free()`, replace and remove this deprecated functionality. --- lib/compress.js | 31 +++++++++++-------------------- test/compress/negate-iife.js | 9 +++++---- test/compress/switch.js | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 9ed368c7..03c8d5dd 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -482,15 +482,6 @@ merge(Compressor.prototype, { return x; }; - var readOnlyPrefix = makePredicate("! ~ + - void typeof"); - function statement_to_expression(stat) { - if (stat.body instanceof AST_UnaryPrefix && readOnlyPrefix(stat.body.operator)) { - return stat.body.expression; - } else { - return stat.body; - } - } - function is_iife_call(node) { if (node instanceof AST_Call && !(node instanceof AST_New)) { return node.expression instanceof AST_Function || is_iife_call(node.expression); @@ -984,10 +975,10 @@ merge(Compressor.prototype, { }; statements.forEach(function(stat){ if (stat instanceof AST_SimpleStatement) { - if (seqLength(seq) >= compressor.sequences_limit) { - push_seq(); - } - seq.push(seq.length > 0 ? statement_to_expression(stat) : stat.body); + if (seqLength(seq) >= compressor.sequences_limit) push_seq(); + var body = stat.body; + if (seq.length > 0) body = body.drop_side_effect_free(compressor); + if (body) seq.push(body); } else { push_seq(); ret.push(stat); @@ -1036,7 +1027,7 @@ merge(Compressor.prototype, { stat.init = cons_seq(stat.init); } else if (!stat.init) { - stat.init = statement_to_expression(prev); + stat.init = prev.body.drop_side_effect_free(compressor); ret.pop(); } } catch(ex) { @@ -2170,6 +2161,7 @@ merge(Compressor.prototype, { switch (this.operator) { case "&&": case "||": + if (right === this.right) return this; var node = this.clone(); node.right = right; return node; @@ -2423,8 +2415,8 @@ merge(Compressor.prototype, { return make_node(AST_SimpleStatement, self, { body: make_node(AST_Conditional, self, { condition : self.condition, - consequent : statement_to_expression(self.body), - alternative : statement_to_expression(self.alternative) + consequent : self.body.body, + alternative : self.alternative.body }) }).optimize(compressor); } @@ -2440,25 +2432,24 @@ merge(Compressor.prototype, { body: make_node(AST_Binary, self, { operator : "||", left : negated, - right : statement_to_expression(self.body) + right : self.body.body }) }).optimize(compressor); return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "&&", left : self.condition, - right : statement_to_expression(self.body) + right : self.body.body }) }).optimize(compressor); } if (self.body instanceof AST_EmptyStatement - && self.alternative && self.alternative instanceof AST_SimpleStatement) { return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "||", left : self.condition, - right : statement_to_expression(self.alternative) + right : self.alternative.body }) }).optimize(compressor); } diff --git a/test/compress/negate-iife.js b/test/compress/negate-iife.js index e9ad37db..343e8e16 100644 --- a/test/compress/negate-iife.js +++ b/test/compress/negate-iife.js @@ -353,8 +353,9 @@ issue_1254_negate_iife_nested: { issue_1288: { options = { - negate_iife: true, conditionals: true, + negate_iife: true, + side_effects: false, }; input: { if (w) ; @@ -374,11 +375,11 @@ issue_1288: { })(0); } expect: { - w || function f() {}(); - x || function() { + w || !function f() {}(); + x || !function() { x = {}; }(); - y ? function() {}() : function(z) { + y ? !function() {}() : !function(z) { return z; }(0); } diff --git a/test/compress/switch.js b/test/compress/switch.js index 654a838b..c3e76302 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -579,3 +579,17 @@ issue_1690_2: { } expect_stdout: "PASS" } + +if_switch_typeof: { + options = { + conditionals: true, + dead_code: true, + side_effects: true, + } + input: { + if (a) switch(typeof b) {} + } + expect: { + a; + } +} From c526da59a1a9b1a1c5689cfdcc840b36850ed250 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Mar 2017 18:09:35 +0800 Subject: [PATCH 30/38] `has_side_effects()` should take `AST_Switch.expression` into account (#1699) fixes #1698 --- lib/compress.js | 8 ++++++-- test/compress/switch.js | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 03c8d5dd..1146f300 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1610,9 +1610,13 @@ merge(Compressor.prototype, { def(AST_Block, function(compressor){ return any(this.body, compressor); }); + def(AST_Switch, function(compressor){ + return this.expression.has_side_effects(compressor) + || any(this.body, compressor); + }); def(AST_Case, function(compressor){ - return any(this.body, compressor) - || this.expression.has_side_effects(compressor); + return this.expression.has_side_effects(compressor) + || any(this.body, compressor); }); def(AST_Try, function(compressor){ return any(this.body, compressor) diff --git a/test/compress/switch.js b/test/compress/switch.js index c3e76302..9f9d3568 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -593,3 +593,24 @@ if_switch_typeof: { a; } } + +issue_1698: { + options = { + side_effects: true, + } + input: { + var a = 1; + !function() { + switch (a++) {} + }(); + console.log(a); + } + expect: { + var a = 1; + !function() { + switch (a++) {} + }(); + console.log(a); + } + expect_stdout: "2" +} From aa3f647656ce46469d20bd477b600ca09bc3f964 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 27 Mar 2017 21:49:08 +0800 Subject: [PATCH 31/38] ufuzz: workaround for Function.toString() v2 (#1700) --- test/ufuzz.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index 491526d7..c1ac8f4c 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -121,7 +121,20 @@ var TYPEOF_OUTCOMES = [ 'symbol', 'crap' ]; -var FUNC_TOSTRING = 'Function.prototype.toString=function(){return"function(){}"};'; +var FUNC_TOSTRING = [ + "Function.prototype.toString = function() {", + " var ids = [];", + " return function() {", + " var i = ids.indexOf(this);", + " if (i < 0) {", + " i = ids.length;", + " ids.push(this);", + " }", + ' return "[Function: __func_" + i + "__]";', + " }", + "}();", + "" +].join("\n"); function run_code(code) { var stdout = ""; From 984a21704e126616a74d65a1e8790aeccd02f548 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 28 Mar 2017 03:26:35 +0800 Subject: [PATCH 32/38] fix mangle for variable declared within catch block (#1706) fixes #1704 --- lib/scope.js | 9 +- test/compress/issue-1704.js | 175 ++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 test/compress/issue-1704.js diff --git a/lib/scope.js b/lib/scope.js index b255032c..c2d0b552 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -154,6 +154,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { defun.def_variable(node); + if (defun !== scope) node.mark_enclosed(options); } else if (node instanceof AST_SymbolCatch) { scope.def_variable(node); @@ -262,9 +263,8 @@ AST_Lambda.DEFMETHOD("init_scope_vars", function(){ })); }); -AST_SymbolRef.DEFMETHOD("reference", function(options) { +AST_Symbol.DEFMETHOD("mark_enclosed", function(options) { var def = this.definition(); - def.references.push(this); var s = this.scope; while (s) { push_uniq(s.enclosed, def); @@ -278,6 +278,11 @@ AST_SymbolRef.DEFMETHOD("reference", function(options) { } }); +AST_SymbolRef.DEFMETHOD("reference", function(options) { + this.definition().references.push(this); + this.mark_enclosed(options); +}); + AST_Scope.DEFMETHOD("find_variable", function(name){ if (name instanceof AST_Symbol) name = name.name; return this.variables.get(name) diff --git a/test/compress/issue-1704.js b/test/compress/issue-1704.js new file mode 100644 index 00000000..3fa637fe --- /dev/null +++ b/test/compress/issue-1704.js @@ -0,0 +1,175 @@ +mangle_catch: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(o){a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(args){a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_var: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(o){var a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_var_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var a="FAIL";try{throw 1}catch(args){var a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(c){o="PASS"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(c){o="PASS"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_var_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(r){var o="PASS"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_var_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + var a = "FAIL"; + try { + throw 1; + } catch (args) { + var a = "PASS"; + } + console.log(a); + } + expect_exact: 'var o="FAIL";try{throw 1}catch(r){var o="PASS"}console.log(o);' + expect_stdout: "PASS" +} From 67d0237f73c3147855983edde137cd95a2cb1749 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 28 Mar 2017 03:59:13 +0800 Subject: [PATCH 33/38] fix tail trimming of switch blocks (#1707) now guarded under `dead_code` fixes #1705 --- lib/compress.js | 105 ++++++++++++++++++++-------------------- test/compress/switch.js | 66 +++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 53 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 1146f300..e2fde883 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2509,74 +2509,73 @@ merge(Compressor.prototype, { var expression = make_node_from_constant(value, self.expression).transform(compressor); self.expression = best_of_expression(expression, self.expression); } - if (compressor.option("dead_code")) { - var prev_block; - var decl = []; - var body = []; - var default_branch; - var exact_match; - var fallthrough; - for (var i = 0, len = self.body.length; i < len && !exact_match; i++) { - branch = self.body[i]; - if (branch instanceof AST_Default) { - if (!default_branch) default_branch = branch; - else if (!fallthrough) { - extract_declarations_from_unreachable_code(compressor, branch, decl); - continue; - } - } else if (value !== self.expression) { - var exp = branch.expression.evaluate(compressor); - if (exp === value) { - exact_match = branch; - if (default_branch) { - body.splice(body.indexOf(default_branch), 1); - extract_declarations_from_unreachable_code(compressor, default_branch, decl); - } - } else if (exp !== branch.expression && !fallthrough) { - extract_declarations_from_unreachable_code(compressor, branch, decl); - continue; - } + if (!compressor.option("dead_code")) return self; + var prev_block; + var decl = []; + var body = []; + var default_branch; + var exact_match; + var fallthrough; + for (var i = 0, len = self.body.length; i < len && !exact_match; i++) { + branch = self.body[i]; + if (branch instanceof AST_Default) { + if (!default_branch) default_branch = branch; + else if (!fallthrough) { + extract_declarations_from_unreachable_code(compressor, branch, decl); + continue; } - if (aborts(branch)) { - var block = make_node(AST_BlockStatement, branch, branch).print_to_string(); - if (!fallthrough && prev_block === block) body[body.length - 1].body = []; - body.push(branch); - prev_block = block; - fallthrough = false; - } else { - body.push(branch); - prev_block = null; - fallthrough = true; + } else if (value !== self.expression) { + var exp = branch.expression.evaluate(compressor); + if (exp === value) { + exact_match = branch; + if (default_branch) { + body.splice(body.indexOf(default_branch), 1); + extract_declarations_from_unreachable_code(compressor, default_branch, decl); + default_branch = null; + } + } else if (exp !== branch.expression && !fallthrough) { + extract_declarations_from_unreachable_code(compressor, branch, decl); + continue; } } - for (; i < len && fallthrough; i++) { - branch = self.body[i]; - exact_match.body = exact_match.body.concat(branch.body); - fallthrough = !aborts(exact_match); + if (aborts(branch)) { + var block = make_node(AST_BlockStatement, branch, branch).print_to_string(); + if (!fallthrough && prev_block === block) body[body.length - 1].body = []; + body.push(branch); + prev_block = block; + fallthrough = false; + } else { + body.push(branch); + prev_block = null; + fallthrough = true; } - while (i < len) extract_declarations_from_unreachable_code(compressor, self.body[i++], decl); - if (body.length > 0) { - body[0].body = decl.concat(body[0].body); - } - self.body = body; } - while (branch = self.body[self.body.length - 1]) { + for (; i < len && fallthrough; i++) { + branch = self.body[i]; + exact_match.body = exact_match.body.concat(branch.body); + fallthrough = !aborts(exact_match); + } + while (i < len) extract_declarations_from_unreachable_code(compressor, self.body[i++], decl); + if (body.length > 0) { + body[0].body = decl.concat(body[0].body); + } + self.body = body; + while (branch = body[body.length - 1]) { var stat = branch.body[branch.body.length - 1]; if (stat instanceof AST_Break && compressor.loopcontrol_target(stat.label) === self) branch.body.pop(); - if (branch.body.length - || branch instanceof AST_Case - && branch.expression.has_side_effects(compressor)) break; - self.body.pop(); + if (branch.body.length || branch instanceof AST_Case + && (default_branch || branch.expression.has_side_effects(compressor))) break; + if (body.pop() === default_branch) default_branch = null; } - if (decl && self.body.length == 0) { + if (body.length == 0) { return make_node(AST_BlockStatement, self, { body: decl.concat(make_node(AST_SimpleStatement, self.expression, { body: self.expression })) }).optimize(compressor); } - if (body && body.length == 1 && (body[0] === exact_match || body[0] === default_branch)) { + if (body.length == 1 && (body[0] === exact_match || body[0] === default_branch)) { var has_break = false; var tw = new TreeWalker(function(node) { if (has_break diff --git a/test/compress/switch.js b/test/compress/switch.js index 9f9d3568..5c12449c 100644 --- a/test/compress/switch.js +++ b/test/compress/switch.js @@ -614,3 +614,69 @@ issue_1698: { } expect_stdout: "2" } + +issue_1705_1: { + options = { + dead_code: true, + } + input: { + var a = 0; + switch (a) { + default: + console.log("FAIL"); + case 0: + break; + } + } + expect: { + var a = 0; + switch (a) { + default: + console.log("FAIL"); + case 0: + } + } + expect_stdout: true +} + +issue_1705_2: { + options = { + dead_code: true, + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a = 0; + switch (a) { + default: + console.log("FAIL"); + case 0: + break; + } + } + expect: { + } + expect_stdout: true +} + +issue_1705_3: { + options = { + dead_code: true, + } + input: { + switch (a) { + case 0: + break; + default: + break; + } + } + expect: { + a; + } + expect_stdout: true +} From 65da9acce6bd2548e5ffc7f35527ff62ff3f2fdd Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 28 Mar 2017 16:42:39 +0800 Subject: [PATCH 34/38] handle var within catch of the same name (#1711) The following code prints `1`: var a = 1; !function(){ a = 4; try{ throw 2; } catch (a) { var a = 3; } }(); console.log(a); fixes #1708 --- lib/scope.js | 24 +++-- test/compress/issue-1704.js | 172 ++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 5 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index c2d0b552..025d4ca3 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -75,9 +75,16 @@ SymbolDef.prototype = { } else if (!this.mangled_name && !this.unmangleable(options)) { var s = this.scope; - if (!options.screw_ie8 && this.orig[0] instanceof AST_SymbolLambda) + var sym = this.orig[0]; + if (!options.screw_ie8 && sym instanceof AST_SymbolLambda) s = s.parent_scope; - this.mangled_name = s.next_mangled(options, this); + var def; + if (options.screw_ie8 + && sym instanceof AST_SymbolCatch + && (def = s.parent_scope.find_variable(sym))) { + this.mangled_name = def.mangled_name || def.name; + } else + this.mangled_name = s.next_mangled(options, this); if (this.global && cache) { cache.set(this.name, this.mangled_name); } @@ -152,9 +159,16 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ (node.scope = defun.parent_scope).def_function(node); } else if (node instanceof AST_SymbolVar - || node instanceof AST_SymbolConst) { + || node instanceof AST_SymbolConst) { defun.def_variable(node); - if (defun !== scope) node.mark_enclosed(options); + if (defun !== scope) { + node.mark_enclosed(options); + var def = scope.find_variable(node); + if (node.thedef !== def) { + node.thedef = def; + node.reference(options); + } + } } else if (node instanceof AST_SymbolCatch) { scope.def_variable(node); @@ -278,7 +292,7 @@ AST_Symbol.DEFMETHOD("mark_enclosed", function(options) { } }); -AST_SymbolRef.DEFMETHOD("reference", function(options) { +AST_Symbol.DEFMETHOD("reference", function(options) { this.definition().references.push(this); this.mark_enclosed(options); }); diff --git a/test/compress/issue-1704.js b/test/compress/issue-1704.js index 3fa637fe..a73f7f99 100644 --- a/test/compress/issue-1704.js +++ b/test/compress/issue-1704.js @@ -173,3 +173,175 @@ mangle_catch_var_ie8_toplevel: { expect_exact: 'var o="FAIL";try{throw 1}catch(r){var o="PASS"}console.log(o);' expect_stdout: "PASS" } + +mangle_catch_redef_1: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var a="PASS";try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_redef_1_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var a="PASS";try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_redef_1_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var o="PASS";try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_redef_1_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + var a = "PASS"; + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'var o="PASS";try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "PASS" +} + +mangle_catch_redef_2: { + options = { + screw_ie8: true, + toplevel: false, + } + mangle = { + screw_ie8: true, + toplevel: false, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "undefined" +} + +mangle_catch_redef_2_ie8: { + options = { + screw_ie8: false, + toplevel: false, + } + mangle = { + screw_ie8: false, + toplevel: false, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(a){var a="FAIL2"}console.log(a);' + expect_stdout: "undefined" +} + +mangle_catch_redef_2_toplevel: { + options = { + screw_ie8: true, + toplevel: true, + } + mangle = { + screw_ie8: true, + toplevel: true, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "undefined" +} + +mangle_catch_redef_2_ie8_toplevel: { + options = { + screw_ie8: false, + toplevel: true, + } + mangle = { + screw_ie8: false, + toplevel: true, + } + input: { + try { + throw "FAIL1"; + } catch (a) { + var a = "FAIL2"; + } + console.log(a); + } + expect_exact: 'try{throw"FAIL1"}catch(o){var o="FAIL2"}console.log(o);' + expect_stdout: "undefined" +} From fb177a6312ccd90918c9f8f0f867c2fddd85a2c8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 28 Mar 2017 17:02:20 +0800 Subject: [PATCH 35/38] drop anonymous function name when overshadowed by other declarations (#1712) fixes #1709 --- lib/compress.js | 10 ++++++--- test/compress/drop-unused.js | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index e2fde883..e4863d9f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1850,9 +1850,13 @@ merge(Compressor.prototype, { function before(node, descend, in_list) { if (node instanceof AST_Function && node.name - && !compressor.option("keep_fnames") - && !(node.name.definition().id in in_use_ids)) { - node.name = null; + && !compressor.option("keep_fnames")) { + var def = node.name.definition(); + // any declarations with same name will overshadow + // name of this anonymous function and can therefore + // never be used anywhere + if (!(def.id in in_use_ids) || def.orig.length > 1) + node.name = null; } if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { var trim = !compressor.option("keep_fargs"); diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index fabf8d9f..10ca5499 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -805,3 +805,42 @@ issue_1656: { } expect_exact: "for (;;) ;" } + +issue_1709: { + options = { + unused: true, + } + input: { + console.log( + function x() { + var x = 1; + return x; + }(), + function y() { + const y = 2; + return y; + }(), + function z() { + function z() {} + return z; + }() + ); + } + expect: { + console.log( + function() { + var x = 1; + return x; + }(), + function() { + const y = 2; + return y; + }(), + function() { + function z() {} + return z; + }() + ); + } + expect_stdout: true +} From f71f4905b004a559f0612e0e0928204f912a66bc Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 28 Mar 2017 17:08:16 +0800 Subject: [PATCH 36/38] fix `is_number()` on `+=` (#1714) fixes #1710 --- lib/compress.js | 4 ++-- test/compress/numbers.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index e4863d9f..c60f8632 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1167,9 +1167,9 @@ merge(Compressor.prototype, { && this.left.is_number(compressor) && this.right.is_number(compressor); }); - var assign = makePredicate("-= *= /= %= &= |= ^= <<= >>= >>>="); def(AST_Assign, function(compressor){ - return assign(this.operator) || this.right.is_number(compressor); + return binary(this.operator.slice(0, -1)) + || this.operator == "=" && this.right.is_number(compressor); }); def(AST_Seq, function(compressor){ return this.cdr.is_number(compressor); diff --git a/test/compress/numbers.js b/test/compress/numbers.js index 0b40bb9c..ea439ecc 100644 --- a/test/compress/numbers.js +++ b/test/compress/numbers.js @@ -153,3 +153,18 @@ evaluate_4: { ); } } + +issue_1710: { + options = { + evaluate: true, + } + input: { + var x = {}; + console.log((x += 1) + -x); + } + expect: { + var x = {}; + console.log((x += 1) + -x); + } + expect_stdout: true +} From c909ffb7156eb99e83afd8e1e75a876039bdc65b Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 28 Mar 2017 21:25:49 +0800 Subject: [PATCH 37/38] fix `unused` on var of the same name within catch (#1716) fixes #1715 --- lib/compress.js | 8 +++- test/compress/drop-unused.js | 87 ++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index c60f8632..64c654dd 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1894,7 +1894,13 @@ merge(Compressor.prototype, { if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ if (def.value) def.value = def.value.transform(tt); - if (def.name.definition().id in in_use_ids) return true; + var sym = def.name.definition(); + if (sym.id in in_use_ids) return true; + if (sym.orig[0] instanceof AST_SymbolCatch + && sym.scope.parent_scope.find_variable(def.name).orig[0] === def.name) { + def.value = def.value && def.value.drop_side_effect_free(compressor); + return true; + } var w = { name : def.name.name, file : def.name.start.file, diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 10ca5499..aa875ece 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -844,3 +844,90 @@ issue_1709: { } expect_stdout: true } + +issue_1715_1: { + options = { + unused: true, + } + input: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a; + } + } + f(); + console.log(a); + } + expect: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a; + } + } + f(); + console.log(a); + } + expect_stdout: "1" +} + +issue_1715_2: { + options = { + unused: true, + } + input: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a = 2; + } + } + f(); + console.log(a); + } + expect: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a; + } + } + f(); + console.log(a); + } + expect_stdout: "1" +} + +issue_1715_3: { + options = { + unused: true, + } + input: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a = 2 + x(); + } + } + f(); + console.log(a); + } + expect: { + var a = 1; + function f() { + a++; + try {} catch (a) { + var a = x(); + } + } + f(); + console.log(a); + } + expect_stdout: "1" +} From 6ab3224c0d724322597f5709e3f382cc913d96bb Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 28 Mar 2017 21:49:04 +0800 Subject: [PATCH 38/38] v2.8.17 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce6f309c..2ada98ea 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.8.16", + "version": "2.8.17", "engines": { "node": ">=0.8.0" },