From b80062c490178552f45fe66acfa04e6d9c3a6363 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 19 Nov 2017 14:56:23 +0800 Subject: [PATCH 01/10] enable `hoist_props` by default (#2492) --- README.md | 10 +++++----- lib/compress.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 18fc1b151bb..e4380e20178 100644 --- a/README.md +++ b/README.md @@ -632,7 +632,7 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `hoist_funs` (default: `true`) -- hoist function declarations -- `hoist_props` (default: `false`) -- hoist properties from constant object and +- `hoist_props` (default: `true`) -- hoist properties from constant object and array literals into regular variables subject to a set of constraints. For example: `var o={p:1, q:2}; f(o.p, o.q);` is converted to `f(1, 2);`. Note: `hoist_props` works best with `mangle` enabled, the `compress` option `passes` set to `2` or higher, @@ -690,11 +690,11 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u `foo` is certain to not throw, i.e. not `null` or `undefined`. - `reduce_funcs` (default: `true`) -- Allows single-use functions to be - inlined as function expressions when permissible allowing further - optimization. Enabled by default. Option depends on `reduce_vars` - being enabled. Some code runs faster in the Chrome V8 engine if this + inlined as function expressions when permissible allowing further + optimization. Enabled by default. Option depends on `reduce_vars` + being enabled. Some code runs faster in the Chrome V8 engine if this option is disabled. Does not negatively impact other major browsers. - + - `reduce_vars` (default: `true`) -- Improve optimization on variables assigned with and used as constant values. diff --git a/lib/compress.js b/lib/compress.js index 2577643b686..4727aad4a5f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -60,7 +60,7 @@ function Compressor(options, false_by_default) { expression : false, global_defs : {}, hoist_funs : !false_by_default, - hoist_props : false, + hoist_props : !false_by_default, hoist_vars : false, ie8 : false, if_return : !false_by_default, From f4e2fb9864a8c5dd6fb24870c4c09761b5914f75 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 19 Nov 2017 19:29:51 +0800 Subject: [PATCH 02/10] expand symbol space to improve compression (#2460) - give globally distinct names to distinct variables - improve ability to compress cross-scoped - introduce `options.rename` to `minify()` - default `true` if both `compress` & `mangle` --- bin/uglifyjs | 2 + lib/minify.js | 12 +++++- lib/scope.js | 95 ++++++++++++++++++++++++++++++++++---------- test/mocha/minify.js | 3 +- 4 files changed, 89 insertions(+), 23 deletions(-) diff --git a/bin/uglifyjs b/bin/uglifyjs index 04c402d3366..661f726060b 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -47,6 +47,7 @@ program.option("-d, --define [=value]", "Global definitions.", parse_js("d program.option("--ie8", "Support non-standard Internet Explorer 8."); program.option("--keep-fnames", "Do not mangle/drop function names. Useful for code relying on Function.prototype.name."); program.option("--name-cache ", "File to hold mangled name mappings."); +program.option("--no-rename", "Disable symbol expansion."); program.option("--self", "Build UglifyJS as a library (implies --wrap UglifyJS)"); program.option("--source-map [options]", "Enable source map/specify source map options.", parse_source_map()); program.option("--timings", "Display operations run time on STDERR.") @@ -65,6 +66,7 @@ if (!program.output && program.sourceMap && program.sourceMap.url != "inline") { "compress", "ie8", "mangle", + "rename", "sourceMap", "toplevel", "wrap" diff --git a/lib/minify.js b/lib/minify.js index f9d726bf4d7..806c3aebae9 100644 --- a/lib/minify.js +++ b/lib/minify.js @@ -55,6 +55,7 @@ function minify(files, options) { nameCache: null, output: {}, parse: {}, + rename: undefined, sourceMap: false, timings: false, toplevel: false, @@ -64,6 +65,9 @@ function minify(files, options) { var timings = options.timings && { start: Date.now() }; + if (options.rename === undefined) { + options.rename = options.compress && options.mangle; + } set_shorthand("ie8", options, [ "compress", "mangle", "output" ]); set_shorthand("keep_fnames", options, [ "compress", "mangle" ]); set_shorthand("toplevel", options, [ "compress", "mangle" ]); @@ -137,6 +141,11 @@ function minify(files, options) { if (options.wrap) { toplevel = toplevel.wrap_commonjs(options.wrap); } + if (timings) timings.rename = Date.now(); + if (options.rename) { + toplevel.figure_out_scope(options.mangle); + toplevel.expand_names(options.mangle); + } if (timings) timings.compress = Date.now(); if (options.compress) toplevel = new Compressor(options.compress).compress(toplevel); if (timings) timings.scope = Date.now(); @@ -197,7 +206,8 @@ function minify(files, options) { if (timings) { timings.end = Date.now(); result.timings = { - parse: 1e-3 * (timings.compress - timings.parse), + parse: 1e-3 * (timings.rename - timings.parse), + rename: 1e-3 * (timings.compress - timings.rename), compress: 1e-3 * (timings.scope - timings.compress), scope: 1e-3 * (timings.mangle - timings.scope), mangle: 1e-3 * (timings.properties - timings.mangle), diff --git a/lib/scope.js b/lib/scope.js index 0d2a7aeb99c..ea35c0bfdfe 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -43,7 +43,7 @@ "use strict"; -function SymbolDef(scope, index, orig) { +function SymbolDef(scope, orig) { this.name = orig.name; this.orig = [ orig ]; this.eliminated = 0; @@ -53,7 +53,6 @@ function SymbolDef(scope, index, orig) { this.global = false; this.mangled_name = null; this.undeclared = false; - this.index = index; this.id = SymbolDef.next_id++; }; @@ -253,7 +252,7 @@ AST_Toplevel.DEFMETHOD("def_global", function(node){ if (globals.has(name)) { return globals.get(name); } else { - var g = new SymbolDef(this, globals.size(), node); + var g = new SymbolDef(this, node); g.undeclared = true; g.global = true; globals.set(name, g); @@ -314,7 +313,7 @@ AST_Scope.DEFMETHOD("def_function", function(symbol){ AST_Scope.DEFMETHOD("def_variable", function(symbol){ var def; if (!this.variables.has(symbol.name)) { - def = new SymbolDef(this, this.variables.size(), symbol); + def = new SymbolDef(this, symbol); this.variables.set(symbol.name, def); def.global = !this.parent_scope; } else { @@ -332,7 +331,7 @@ AST_Scope.DEFMETHOD("next_mangled", function(options){ // https://github.com/mishoo/UglifyJS2/issues/242 -- do not // shadow a name reserved from mangling. - if (options.reserved.indexOf(m) >= 0) continue; + if (member(m, options.reserved)) continue; // we must ensure that the mangled name does not shadow a name // from some parent scope that is referenced in this or in @@ -384,7 +383,7 @@ AST_Symbol.DEFMETHOD("global", function(){ return this.definition().global; }); -AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ +AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options) { options = defaults(options, { eval : false, ie8 : false, @@ -393,15 +392,14 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ toplevel : false, }); if (!Array.isArray(options.reserved)) options.reserved = []; + // Never mangle arguments + push_uniq(options.reserved, "arguments"); return options; }); AST_Toplevel.DEFMETHOD("mangle_names", function(options){ options = this._default_mangler_options(options); - // Never mangle arguments - options.reserved.push('arguments'); - // We only need to mangle declaration nodes. Special logic wired // into the code generator will display the mangled name if it's // present (and for AST_SymbolRef-s it'll use the mangled name of @@ -410,11 +408,7 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ var to_mangle = []; if (options.cache) { - this.globals.each(function(symbol){ - if (options.reserved.indexOf(symbol.name) < 0) { - to_mangle.push(symbol); - } - }); + this.globals.each(collect); } var tw = new TreeWalker(function(node, descend){ @@ -426,13 +420,7 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ return true; // don't descend again in TreeWalker } if (node instanceof AST_Scope) { - var p = tw.parent(), a = []; - node.variables.each(function(symbol){ - if (options.reserved.indexOf(symbol.name) < 0) { - a.push(symbol); - } - }); - to_mangle.push.apply(to_mangle, a); + node.variables.each(collect); return; } if (node instanceof AST_Label) { @@ -452,6 +440,71 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ if (options.cache) { options.cache.cname = this.cname; } + + function collect(symbol) { + if (!member(symbol.name, options.reserved)) { + to_mangle.push(symbol); + } + } +}); + +AST_Toplevel.DEFMETHOD("find_unique_prefix", function(options) { + var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_"; + var cache = options.cache && options.cache.props; + var prefixes = Object.create(null); + options.reserved.forEach(add_prefix); + this.globals.each(add_def); + this.walk(new TreeWalker(function(node) { + if (node instanceof AST_Scope) node.variables.each(add_def); + })); + var prefix, i = 0; + do { + prefix = create_name(i++); + } while (prefixes[prefix]); + return prefix; + + function add_prefix(name) { + if (/[0-9]$/.test(name)) { + prefixes[name.replace(/[0-9]+$/, "")] = true; + } + } + + function add_def(def) { + var name = def.name; + if (def.global && cache && cache.has(name)) name = cache.get(name); + else if (!def.unmangleable(options)) return; + add_prefix(name); + } + + function create_name(num) { + var name = ""; + do { + name += letters[num % letters.length]; + num = Math.floor(num / letters.length); + } while (num); + return name; + } +}); + +AST_Toplevel.DEFMETHOD("expand_names", function(options) { + options = this._default_mangler_options(options); + var prefix = this.find_unique_prefix(options); + this.globals.each(rename); + this.walk(new TreeWalker(function(node) { + if (node instanceof AST_Scope) node.variables.each(rename); + })); + + function rename(def) { + if (def.global || def.unmangleable(options)) return; + if (member(def.name, options.reserved)) return; + var name = prefix + def.id; + def.orig.forEach(function(sym) { + sym.name = name; + }); + def.references.forEach(function(sym) { + sym.name = name; + }); + } }); AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options){ diff --git a/test/mocha/minify.js b/test/mocha/minify.js index 516541c35e4..157d651564a 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -323,7 +323,8 @@ describe("minify", function() { Uglify.minify(ast, { compress: { sequences: false - } + }, + mangle: false }); assert.ok(stat.body); assert.strictEqual(stat.print_to_string(), "a=x()"); From 30cfea2e7a95fd5aaa8092ea0b305ef0be760534 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Nov 2017 03:05:43 +0800 Subject: [PATCH 03/10] fix `rename` (#2501) - suppress spurious `rename` from `commander` - handle `AST_SymbolCatch` correctly --- bin/uglifyjs | 1 + lib/scope.js | 9 +- test/compress/rename.js | 536 ++++++++++++++++++++++++++++++++++++++++ test/run-tests.js | 4 + 4 files changed, 547 insertions(+), 3 deletions(-) create mode 100644 test/compress/rename.js diff --git a/bin/uglifyjs b/bin/uglifyjs index 661f726060b..718397c1e47 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -72,6 +72,7 @@ if (!program.output && program.sourceMap && program.sourceMap.url != "inline") { "wrap" ].forEach(function(name) { if (name in program) { + if (name == "rename" && program[name]) return; options[name] = program[name]; } }); diff --git a/lib/scope.js b/lib/scope.js index ea35c0bfdfe..f3010e7c675 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -456,6 +456,7 @@ AST_Toplevel.DEFMETHOD("find_unique_prefix", function(options) { this.globals.each(add_def); this.walk(new TreeWalker(function(node) { if (node instanceof AST_Scope) node.variables.each(add_def); + if (node instanceof AST_SymbolCatch) add_def(node.definition()); })); var prefix, i = 0; do { @@ -492,17 +493,19 @@ AST_Toplevel.DEFMETHOD("expand_names", function(options) { this.globals.each(rename); this.walk(new TreeWalker(function(node) { if (node instanceof AST_Scope) node.variables.each(rename); + if (node instanceof AST_SymbolCatch) rename(node.definition()); })); function rename(def) { if (def.global || def.unmangleable(options)) return; if (member(def.name, options.reserved)) return; - var name = prefix + def.id; + var d = def.redefined(); + def.name = d ? d.name : prefix + def.id; def.orig.forEach(function(sym) { - sym.name = name; + sym.name = def.name; }); def.references.forEach(function(sym) { - sym.name = name; + sym.name = def.name; }); } }); diff --git a/test/compress/rename.js b/test/compress/rename.js new file mode 100644 index 00000000000..defc6cf5653 --- /dev/null +++ b/test/compress/rename.js @@ -0,0 +1,536 @@ +mangle_catch: { + rename = true + options = { + ie8: false, + toplevel: false, + } + mangle = { + 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(o){a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_ie8: { + rename = true + options = { + ie8: true, + toplevel: false, + } + mangle = { + 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(args){a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_var: { + rename = true + options = { + ie8: false, + toplevel: false, + } + mangle = { + 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(o){var a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_var_ie8: { + rename = true + options = { + ie8: true, + toplevel: false, + } + mangle = { + 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(args){var a="PASS"}console.log(a);' + expect_stdout: "PASS" +} + +mangle_catch_toplevel: { + rename = true + options = { + ie8: false, + toplevel: true, + } + mangle = { + 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_ie8_toplevel: { + rename = true + options = { + ie8: true, + toplevel: true, + } + mangle = { + 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_var_toplevel: { + rename = true + options = { + ie8: false, + toplevel: true, + } + mangle = { + 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" +} + +mangle_catch_var_ie8_toplevel: { + rename = true + options = { + ie8: true, + toplevel: true, + } + mangle = { + 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_redef_1: { + rename = true + options = { + ie8: false, + toplevel: false, + } + mangle = { + 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_ie8: { + rename = true + options = { + ie8: true, + toplevel: false, + } + mangle = { + 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_toplevel: { + rename = true + options = { + ie8: false, + toplevel: true, + } + mangle = { + 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_1_ie8_toplevel: { + rename = true + options = { + ie8: true, + toplevel: true, + } + mangle = { + 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_2: { + rename = true + options = { + ie8: false, + toplevel: false, + } + mangle = { + 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_ie8: { + rename = true + options = { + ie8: true, + toplevel: false, + } + mangle = { + 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_toplevel: { + rename = true + options = { + ie8: false, + toplevel: true, + } + mangle = { + 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" +} + +mangle_catch_redef_2_ie8_toplevel: { + rename = true + options = { + ie8: true, + toplevel: true, + } + mangle = { + 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" +} + +issue_2120_1: { + rename = true + mangle = { + ie8: false, + } + input: { + "aaaaaaaa"; + var a = 1, b = "FAIL"; + try { + throw 1; + } catch (c) { + try { + throw 0; + } catch (a) { + if (c) b = "PASS"; + } + } + console.log(b); + } + expect: { + "aaaaaaaa"; + var a = 1, b = "FAIL"; + try { + throw 1; + } catch (t) { + try { + throw 0; + } catch (a) { + if (t) b = "PASS"; + } + } + console.log(b); + } + expect_stdout: "PASS" +} + +issue_2120_2: { + rename = true + mangle = { + ie8: true, + } + input: { + "aaaaaaaa"; + var a = 1, b = "FAIL"; + try { + throw 1; + } catch (c) { + try { + throw 0; + } catch (a) { + if (c) b = "PASS"; + } + } + console.log(b); + } + expect: { + "aaaaaaaa"; + var a = 1, b = "FAIL"; + try { + throw 1; + } catch (c) { + try { + throw 0; + } catch (a) { + if (c) b = "PASS"; + } + } + console.log(b); + } + expect_stdout: "PASS" +} +function_iife_catch: { + rename = true + mangle = { + ie8: false, + } + input: { + function f(n) { + !function() { + try { + throw 0; + } catch (n) { + var a = 1; + console.log(n, a); + } + }(); + } + f(); + } + expect_exact: "function f(o){!function(){try{throw 0}catch(c){var o=1;console.log(c,o)}}()}f();" + expect_stdout: "0 1" +} + +function_iife_catch_ie8: { + rename = true + mangle = { + ie8: true, + } + input: { + function f(n) { + !function() { + try { + throw 0; + } catch (n) { + var a = 1; + console.log(n, a); + } + }(); + } + f(); + } + expect_exact: "function f(o){!function(){try{throw 0}catch(o){var c=1;console.log(o,c)}}()}f();" + expect_stdout: "0 1" +} + +function_catch_catch: { + rename = true + mangle = { + ie8: false, + } + input: { + var o = 0; + function f() { + try { + throw 1; + } catch (c) { + try { + throw 2; + } catch (o) { + var o = 3; + console.log(o); + } + } + console.log(o); + } + f(); + } + expect_exact: "var o=0;function f(){try{throw 1}catch(c){try{throw 2}catch(o){var o=3;console.log(o)}}console.log(o)}f();" + expect_stdout: [ + "3", + "undefined", + ] +} + +function_catch_catch_ie8: { + rename = true + mangle = { + ie8: true, + } + input: { + var o = 0; + function f() { + try { + throw 1; + } catch (c) { + try { + throw 2; + } catch (o) { + var o = 3; + console.log(o); + } + } + console.log(o); + } + f(); + } + expect_exact: "var o=0;function f(){try{throw 1}catch(c){try{throw 2}catch(o){var o=3;console.log(o)}}console.log(o)}f();" + expect_stdout: [ + "3", + "undefined", + ] +} diff --git a/test/run-tests.js b/test/run-tests.js index 0051c6c2f59..e95bbb83cd6 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -117,6 +117,10 @@ function run_compress_tests() { test.mangle.properties.reserved = quoted_props; U.reserve_quoted_keys(input, quoted_props); } + if (test.rename) { + input.figure_out_scope(test.mangle); + input.expand_names(test.mangle); + } var cmp = new U.Compressor(options, true); var output = cmp.compress(input); output.figure_out_scope(test.mangle); From 8987780db66ad178f42bdd817f27853b55c37b27 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Nov 2017 04:12:37 +0800 Subject: [PATCH 04/10] eliminate invalid state caching in `collapse_vars` (#2502) fixes #2497 --- lib/compress.js | 24 ++++++++++---------- test/compress/collapse_vars.js | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 4727aad4a5f..987ad01c2b2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -869,7 +869,7 @@ merge(Compressor.prototype, { && !(node instanceof AST_SymbolDeclaration) && lhs.equivalent_to(node)) { if (is_lhs(node, parent)) { - if (candidate.multiple) replaced++; + if (value_def) replaced++; return node; } CHANGED = abort = true; @@ -884,7 +884,7 @@ merge(Compressor.prototype, { return make_node(AST_UnaryPrefix, candidate, candidate); } if (candidate instanceof AST_VarDef) { - if (candidate.multiple) { + if (value_def) { abort = false; return node; } @@ -958,12 +958,13 @@ merge(Compressor.prototype, { extract_candidates(statements[stat_index]); while (candidates.length > 0) { var candidate = candidates.pop(); + var value_def = null; var lhs = get_lhs(candidate); if (!lhs || is_lhs_read_only(lhs) || lhs.has_side_effects(compressor)) continue; // Locate symbols which may execute code outside of scanning range var lvalues = get_lvalues(candidate); if (lhs instanceof AST_SymbolRef) lvalues[lhs.name] = false; - var replace_all = candidate.multiple; + var replace_all = value_def; if (!replace_all && lhs instanceof AST_SymbolRef) { var def = lhs.definition(); replace_all = def.references.length - def.replaced == 1; @@ -980,13 +981,12 @@ merge(Compressor.prototype, { for (var i = stat_index; !abort && i < statements.length; i++) { statements[i].transform(scanner); } - if (candidate.multiple) { + if (value_def) { var def = candidate.name.definition(); if (abort && def.references.length - def.replaced > replaced) replaced = false; else { abort = false; hit = candidate.name instanceof AST_SymbolFunarg; - var value_def = candidate.value.definition(); for (var i = stat_index; !abort && i < statements.length; i++) { statements[i].transform(multi_replacer); } @@ -1061,13 +1061,13 @@ merge(Compressor.prototype, { } } - function mangleable_var(expr) { - var value = expr.value; - if (!(value instanceof AST_SymbolRef)) return false; - if (value.name == "arguments") return false; - if (value.definition().undeclared) return false; - expr.multiple = true; - return true; + function mangleable_var(var_def) { + var value = var_def.value; + if (!(value instanceof AST_SymbolRef)) return; + if (value.name == "arguments") return; + var def = value.definition(); + if (def.undeclared) return; + return value_def = def; } function get_lhs(expr) { diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 10735b28988..e89d44dfb6a 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -3578,3 +3578,43 @@ issue_2436_14: { } expect_stdout: true } + +issue_2497: { + options = { + collapse_vars: true, + unused: true, + } + input: { + function sample() { + if (true) { + for (var i = 0; i < 1; ++i) { + for (var k = 0; k < 1; ++k) { + var value = 1; + var x = value; + value = x ? x + 1 : 0; + } + } + } else { + for (var i = 0; i < 1; ++i) { + for (var k = 0; k < 1; ++k) { + var value = 1; + } + } + } + } + } + expect: { + function sample() { + if (true) + for (i = 0; i < 1; ++i) + for (k = 0; k < 1; ++k) { + value = 1; + value = value ? value + 1 : 0; + } + else + for (var i = 0; i < 1; ++i) + for (var k = 0; k < 1; ++k) + var value=1; + } + } +} From aa9bdf416e5b288bbae8417780abf2f235bacfd4 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Nov 2017 07:03:37 +0800 Subject: [PATCH 05/10] make `AST_Lambda.contains_this()` less magical (#2505) --- lib/compress.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 987ad01c2b2..ce881c0578e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -610,7 +610,8 @@ merge(Compressor.prototype, { || !immutable && parent instanceof AST_Call && parent.expression === node - && (!(value instanceof AST_Function) || value.contains_this(parent))) { + && (!(value instanceof AST_Function) + || !(parent instanceof AST_New) && value.contains_this())) { return true; } else if (parent instanceof AST_Array) { return is_modified(parent, parent, level + 1); @@ -4764,8 +4765,7 @@ merge(Compressor.prototype, { return self; }); - AST_Lambda.DEFMETHOD("contains_this", function(grandparent) { - if (grandparent instanceof AST_New) return false; + AST_Lambda.DEFMETHOD("contains_this", function() { var result; var self = this; self.walk(new TreeWalker(function(node) { @@ -4789,7 +4789,8 @@ merge(Compressor.prototype, { })) break; var value = prop.value; if (value instanceof AST_Function - && value.contains_this(compressor.parent())) break; + && !(compressor.parent() instanceof AST_New) + && value.contains_this()) break; return make_node(AST_Sub, this, { expression: make_node(AST_Array, expr, { elements: props.map(function(prop) { From eb001dc1d9efbb81841410c06d854091473061ee Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Nov 2017 07:26:22 +0800 Subject: [PATCH 06/10] fix argument/atom collision by `collapse_vars` (#2507) fixes #2506 --- lib/compress.js | 24 ++++++++++++++------- test/compress/collapse_vars.js | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index ce881c0578e..d8a0bd7c762 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -806,6 +806,12 @@ merge(Compressor.prototype, { } } + function is_identifier_atom(node) { + return node instanceof AST_Infinity + || node instanceof AST_NaN + || node instanceof AST_Undefined; + } + function tighten_body(statements, compressor) { var CHANGED, max_iter = 10; do { @@ -890,14 +896,19 @@ merge(Compressor.prototype, { return node; } var def = candidate.name.definition(); + var value = candidate.value; if (def.references.length - def.replaced == 1 && !compressor.exposed(def)) { def.replaced++; - return maintain_this_binding(parent, node, candidate.value); + if (funarg && is_identifier_atom(value)) { + return value.transform(compressor); + } else { + return maintain_this_binding(parent, node, value); + } } return make_node(AST_Assign, candidate, { operator: "=", left: make_node(AST_SymbolRef, candidate.name, candidate.name), - right: candidate.value + right: value }); } candidate.write_only = false; @@ -971,7 +982,8 @@ merge(Compressor.prototype, { replace_all = def.references.length - def.replaced == 1; } var side_effects = value_has_side_effects(candidate); - var hit = candidate.name instanceof AST_SymbolFunarg; + var funarg = candidate.name instanceof AST_SymbolFunarg; + var hit = funarg; var abort = false, replaced = 0, can_replace = !args || !hit; if (!can_replace) { for (var j = compressor.self().argnames.lastIndexOf(candidate.name) + 1; !abort && j < args.length; j++) { @@ -987,7 +999,7 @@ merge(Compressor.prototype, { if (abort && def.references.length - def.replaced > replaced) replaced = false; else { abort = false; - hit = candidate.name instanceof AST_SymbolFunarg; + hit = funarg; for (var i = stat_index; !abort && i < statements.length; i++) { statements[i].transform(multi_replacer); } @@ -3810,9 +3822,7 @@ merge(Compressor.prototype, { if (self.operator == "delete" && !(e instanceof AST_SymbolRef || e instanceof AST_PropAccess - || e instanceof AST_NaN - || e instanceof AST_Infinity - || e instanceof AST_Undefined)) { + || is_identifier_atom(e))) { if (e instanceof AST_Sequence) { e = e.expressions.slice(); e.push(make_node(AST_True, self)); diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index e89d44dfb6a..844d5b0fe4b 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -3618,3 +3618,41 @@ issue_2497: { } } } + +issue_2506: { + options = { + collapse_vars: true, + reduce_vars: true, + unused: true, + } + input: { + var c = 0; + function f0(bar) { + function f1(Infinity_2) { + function f13(NaN) { + if (false <= NaN & this >> 1 >= 0) { + c++; + } + } + var b_2 = f13(NaN, c++); + } + var bar = f1(-3, -1); + } + f0(false); + console.log(c); + } + expect: { + var c = 0; + function f0(bar) { + (function(Infinity_2) { + (function(NaN) { + if (false <= 0/0 & this >> 1 >= 0) + c++; + })(0, c++); + })(); + } + f0(false); + console.log(c); + } + expect_stdout: "1" +} From 3b28b915ebad2d9452c0e8649033ab4813eabca4 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 24 Nov 2017 14:07:39 +0800 Subject: [PATCH 07/10] extend escape analysis on constant expression properties (#2509) fixes #2508 --- lib/compress.js | 16 +++-- test/compress/hoist_props.js | 133 +++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index d8a0bd7c762..744a1ea5ba7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -333,7 +333,7 @@ merge(Compressor.prototype, { } } } - mark_escaped(d, node, value, 0); + mark_escaped(d, node.scope, node, value, 0); } if (node instanceof AST_SymbolCatch) { node.definition().fixed = false; @@ -623,10 +623,12 @@ merge(Compressor.prototype, { } } - function mark_escaped(d, node, value, level) { + function mark_escaped(d, scope, node, value, level) { var parent = tw.parent(level); - if (value instanceof AST_Constant) return; - if (level > 0 && value instanceof AST_Function) return; + if (value) { + if (value.is_constant()) return; + if (level > 0 && value.is_constant_expression(scope)) return; + } if (parent instanceof AST_Assign && parent.operator == "=" && node === parent.right || parent instanceof AST_Call && node !== parent.expression || parent instanceof AST_Return && node === parent.value && node.scope !== d.scope @@ -634,13 +636,13 @@ merge(Compressor.prototype, { d.escaped = true; return; } else if (parent instanceof AST_Array) { - mark_escaped(d, parent, parent, level + 1); + mark_escaped(d, scope, parent, parent, level + 1); } else if (parent instanceof AST_ObjectKeyVal && node === parent.value) { var obj = tw.parent(level + 1); - mark_escaped(d, obj, obj, level + 2); + mark_escaped(d, scope, obj, obj, level + 2); } else if (parent instanceof AST_PropAccess && node === parent.expression) { value = read_property(value, parent.property); - mark_escaped(d, parent, value, level + 1); + mark_escaped(d, scope, parent, value, level + 1); if (value) return; } if (level == 0) d.direct_access = true; diff --git a/test/compress/hoist_props.js b/test/compress/hoist_props.js index 40d36acefdf..b2dd0270054 100644 --- a/test/compress/hoist_props.js +++ b/test/compress/hoist_props.js @@ -500,3 +500,136 @@ issue_2473_4: { } expect_stdout: "1 2" } + +issue_2508_1: { + options = { + collapse_vars: true, + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + a: [ 1 ], + f: function(x) { + console.log(x); + } + }; + o.f(o.a); + } + expect: { + (function(x) { + console.log(x); + })([ 1 ]); + } + expect_stdout: true +} + +issue_2508_2: { + options = { + collapse_vars: true, + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + a: { b: 2 }, + f: function(x) { + console.log(x); + } + }; + o.f(o.a); + } + expect: { + (function(x) { + console.log(x); + })({ b: 2 }); + } + expect_stdout: true +} + +issue_2508_3: { + options = { + collapse_vars: true, + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + a: [ o ], + f: function(x) { + console.log(x); + } + }; + o.f(o.a); + } + expect: { + var o = { + a: [ o ], + f: function(x) { + console.log(x); + } + }; + o.f(o.a); + } + expect_stdout: true +} + +issue_2508_4: { + options = { + collapse_vars: true, + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + a: { b: o }, + f: function(x) { + console.log(x); + } + }; + o.f(o.a); + } + expect: { + var o = { + a: { b: o }, + f: function(x) { + console.log(x); + } + }; + o.f(o.a); + } + expect_stdout: true +} + +issue_2508_5: { + options = { + collapse_vars: true, + hoist_props: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var o = { + f: function(x) { + console.log(x); + } + }; + o.f(o.f); + } + expect: { + var o_f = function(x) { + console.log(x); + }; + o_f(o_f); + } + expect_stdout: true +} From 97c464dbf5aec124cba9cfeedb896fa97818622c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Nov 2017 19:07:46 +0800 Subject: [PATCH 08/10] fix wording and formatting (#2512) --- README.md | 62 +++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index e4380e20178..8a34f6e4905 100644 --- a/README.md +++ b/README.md @@ -598,17 +598,17 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u ## Compress options -- `booleans` (default: `true`) -- various optimizations for boolean context, for example `!!a - ? b : c → a ? b : c` +- `booleans` (default: `true`) -- various optimizations for boolean context, + for example `!!a ? b : c → a ? b : c` -- `cascade` (default: `true`) -- small optimization for sequences, transform `x, x` into `x` - and `x = something(), x` into `x = something()` +- `cascade` (default: `true`) -- small optimization for sequences, transform + `x, x` into `x` and `x = something(), x` into `x = something()` -- `collapse_vars` (default: `true`) -- Collapse single-use non-constant variables - side - effects permitting. +- `collapse_vars` (default: `true`) -- Collapse single-use non-constant variables, + side effects permitting. -- `comparisons` (default: `true`) -- apply certain optimizations to binary nodes, for example: - `!(a <= b) → a > b` (only when `unsafe_comps`), attempts to negate binary +- `comparisons` (default: `true`) -- apply certain optimizations to binary nodes, + e.g. `!(a <= b) → a > b` (only when `unsafe_comps`), attempts to negate binary nodes, e.g. `a = !b && !c && !d && !e → a=!(b||c||d||e)` etc. - `conditionals` (default: `true`) -- apply optimizations for `if`-s and conditional @@ -616,7 +616,7 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `dead_code` (default: `true`) -- remove unreachable code -- `drop_console` (default: `false`) -- default `false`. Pass `true` to discard calls to +- `drop_console` (default: `false`) -- Pass `true` to discard calls to `console.*` functions. If you wish to drop a specific function call such as `console.info` and/or retain side effects from function arguments after dropping the function call then use `pure_funcs` instead. @@ -625,7 +625,7 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `evaluate` (default: `true`) -- attempt to evaluate constant expressions -- `expression` (default: `false`) -- default `false`. Pass `true` to preserve completion values +- `expression` (default: `false`) -- Pass `true` to preserve completion values from terminal statements without `return`, e.g. in bookmarklets. - `global_defs` (default: `{}`) -- see [conditional compilation](#conditional-compilation) @@ -647,19 +647,18 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `join_vars` (default: `true`) -- join consecutive `var` statements -- `keep_fargs` (default: `true`) -- default `true`. Prevents the - compressor from discarding unused function arguments. You need this - for code which relies on `Function.length`. +- `keep_fargs` (default: `true`) -- Prevents the compressor from discarding unused + function arguments. You need this for code which relies on `Function.length`. - `keep_fnames` (default: `false`) -- Pass `true` to prevent the compressor from discarding function names. Useful for code relying on `Function.prototype.name`. See also: the `keep_fnames` [mangle option](#mangle). -- `keep_infinity` (default: `false`) -- default `false`. Pass `true` to prevent `Infinity` from +- `keep_infinity` (default: `false`) -- Pass `true` to prevent `Infinity` from being compressed into `1/0`, which may cause performance issues on Chrome. -- `loops` (default: `true`) -- optimizations for `do`, `while` and `for` loops when we can - statically determine the condition +- `loops` (default: `true`) -- optimizations for `do`, `while` and `for` loops + when we can statically determine the condition. - `negate_iife` (default: `true`) -- negate "Immediately-Called Function Expressions" where the return value is discarded, to avoid the parens that the @@ -707,21 +706,22 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u occasions the default sequences limit leads to very slow compress times in which case a value of `20` or less is recommended. -- `side_effects` (default: `true`) -- default `true`. Pass `false` to disable potentially dropping +- `side_effects` (default: `true`) -- Pass `false` to disable potentially dropping functions marked as "pure". A function call is marked as "pure" if a comment annotation `/*@__PURE__*/` or `/*#__PURE__*/` immediately precedes the call. For example: `/*@__PURE__*/foo();` - `switches` (default: `true`) -- de-duplicate and remove unreachable `switch` branches -- `toplevel` (default: `false`) -- drop unreferenced functions (`"funcs"`) and/or variables (`"vars"`) - in the top level scope (`false` by default, `true` to drop both unreferenced - functions and variables) +- `toplevel` (default: `false`) -- drop unreferenced functions (`"funcs"`) and/or + variables (`"vars"`) in the top level scope (`false` by default, `true` to drop + both unreferenced functions and variables) -- `top_retain` (default: `null`) -- prevent specific toplevel functions and variables from `unused` - removal (can be array, comma-separated, RegExp or function. Implies `toplevel`) +- `top_retain` (default: `null`) -- prevent specific toplevel functions and + variables from `unused` removal (can be array, comma-separated, RegExp or + function. Implies `toplevel`) -- `typeofs` (default: `true`) -- default `true`. Transforms `typeof foo == "undefined"` into +- `typeofs` (default: `true`) -- Transforms `typeof foo == "undefined"` into `foo === void 0`. Note: recommend to set this value to `false` for IE10 and earlier versions due to known issues. @@ -746,25 +746,25 @@ If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.u - `unsafe_regexp` (default: `false`) -- enable substitutions of variables with `RegExp` values the same way as if they are constants. -- `unused` (default: `true`) -- drop unreferenced functions and variables (simple direct variable - assignments do not count as references unless set to `"keep_assign"`) +- `unused` (default: `true`) -- drop unreferenced functions and variables (simple + direct variable assignments do not count as references unless set to `"keep_assign"`) -- `warnings` (default: `false`) -- display warnings when dropping unreachable code or unused - declarations etc. +- `warnings` (default: `false`) -- display warnings when dropping unreachable + code or unused declarations etc. ## Mangle options -- `eval` (default `false`). Pass `true` to mangle names visible in scopes +- `eval` (default `false`) -- Pass `true` to mangle names visible in scopes where `eval` or `with` are used. -- `keep_fnames` (default `false`). Pass `true` to not mangle function names. +- `keep_fnames` (default `false`) -- Pass `true` to not mangle function names. Useful for code relying on `Function.prototype.name`. See also: the `keep_fnames` [compress option](#compress-options). -- `reserved` (default `[]`). Pass an array of identifiers that should be +- `reserved` (default `[]`) -- Pass an array of identifiers that should be excluded from mangling. Example: `["foo", "bar"]`. -- `toplevel` (default `false`). Pass `true` to mangle names declared in the +- `toplevel` (default `false`) -- Pass `true` to mangle names declared in the top level scope. Examples: From c141ae6f8dcfa058f03ae3580275c87b564b77a3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 25 Nov 2017 22:52:46 +0800 Subject: [PATCH 09/10] fix argument/atom collision by `properties` (#2514) fixes #2513 --- lib/compress.js | 19 +++++++++++-------- test/compress/properties.js | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 744a1ea5ba7..22415f4d131 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -4708,6 +4708,17 @@ merge(Compressor.prototype, { if (compressor.option("properties")) { var key = prop.evaluate(compressor); if (key !== prop) { + if (typeof key == "string") { + if (key == "undefined") { + key = undefined; + } else { + var value = parseFloat(key); + if (value.toString() == key) { + key = value; + } + } + } + prop = self.property = best_of_expression(prop, make_node_from_constant(key, prop).transform(compressor)); var property = "" + key; if (is_identifier_string(property) && property.length <= prop.print_to_string().length + 1) { @@ -4716,14 +4727,6 @@ merge(Compressor.prototype, { property: property }).optimize(compressor); } - if (!(prop instanceof AST_Number)) { - var value = parseFloat(property); - if (value.toString() == property) { - prop = self.property = make_node(AST_Number, prop, { - value: value - }); - } - } } } if (is_lhs(self, compressor.parent())) return self; diff --git a/test/compress/properties.js b/test/compress/properties.js index f2e59321d52..6d4c0281b66 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -1028,3 +1028,29 @@ new_this: { }(42); } } + +issue_2513: { + options = { + evaluate: true, + properties: true, + } + input: { + !function(Infinity, NaN, undefined) { + console.log("a"[1/0], "b"["Infinity"]); + console.log("c"[0/0], "d"["NaN"]); + console.log("e"[void 0], "f"["undefined"]); + }(0, 0, 0); + } + expect: { + !function(Infinity, NaN, undefined) { + console.log("a"[1/0], "b"[1/0]); + console.log("c".NaN, "d".NaN); + console.log("e"[void 0], "f"[void 0]); + }(0, 0, 0); + } + expect_stdout: [ + "undefined undefined", + "undefined undefined", + "undefined undefined", + ] +} From b37a68c84f7b74de8ec7fc862792964c436fa2ec Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 26 Nov 2017 04:08:35 +0800 Subject: [PATCH 10/10] v3.2.0 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ba9cbe8b38b..cbde945a118 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": "3.1.10", + "version": "3.2.0", "engines": { "node": ">=0.8.0" }, @@ -29,11 +29,11 @@ "LICENSE" ], "dependencies": { - "commander": "~2.11.0", + "commander": "~2.12.1", "source-map": "~0.6.1" }, "devDependencies": { - "acorn": "~5.1.1", + "acorn": "~5.2.1", "mocha": "~3.5.1", "semver": "~5.4.1" },