diff --git a/.travis.yml b/.travis.yml index b2aef3dc..06929a34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ node_js: - "0.12" - "4" - "6" - - "7" env: - UGLIFYJS_TEST_ALL=1 matrix: diff --git a/README.md b/README.md index 10b0b468..d4a624a1 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ There's also an [in-browser online demo](http://lisperator.net/uglifyjs/#demo) (for Firefox, Chrome and probably Safari). -Note: release versions of `uglify-js` only support ECMAScript 5 (ES5). If you wish to minify +#### Note: +- release versions of `uglify-js` only support ECMAScript 5 (ES5). If you wish to minify ES2015+ (ES6+) code then please use the [harmony](#harmony) development branch. +- Node 7 has a known performance regression and runs `uglify-js` twice as slow. Install ------- diff --git a/lib/compress.js b/lib/compress.js index f6419954..c3be6b2c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -132,6 +132,11 @@ merge(Compressor.prototype, { } return node; }, + info: function() { + if (this.options.warnings == "verbose") { + AST_Node.warn.apply(AST_Node, arguments); + } + }, warn: function(text, props) { if (this.options.warnings) { // only emit unique warnings @@ -629,12 +634,24 @@ merge(Compressor.prototype, { || node instanceof AST_IterationStatement || (parent instanceof AST_If && node !== parent.condition) || (parent instanceof AST_Conditional && node !== parent.condition) + || (node instanceof AST_SymbolRef + && !are_references_in_scope(node.definition(), self)) || (parent instanceof AST_Binary && (parent.operator == "&&" || parent.operator == "||") && node === parent.right) || (parent instanceof AST_Switch && node !== parent.expression)) { return side_effects_encountered = unwind = true, node; } + function are_references_in_scope(def, scope) { + if (def.orig.length === 1 + && def.orig[0] instanceof AST_SymbolDefun) return true; + if (def.scope.get_defun_scope() !== scope) return false; + var refs = def.references; + for (var i = 0, len = refs.length; i < len; i++) { + if (refs[i].scope.get_defun_scope() !== scope) return false; + } + return true; + } }, function postorder(node) { if (unwind) return node; @@ -679,7 +696,7 @@ merge(Compressor.prototype, { // Further optimize statement after substitution. stat.reset_opt_flags(compressor); - compressor.warn("Collapsing " + (is_constant ? "constant" : "variable") + + compressor.info("Collapsing " + (is_constant ? "constant" : "variable") + " " + var_name + " [{file}:{line},{col}]", node.start); CHANGED = true; return value; @@ -1933,7 +1950,7 @@ merge(Compressor.prototype, { sym.__unused = true; if (trim) { a.pop(); - compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { + compressor[sym.unreferenced() ? "warn" : "info"]("Dropping unused function argument {name} [{file}:{line},{col}]", { name : sym.name, file : sym.start.file, line : sym.start.line, @@ -1949,7 +1966,7 @@ merge(Compressor.prototype, { if ((node instanceof AST_Defun || node instanceof AST_DefClass) && node !== self) { var keep = (node.name.definition().id in in_use_ids) || !drop_funcs && node.name.definition().global; if (!keep) { - compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", { + compressor[node.name.unreferenced() ? "warn" : "info"]("Dropping unused function {name} [{file}:{line},{col}]", { name : node.name.name, file : node.name.start.file, line : node.name.start.line, @@ -1976,7 +1993,7 @@ merge(Compressor.prototype, { compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", w); return true; } - compressor.warn("Dropping unused variable {name} [{file}:{line},{col}]", w); + compressor[def.name.unreferenced() ? "warn" : "info"]("Dropping unused variable {name} [{file}:{line},{col}]", w); return false; }); // place uninitialized names at the start diff --git a/lib/parse.js b/lib/parse.js index 6c79ae6c..4c410040 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -2467,12 +2467,52 @@ function parse($TEXT, options) { }; function is_assignable(expr) { - if (!options.strict) return true; - if (expr instanceof AST_This) return false; - if (expr instanceof AST_Super) return false; - return (expr instanceof AST_PropAccess || expr instanceof AST_Symbol); + return expr instanceof AST_PropAccess || expr instanceof AST_SymbolRef; }; + function to_destructuring(node) { + if (node instanceof AST_Object) { + node = new AST_Destructuring({ + start: node.start, + names: node.properties.map(to_destructuring), + is_array: false, + end: node.end + }); + } else if (node instanceof AST_Array) { + var names = []; + + for (var i = 0; i < node.elements.length; i++) { + // Only allow expansion as last element + if (node.elements[i] instanceof AST_Expansion) { + if (i + 1 !== node.elements.length) { + token_error(node.elements[i].start, "Spread must the be last element in destructuring array"); + } + node.elements[i].expression = to_destructuring(node.elements[i].expression); + } + + names.push(to_destructuring(node.elements[i])); + } + + node = new AST_Destructuring({ + start: node.start, + names: names, + is_array: true, + end: node.end + }); + } else if (node instanceof AST_ObjectProperty) { + node.value = to_destructuring(node.value); + } else if (node instanceof AST_Assign) { + node = new AST_DefaultAssign({ + start: node.start, + left: node.left, + operator: "=", + right: node.right, + end: node.end + }); + } + return node; + } + // In ES6, AssignmentExpression can also be an ArrowFunction var maybe_assign = function(no_in) { var start = S.token; @@ -2506,56 +2546,7 @@ function parse($TEXT, options) { var val = S.token.value; if (is("operator") && ASSIGNMENT(val)) { - if (is_assignable(left)) { - - var walk = function(node) { - var newNode; - if (node instanceof AST_Object) { - newNode = new AST_Destructuring({ - start: node.start, - names: node.properties.map(walk), - is_array: false, - end: node.end - }); - node = newNode; - } else if (node instanceof AST_Array) { - var names = []; - - for (var i = 0; i < node.elements.length; i++) { - // Only allow expansion as last element - if (node.elements[i] instanceof AST_Expansion) { - if (i + 1 !== node.elements.length) { - token_error(node.elements[i].start, "Spread must the be last element in destructuring array"); - } - node.elements[i].expression = walk(node.elements[i].expression); - } - - names.push(walk(node.elements[i])); - } - - newNode = new AST_Destructuring({ - start: node.start, - names: names, - is_array: true, - end: node.end - }); - node = newNode; - } else if (node instanceof AST_ObjectProperty) { - node.value = walk(node.value); - } else if (node instanceof AST_Assign) { - node = new AST_DefaultAssign({ - start: node.start, - left: node.left, - operator: "=", - right: node.right, - end: node.end - }); - } - - return node; - } - left = walk(left); - + if (is_assignable(left) || (left = to_destructuring(left)) instanceof AST_Destructuring) { next(); return new AST_Assign({ start : start, diff --git a/package.json b/package.json index ab0b87e3..f095c793 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.14", + "version": "2.8.15", "engines": { "node": ">=0.8.0" }, @@ -30,7 +30,6 @@ ], "dependencies": { "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", "yargs": "~3.10.0" }, "devDependencies": { @@ -40,13 +39,15 @@ "estraverse": "~1.5.1", "mocha": "~2.3.4" }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + }, "browserify": { "transform": [ "uglify-to-browserify" ] }, "scripts": { - "shrinkwrap": "rm ./npm-shrinkwrap.json; rm -rf ./node_modules; npm i && npm shrinkwrap && npm outdated", "test": "node test/run-tests.js" }, "keywords": ["uglify", "uglify-js", "minify", "minifier"] diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index acca9bed..dad2adf2 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1514,3 +1514,110 @@ issue_1605_2: { (new Object).p = 1; } } + +issue_1631_1: { + options = { + cascade: true, + collapse_vars: true, + hoist_funs: true, + join_vars: true, + sequences: true, + side_effects: true, + } + input: { + var pc = 0; + function f(x) { + pc = 200; + return 100; + } + function x() { + var t = f(); + pc += t; + return pc; + } + console.log(x()); + } + expect: { + function f(x) { + return pc = 200, 100; + } + function x() { + var t = f(); + return pc += t; + } + var pc = 0; + console.log(x()); + } + expect_stdout: "300" +} + +issue_1631_2: { + options = { + cascade: true, + collapse_vars: true, + hoist_funs: true, + join_vars: true, + sequences: true, + side_effects: true, + } + input: { + var a = 0, b = 1; + function f() { + a = 2; + return 4; + } + function g() { + var t = f(); + b = a + t; + return b; + } + console.log(g()); + } + expect: { + function f() { + return a = 2, 4; + } + function g() { + var t = f(); + return b = a + t; + } + var a = 0, b = 1; + console.log(g()); + } + expect_stdout: "6" +} + +issue_1631_3: { + options = { + cascade: true, + collapse_vars: true, + hoist_funs: true, + join_vars: true, + sequences: true, + side_effects: true, + } + input: { + function g() { + var a = 0, b = 1; + function f() { + a = 2; + return 4; + } + var t = f(); + b = a + t; + return b; + } + console.log(g()); + } + expect: { + function g() { + function f() { + return a = 2, 4; + } + var a = 0, b = 1, t = f(); + return b = a + t; + } + console.log(g()); + } + expect_stdout: "6" +} diff --git a/test/compress/html_comments.js b/test/compress/html_comments.js index 39973c3d..8687327f 100644 --- a/test/compress/html_comments.js +++ b/test/compress/html_comments.js @@ -47,22 +47,6 @@ html_comment_in_greater_than_or_equal: { expect_exact: "function f(a,b){return a-- >=b}"; } -html_comment_in_right_shift_assign: { - input: { - // Note: illegal javascript - function f(a, b) { return a-- >>= b; } - } - expect_exact: "function f(a,b){return a-- >>=b}"; -} - -html_comment_in_zero_fill_right_shift_assign: { - input: { - // Note: illegal javascript - function f(a, b) { return a-- >>>= b; } - } - expect_exact: "function f(a,b){return a-- >>>=b}"; -} - html_comment_in_string_literal: { input: { function f() { return "comment in"; } diff --git a/test/compress/issue-1034.js b/test/compress/issue-1034.js index b91eaced..57c584ab 100644 --- a/test/compress/issue-1034.js +++ b/test/compress/issue-1034.js @@ -39,7 +39,7 @@ non_hoisted_function_after_return_2a: { hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, if_return: true, join_vars: true, cascade: true, side_effects: true, - collapse_vars: false, passes: 2 + collapse_vars: false, passes: 2, warnings: "verbose" } input: { function foo(x) { @@ -75,7 +75,7 @@ non_hoisted_function_after_return_2a: { "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:53,12]", "WARN: Dropping unreachable code [test/compress/issue-1034.js:56,12]", "WARN: Dropping unused variable b [test/compress/issue-1034.js:51,20]", - "WARN: Dropping unused variable c [test/compress/issue-1034.js:53,16]" + "WARN: Dropping unused variable c [test/compress/issue-1034.js:53,16]", ] } @@ -114,8 +114,5 @@ non_hoisted_function_after_return_2b: { "WARN: Dropping unreachable code [test/compress/issue-1034.js:97,12]", "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:97,12]", "WARN: Dropping unreachable code [test/compress/issue-1034.js:101,12]", - "WARN: Dropping unused variable b [test/compress/issue-1034.js:95,20]", - "WARN: Dropping unused variable c [test/compress/issue-1034.js:97,16]" ] } - diff --git a/test/input/issue-1632/^{foo}[bar](baz)+$.js b/test/input/issue-1632/^{foo}[bar](baz)+$.js new file mode 100644 index 00000000..f92e3a10 --- /dev/null +++ b/test/input/issue-1632/^{foo}[bar](baz)+$.js @@ -0,0 +1 @@ +console.log(x); \ No newline at end of file diff --git a/test/mocha/glob.js b/test/mocha/glob.js index 30313656..e291efc8 100644 --- a/test/mocha/glob.js +++ b/test/mocha/glob.js @@ -1,10 +1,11 @@ var Uglify = require('../../'); var assert = require("assert"); +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){print("Foo:",2*o)}var print=console.log.bind(console);'); + assert.strictEqual(result.code, 'function foo(o){var n=2*o;print("Foo:",n)}var print=console.log.bind(console);'); }); it("minify() with an array of one input file glob.", function() { var result = Uglify.minify([ @@ -19,6 +20,39 @@ describe("minify() with input file globs", function() { ], { compress: { toplevel: true } }); - assert.strictEqual(result.code, 'var print=console.log.bind(console);print("qux",function(n){return 3*n}(3),function(n){return n/2}(12)),function(n){print("Foo:",2*n)}(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){var o=2*n;print("Foo:",o)}(11);'); + }); + it("should throw with non-matching glob string", function() { + var glob = "test/input/issue-1242/blah.*"; + assert.strictEqual(Uglify.simple_glob(glob).length, 1); + assert.strictEqual(Uglify.simple_glob(glob)[0], glob); + assert.throws(function() { + Uglify.minify(glob); + }, "should throw file not found"); + }); + it('"?" in glob string should not match "/"', function() { + var glob = "test/input?issue-1242/foo.*"; + assert.strictEqual(Uglify.simple_glob(glob).length, 1); + assert.strictEqual(Uglify.simple_glob(glob)[0], glob); + assert.throws(function() { + Uglify.minify(glob); + }, "should throw file not found"); + }); + it("should handle special characters in glob string", function() { + var result = Uglify.minify("test/input/issue-1632/^{*}[???](*)+$.??"); + assert.strictEqual(result.code, "console.log(x);"); + }); + it("should handle array of glob strings - matching and otherwise", function() { + var dir = "test/input/issue-1242"; + var matches = Uglify.simple_glob([ + path.join(dir, "b*.es5"), + path.join(dir, "z*.es5"), + path.join(dir, "*.js"), + ]); + assert.strictEqual(matches.length, 4); + assert.strictEqual(matches[0], path.join(dir, "bar.es5")); + assert.strictEqual(matches[1], path.join(dir, "baz.es5")); + assert.strictEqual(matches[2], path.join(dir, "z*.es5")); + assert.strictEqual(matches[3], path.join(dir, "qux.js")); }); }); diff --git a/test/mozilla-ast.js b/test/mozilla-ast.js index b5c6c6ed..e4c84df8 100644 --- a/test/mozilla-ast.js +++ b/test/mozilla-ast.js @@ -5,7 +5,7 @@ var UglifyJS = require(".."), escodegen = require("escodegen"), esfuzz = require("esfuzz"), estraverse = require("estraverse"), - prefix = Array(20).join("\b") + " "; + prefix = "\r "; // Normalizes input AST for UglifyJS in order to get correct comparison. @@ -62,7 +62,7 @@ module.exports = function(options) { var ast1 = normalizeInput(esfuzz.generate({ maxDepth: options.maxDepth })); - + var ast2 = UglifyJS .AST_Node diff --git a/test/run-tests.js b/test/run-tests.js index a3184d72..09e70021 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -114,7 +114,7 @@ function run_compress_tests() { U.AST_Node.warn_function = function(text) { warnings_emitted.push("WARN: " + text); }; - options.warnings = true; + if (!options.warnings) options.warnings = true; } var cmp = new U.Compressor(options, true); var output_options = test.beautify || {}; diff --git a/tools/exports.js b/tools/exports.js index 54aa23e8..d83739d5 100644 --- a/tools/exports.js +++ b/tools/exports.js @@ -18,6 +18,6 @@ exports["tokenizer"] = tokenizer; exports["is_identifier"] = is_identifier; exports["SymbolDef"] = SymbolDef; -if (typeof DEBUG !== "undefined" && DEBUG) { +if (global.UGLIFY_DEBUG) { exports["EXPECT_DIRECTIVE"] = EXPECT_DIRECTIVE; } diff --git a/tools/node.js b/tools/node.js index c64b4e5c..6568a741 100644 --- a/tools/node.js +++ b/tools/node.js @@ -7,7 +7,8 @@ var path = require("path"); var fs = require("fs"); -var FILES = exports.FILES = [ +var UglifyJS = exports; +var FILES = UglifyJS.FILES = [ "../lib/utils.js", "../lib/ast.js", "../lib/parse.js", @@ -20,17 +21,14 @@ var FILES = exports.FILES = [ "../lib/propmangle.js", "./exports.js", ].map(function(file){ - return fs.realpathSync(path.join(path.dirname(__filename), file)); + return require.resolve(file); }); -var UglifyJS = exports; - -new Function("MOZ_SourceMap", "exports", "DEBUG", FILES.map(function(file){ +new Function("MOZ_SourceMap", "exports", FILES.map(function(file){ return fs.readFileSync(file, "utf8"); }).join("\n\n"))( require("source-map"), - UglifyJS, - !!global.UGLIFY_DEBUG + UglifyJS ); UglifyJS.AST_Node.warn_function = function(txt) { @@ -46,7 +44,7 @@ function read_source_map(code) { return JSON.parse(new Buffer(match[2], "base64")); } -exports.minify = function(files, options) { +UglifyJS.minify = function(files, options) { options = UglifyJS.defaults(options, { spidermonkey : false, outSourceMap : null, @@ -181,7 +179,7 @@ exports.minify = function(files, options) { }; }; -// exports.describe_ast = function() { +// UglifyJS.describe_ast = function() { // function doitem(ctor) { // var sub = {}; // ctor.SUBCLASSES.forEach(function(ctor){ @@ -195,7 +193,7 @@ exports.minify = function(files, options) { // return doitem(UglifyJS.AST_Node).sub; // } -exports.describe_ast = function() { +UglifyJS.describe_ast = function() { var out = UglifyJS.OutputStream({ beautify: true }); function doitem(ctor) { out.print("AST_" + ctor.TYPE); @@ -249,13 +247,13 @@ function readReservedFile(filename, reserved) { return reserved; } -exports.readReservedFile = readReservedFile; +UglifyJS.readReservedFile = readReservedFile; -exports.readDefaultReservedFile = function(reserved) { - return readReservedFile(path.join(__dirname, "domprops.json"), reserved); +UglifyJS.readDefaultReservedFile = function(reserved) { + return readReservedFile(require.resolve("./domprops.json"), reserved); }; -exports.readNameCache = function(filename, key) { +UglifyJS.readNameCache = function(filename, key) { var cache = null; if (filename) { try { @@ -273,7 +271,7 @@ exports.readNameCache = function(filename, key) { return cache; }; -exports.writeNameCache = function(filename, key, cache) { +UglifyJS.writeNameCache = function(filename, key, cache) { if (filename) { var data; try { @@ -294,13 +292,9 @@ exports.writeNameCache = function(filename, key, cache) { // Example: "foo/bar/*baz??.*.js" // Argument `glob` may be a string or an array of strings. // Returns an array of strings. Garbage in, garbage out. -exports.simple_glob = function simple_glob(glob) { - var results = []; +UglifyJS.simple_glob = function simple_glob(glob) { if (Array.isArray(glob)) { - glob.forEach(function(elem) { - results = results.concat(simple_glob(elem)); - }); - return results; + return [].concat.apply([], glob.map(simple_glob)); } if (glob.match(/\*|\?/)) { var dir = path.dirname(glob); @@ -308,28 +302,19 @@ exports.simple_glob = function simple_glob(glob) { var entries = fs.readdirSync(dir); } catch (ex) {} if (entries) { - var pattern = "^" + (path.basename(glob) - .replace(/\(/g, "\\(") - .replace(/\)/g, "\\)") - .replace(/\{/g, "\\{") - .replace(/\}/g, "\\}") - .replace(/\[/g, "\\[") - .replace(/\]/g, "\\]") - .replace(/\+/g, "\\+") - .replace(/\^/g, "\\^") - .replace(/\$/g, "\\$") + var pattern = "^" + path.basename(glob) + .replace(/[.+^$[\]\\(){}]/g, "\\$&") .replace(/\*/g, "[^/\\\\]*") - .replace(/\./g, "\\.") - .replace(/\?/g, ".")) + "$"; + .replace(/\?/g, "[^/\\\\]") + "$"; var mod = process.platform === "win32" ? "i" : ""; var rx = new RegExp(pattern, mod); - for (var i in entries) { - if (rx.test(entries[i])) - results.push(dir + "/" + entries[i]); - } + var results = entries.filter(function(name) { + return rx.test(name); + }).map(function(name) { + return path.join(dir, name); + }); + if (results.length) return results; } } - if (results.length === 0) - results = [ glob ]; - return results; + return [ glob ]; };