diff --git a/README.md b/README.md index a8b55843..3e7ed5e5 100644 --- a/README.md +++ b/README.md @@ -450,6 +450,9 @@ UglifyJS will warn about the condition being always false and about dropping unreachable code; for now there is no option to turn off only this specific warning, you can pass `warnings=false` to turn off *all* warnings. +You can specify nested constants like `--define env.DEBUG=false` which requires +`-c unsafe` in order to work. + Another way of doing that is to declare your globals as constants in a separate file and include it into the build. For example you can have a `build/defines.js` file with the following: diff --git a/bin/uglifyjs b/bin/uglifyjs index 8cb2f0df..78d6bcde 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -184,7 +184,7 @@ if (ARGS.reserve_domprops) { } if (ARGS.d) { - if (COMPRESS) COMPRESS.global_defs = getOptions("d"); + if (COMPRESS) COMPRESS.global_defs = getOptions("d", true); } if (ARGS.pure_funcs) { diff --git a/lib/compress.js b/lib/compress.js index 4e45df92..37870abc 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -82,6 +82,22 @@ function Compressor(options, false_by_default) { }, true); var sequences = this.options["sequences"]; this.sequences_limit = sequences == 1 ? 200 : sequences | 0; + if ("global_defs" in this.options) { + var global_defs = this.options["global_defs"]; + for (var key in global_defs) { + var tokens = key.split(/\./); + if (tokens.length > 1) { + var last = tokens.pop(); + tokens.reduce(function(o, token) { + if (!(token in o)) { + o[token] = {}; + } + return o[token]; + }, global_defs)[last] = global_defs[key]; + delete global_defs[key]; + } + } + } this.warnings_produced = {}; }; @@ -154,7 +170,7 @@ merge(Compressor.prototype, { this.walk(new TreeWalker(function(node){ if (node instanceof AST_SymbolRef) { var d = node.definition(); - if (d && d.init) { + if (d.init) { delete d.init._evaluated; } } @@ -175,17 +191,6 @@ merge(Compressor.prototype, { }; function make_node_from_constant(compressor, val, orig) { - // XXX: WIP. - // if (val instanceof AST_Node) return val.transform(new TreeTransformer(null, function(node){ - // if (node instanceof AST_SymbolRef) { - // var scope = compressor.find_parent(AST_Scope); - // var def = scope.find_variable(node); - // node.thedef = def; - // return node; - // } - // })).transform(compressor); - - if (val instanceof AST_Node) return val.transform(compressor); switch (typeof val) { case "string": return make_node(AST_String, orig, { @@ -977,8 +982,11 @@ merge(Compressor.prototype, { }); function isLHS(node, parent) { - return parent instanceof AST_Unary && (parent.operator === "++" || parent.operator === "--") - || parent instanceof AST_Assign && parent.left === node; + return (parent instanceof AST_Unary + && parent.expression === node + && (parent.operator == "++" || parent.operator == "--")) + || (parent instanceof AST_Assign + && parent.left === node); } function best_of(ast1, ast2) { @@ -998,12 +1006,15 @@ merge(Compressor.prototype, { // constant; otherwise it's the original or a replacement node. AST_Node.DEFMETHOD("evaluate", function(compressor){ if (!compressor.option("evaluate")) return [ this ]; - var val; + var val, has_global_defs; try { val = this._eval(compressor); + has_global_defs = compressor._ev_has_global_defs; } catch(ex) { if (ex !== def) throw ex; return [ this ]; + } finally { + delete compressor._ev_has_global_defs; } var node; try { @@ -1011,6 +1022,17 @@ merge(Compressor.prototype, { } catch(ex) { return [ this ]; } + if (has_global_defs) { + var lhs, parent = this, level = 0; + do { + lhs = parent; + parent = compressor.parent(level++); + } while (parent instanceof AST_PropAccess && parent.expression === lhs); + if (isLHS(lhs, parent)) { + compressor.warn('global_defs ' + lhs.print_to_string() + ' redefined [{file}:{line},{col}]', lhs.start); + return [ this ]; + } + } return [ best_of(node, this), val ]; }); var unaryPrefix = makePredicate("! ~ - +"); @@ -1167,7 +1189,14 @@ merge(Compressor.prototype, { this._evaluating = true; try { var d = this.definition(); - if (d && (d.constant || compressor.option("reduce_vars") && !d.modified) && d.init) { + if (d.undeclared) { + var defines = compressor.option("global_defs"); + if (defines && HOP(defines, d.name)) { + compressor._ev_has_global_defs = true; + return defines[d.name]; + } + } + if ((d.constant || compressor.option("reduce_vars") && !d.modified) && d.init) { if (compressor.option("unsafe")) { if (!HOP(d.init, '_evaluated')) { d.init._evaluated = ev(d.init, compressor); @@ -2686,13 +2715,14 @@ merge(Compressor.prototype, { }); OPT(AST_SymbolRef, function(self, compressor){ - if (self.undeclared() && !isLHS(self, compressor.parent())) { - var defines = compressor.option("global_defs"); - if (defines && HOP(defines, self.name)) { - return make_node_from_constant(compressor, defines[self.name], self); + if (self.undeclared()) { + var e = self.evaluate(compressor); + if (e.length > 1) { + return self === e[0] ? make_node_from_constant(compressor, e[1], self) : e[0]; } // testing against !self.scope.uses_with first is an optimization - if (!self.scope.uses_with || !compressor.find_parent(AST_With)) { + if (!isLHS(self, compressor) + && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { case "undefined": return make_node(AST_Undefined, self); diff --git a/test/compress/global_defs.js b/test/compress/global_defs.js new file mode 100644 index 00000000..62e1b7fd --- /dev/null +++ b/test/compress/global_defs.js @@ -0,0 +1,144 @@ +must_replace: { + options = { + evaluate: true, + global_defs: { + D: "foo bar", + } + } + input: { + console.log(D); + } + expect: { + console.log("foo bar"); + } +} + +keyword: { + options = { + evaluate: true, + global_defs: { + undefined: 0, + NaN: 1, + Infinity: 2, + }, + } + input: { + console.log(undefined, NaN, Infinity); + } + expect: { + console.log(0, 1, 2); + } +} + +object: { + options = { + evaluate: true, + global_defs: { + CONFIG: { + DEBUG: [ 0 ], + VALUE: 42, + }, + }, + unsafe: true, + } + input: { + function f(CONFIG) { + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + return CONFIG.VALUE; + } + function h() { + return CONFIG.VALUE; + } + if (CONFIG.DEBUG[0]) + console.debug("foo"); + } + expect: { + function f(CONFIG) { + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + return CONFIG.VALUE; + } + function h() { + return 42; + } + if (0) + console.debug("foo"); + } +} + +expanded: { + options = { + evaluate: true, + global_defs: { + "CONFIG.DEBUG": [ 0 ], + "CONFIG.VALUE": 42, + }, + unsafe: true, + } + input: { + function f(CONFIG) { + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + return CONFIG.VALUE; + } + function h() { + return CONFIG.VALUE; + } + if (CONFIG.DEBUG[0]) + console.debug("foo"); + } + expect: { + function f(CONFIG) { + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + return CONFIG.VALUE; + } + function h() { + return 42; + } + if (0) + console.debug("foo"); + } +} + +mixed: { + options = { + evaluate: true, + global_defs: { + "CONFIG.VALUE": 42, + }, + unsafe: true, + } + input: { + console.log(++CONFIG.DEBUG); + console.log(++CONFIG.VALUE); + console.log(++CONFIG["VAL" + "UE"]); + console.log(++DEBUG[CONFIG.VALUE]); + CONFIG.VALUE.FOO = "bar"; + console.log(CONFIG, ++(1 + 1)); + console.log(({ 42: "foo" })[CONFIG.VALUE] += "bar"); + } + expect: { + console.log(++CONFIG.DEBUG); + console.log(++CONFIG.VALUE); + console.log(++CONFIG["VALUE"]); + console.log(++DEBUG[42]); + CONFIG.VALUE.FOO = "bar"; + console.log(CONFIG, ++2); + console.log("foo" += "bar"); + } + expect_warnings: [ + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:123,22]', + 'WARN: global_defs CONFIG["VALUE"] redefined [test/compress/global_defs.js:124,22]', + 'WARN: global_defs CONFIG.VALUE.FOO redefined [test/compress/global_defs.js:126,8]', + ] +} diff --git a/test/compress/issue-208.js b/test/compress/issue-208.js index 2f103786..58f67bce 100644 --- a/test/compress/issue-208.js +++ b/test/compress/issue-208.js @@ -1,5 +1,6 @@ do_not_update_lhs: { options = { + evaluate: true, global_defs: { DEBUG: 0 } } input: { @@ -16,6 +17,7 @@ do_not_update_lhs: { do_update_rhs: { options = { + evaluate: true, global_defs: { DEBUG: 0 } } input: { @@ -27,3 +29,29 @@ do_update_rhs: { MY_DEBUG += 0; } } + +mixed: { + options = { + evaluate: true, + global_defs: { DEBUG: 0 } + } + input: { + DEBUG = 1; + DEBUG++; + DEBUG += 1; + f(DEBUG); + x = DEBUG; + } + expect: { + DEBUG = 1; + DEBUG++; + DEBUG += 1; + f(0); + x = 0; + } + expect_warnings: [ + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:39,8]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:40,8]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:41,8]', + ] +} diff --git a/test/input/issue-1467/simple.js b/test/input/issue-1467/simple.js new file mode 100644 index 00000000..44d515e3 --- /dev/null +++ b/test/input/issue-1467/simple.js @@ -0,0 +1 @@ +console.log(D); diff --git a/test/input/issue-1467/unsafe.js b/test/input/issue-1467/unsafe.js new file mode 100644 index 00000000..dbf57909 --- /dev/null +++ b/test/input/issue-1467/unsafe.js @@ -0,0 +1 @@ +console.log(C.V, C.D); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index a8de05c5..5ba0db5e 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -100,4 +100,24 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should work with --define", function (done) { + var command = uglifyjscmd + ' test/input/issue-1467/simple.js --define D=5 -c'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "console.log(5);\n"); + done(); + }); + }); + it("Should work with --define & -c unsafe", function (done) { + var command = uglifyjscmd + ' test/input/issue-1467/unsafe.js --define C.D=5,C.V=3 -c unsafe'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "console.log(3,5);\n"); + done(); + }); + }); });