From: alexlamsl Date: Sat, 18 Feb 2017 11:27:31 +0000 (+0800) Subject: enhance `global_defs` X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=e275148998638bdcf795257ed03941ca34e33018;p=UglifyJS.git enhance `global_defs` - support arrays, objects & AST_Node - support `"a.b":1` on both cli & API - emit warning if variable is modified - override top-level variables fixes #1416 closes #1198 closes #1469 --- diff --git a/README.md b/README.md index a2eaeae4..1d1f2fcb 100644 --- a/README.md +++ b/README.md @@ -454,6 +454,8 @@ if (DEBUG) { } ``` +You can specify nested constants in the form of `--define env.DEBUG=false`. + 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. diff --git a/lib/compress.js b/lib/compress.js index a60ba1a1..cb99a173 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -219,17 +219,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, { @@ -991,6 +980,68 @@ merge(Compressor.prototype, { || parent instanceof AST_Assign && parent.left === node; } + (function (def){ + AST_Node.DEFMETHOD("resolve_defines", function(compressor) { + if (!compressor.option("global_defs")) return; + var def = this._find_defs(compressor, ""); + if (def) { + var node, parent = this, level = 0; + do { + node = parent; + parent = compressor.parent(level++); + } while (parent instanceof AST_PropAccess && parent.expression === node); + if (isLHS(node, parent)) { + compressor.warn('global_defs ' + this.print_to_string() + ' redefined [{file}:{line},{col}]', this.start); + } else { + return def; + } + } + }); + function to_node(compressor, value, orig) { + if (value instanceof AST_Node) return make_node(value.CTOR, orig, value); + if (Array.isArray(value)) return make_node(AST_Array, orig, { + elements: value.map(function(value) { + return to_node(compressor, value, orig); + }) + }); + if (value && typeof value == "object") { + var props = []; + for (var key in value) { + props.push(make_node(AST_ObjectKeyVal, orig, { + key: key, + value: to_node(compressor, value[key], orig) + })); + } + return make_node(AST_Object, orig, { + properties: props + }); + } + return make_node_from_constant(compressor, value, orig); + } + def(AST_Node, noop); + def(AST_Dot, function(compressor, suffix){ + return this.expression._find_defs(compressor, suffix + "." + this.property); + }); + def(AST_SymbolRef, function(compressor, suffix){ + if (!this.global()) return; + var name; + var defines = compressor.option("global_defs"); + if (defines && HOP(defines, (name = this.name + suffix))) { + var node = to_node(compressor, defines[name], this); + var top = compressor.find_parent(AST_Toplevel); + node.walk(new TreeWalker(function(node) { + if (node instanceof AST_SymbolRef) { + node.scope = top; + node.thedef = top.def_global(node); + } + })); + return node; + } + }); + })(function(node, func){ + node.DEFMETHOD("_find_defs", func); + }); + function best_of(ast1, ast2) { return ast1.print_to_string().length > ast2.print_to_string().length @@ -2793,21 +2844,20 @@ 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); - } - // testing against !self.scope.uses_with first is an optimization - if (!self.scope.uses_with || !compressor.find_parent(AST_With)) { - switch (self.name) { - case "undefined": - return make_node(AST_Undefined, self); - case "NaN": - return make_node(AST_NaN, self).transform(compressor); - case "Infinity": - return make_node(AST_Infinity, self).transform(compressor); - } + var def = self.resolve_defines(compressor); + if (def) { + return def; + } + // testing against !self.scope.uses_with first is an optimization + if (self.undeclared() && !isLHS(self, compressor.parent()) + && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { + switch (self.name) { + case "undefined": + return make_node(AST_Undefined, self); + case "NaN": + return make_node(AST_NaN, self).transform(compressor); + case "Infinity": + return make_node(AST_Infinity, self).transform(compressor); } } if (compressor.option("evaluate") && !isLHS(self, compressor.parent())) { @@ -3085,6 +3135,10 @@ merge(Compressor.prototype, { }); OPT(AST_Dot, function(self, compressor){ + var def = self.resolve_defines(compressor); + if (def) { + return def; + } var prop = self.property; if (RESERVED_WORDS(prop) && !compressor.option("screw_ie8")) { return make_node(AST_Sub, self, { @@ -3114,4 +3168,12 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_VarDef, function(self, compressor){ + var defines = compressor.option("global_defs"); + if (defines && HOP(defines, self.name.name)) { + compressor.warn('global_defs ' + self.name.name + ' redefined [{file}:{line},{col}]', self.start); + } + return self; + }); + })(); diff --git a/lib/scope.js b/lib/scope.js index 6ad12616..b0d92d7d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -206,14 +206,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.scope.uses_arguments = true; } if (!sym) { - if (globals.has(name)) { - sym = globals.get(name); - } else { - sym = new SymbolDef(self, globals.size(), node); - sym.undeclared = true; - sym.global = true; - globals.set(name, sym); - } + sym = self.def_global(node); } node.thedef = sym; node.reference(options); @@ -227,6 +220,19 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } }); +AST_Toplevel.DEFMETHOD("def_global", function(node){ + var globals = this.globals, name = node.name; + if (globals.has(name)) { + return globals.get(name); + } else { + var g = new SymbolDef(this, globals.size(), node); + g.undeclared = true; + g.global = true; + globals.set(name, g); + return g; + } +}); + 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) diff --git a/test/compress/global_defs.js b/test/compress/global_defs.js new file mode 100644 index 00000000..a69d031e --- /dev/null +++ b/test/compress/global_defs.js @@ -0,0 +1,147 @@ +must_replace: { + options = { + global_defs: { + D: "foo bar", + } + } + input: { + console.log(D); + } + expect: { + console.log("foo bar"); + } +} + +keyword: { + options = { + 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) { + // CONFIG not global - do not replace + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + // CONFIG not global - do not replace + 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 = { + global_defs: { + "CONFIG.DEBUG": [ 0 ], + "CONFIG.VALUE": 42, + }, + } + input: { + function f(CONFIG) { + // CONFIG not global - do not replace + return CONFIG.VALUE; + } + function g() { + var CONFIG = { VALUE: 1 }; + // CONFIG not global - do not replace + 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][0]) + console.debug("foo"); + } +} + +mixed: { + options = { + evaluate: true, + global_defs: { + "CONFIG.VALUE": 42, + "FOO.BAR": "moo", + }, + properties: true, + } + input: { + const FOO = { BAR: 0 }; + console.log(FOO.BAR); + 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); + } + expect: { + const FOO = { BAR: 0 }; + console.log("moo"); + console.log(++CONFIG.DEBUG); + console.log(++CONFIG.VALUE); + console.log(++CONFIG.VALUE); + console.log(++DEBUG[42]); + CONFIG.VALUE.FOO = "bar"; + console.log(CONFIG); + } + expect_warnings: [ + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:126,22]', + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:127,22]', + 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:129,8]', + ] +} diff --git a/test/compress/issue-208.js b/test/compress/issue-208.js index 2f103786..fb9861f6 100644 --- a/test/compress/issue-208.js +++ b/test/compress/issue-208.js @@ -27,3 +27,44 @@ do_update_rhs: { MY_DEBUG += 0; } } + +mixed: { + options = { + evaluate: true, + global_defs: { + DEBUG: 0, + ENV: 1, + FOO: 2, + } + } + input: { + const ENV = 3; + var FOO = 4; + f(ENV * 10); + --FOO; + DEBUG = 1; + DEBUG++; + DEBUG += 1; + f(DEBUG); + x = DEBUG; + } + expect: { + const ENV = 3; + var FOO = 4; + f(10); + --FOO; + DEBUG = 1; + DEBUG++; + DEBUG += 1; + f(0); + x = 0; + } + expect_warnings: [ + 'WARN: global_defs ENV redefined [test/compress/issue-208.js:41,14]', + 'WARN: global_defs FOO redefined [test/compress/issue-208.js:42,12]', + 'WARN: global_defs FOO redefined [test/compress/issue-208.js:44,10]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:45,8]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:46,8]', + 'WARN: global_defs DEBUG redefined [test/compress/issue-208.js:47,8]', + ] +} diff --git a/test/input/global_defs/nested.js b/test/input/global_defs/nested.js new file mode 100644 index 00000000..dbf57909 --- /dev/null +++ b/test/input/global_defs/nested.js @@ -0,0 +1 @@ +console.log(C.V, C.D); diff --git a/test/input/global_defs/simple.js b/test/input/global_defs/simple.js new file mode 100644 index 00000000..44d515e3 --- /dev/null +++ b/test/input/global_defs/simple.js @@ -0,0 +1 @@ +console.log(D); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index c5b571bd..64599c51 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -100,4 +100,34 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should work with --define (simple)", function (done) { + var command = uglifyjscmd + ' test/input/global_defs/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 (nested)", function (done) { + var command = uglifyjscmd + ' test/input/global_defs/nested.js --define C.D=5,C.V=3 -c'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "console.log(3,5);\n"); + done(); + }); + }); + it("Should work with --define (AST_Node)", function (done) { + var command = uglifyjscmd + ' test/input/global_defs/simple.js --define console.log=stdout.println -c'; + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "stdout.println(D);\n"); + done(); + }); + }); });