diff --git a/README.md b/README.md index 67324dbd..6dea439a 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,8 @@ separate file and include it into the build. For example you can have a ```javascript const DEBUG = false; const PRODUCTION = true; +// Alternative for environments that don't support `const` +/** @const */ var STAGING = false; // etc. ``` @@ -404,8 +406,8 @@ and build your code like this: UglifyJS will notice the constants and, since they cannot be altered, it will evaluate references to them to the value itself and drop unreachable -code as usual. The possible downside of this approach is that the build -will contain the `const` declarations. +code as usual. The build will contain the `const` declarations if you use +them. If you are targeting < ES6 environments, use `/** @const */ var`. ## Beautifier options diff --git a/lib/compress.js b/lib/compress.js index 52fbb74d..27c33521 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1311,7 +1311,10 @@ merge(Compressor.prototype, { var seq = node.to_assignments(); var p = tt.parent(); if (p instanceof AST_ForIn && p.init === node) { - if (seq == null) return node.definitions[0].name; + if (seq == null) { + var def = node.definitions[0].name; + return make_node(AST_SymbolRef, def, def); + } return seq; } if (p instanceof AST_For && p.init === node) { diff --git a/lib/output.js b/lib/output.js index c0b86b96..95748f38 100644 --- a/lib/output.js +++ b/lib/output.js @@ -444,11 +444,11 @@ function OutputStream(options) { }); } else if (c.test) { comments = comments.filter(function(comment){ - return c.test(comment.value) || comment.type == "comment5"; + return comment.type == "comment5" || c.test(comment.value); }); } else if (typeof c == "function") { comments = comments.filter(function(comment){ - return c(self, comment) || comment.type == "comment5"; + return comment.type == "comment5" || c(self, comment); }); } diff --git a/lib/parse.js b/lib/parse.js index 072e7fb4..9a9529df 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -408,7 +408,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { if (octal_len > 0) ch = String.fromCharCode(parseInt(ch, 8)); else ch = read_escaped_char(true); } - else if (ch == "\n") parse_error("Unterminated string constant"); + else if ("\r\n\u2028\u2029".indexOf(ch) >= 0) parse_error("Unterminated string constant"); else if (ch == quote) break; ret += ch; } @@ -1356,6 +1356,13 @@ function parse($TEXT, options) { break; } break; + case "operator": + if (!is_identifier_string(tok.value)) { + throw new JS_Parse_Error("Invalid getter/setter name: " + tok.value, + tok.file, tok.line, tok.col, tok.pos); + } + ret = _make_symbol(AST_SymbolRef); + break; } next(); return ret; diff --git a/lib/scope.js b/lib/scope.js index ac58ad80..6bc12339 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -100,6 +100,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var labels = new Dictionary(); var defun = null; + var last_var_had_const_pragma = false; var nesting = 0; var in_destructuring = null; var tw = new TreeWalker(function(node, descend){ @@ -176,10 +177,13 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // inside the class. (node.scope = defun.parent_scope).def_function(node); } + else if (node instanceof AST_Var) { + last_var_had_const_pragma = node.has_const_pragma(); + } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); - def.constant = node instanceof AST_SymbolConst; + def.constant = node instanceof AST_SymbolConst || last_var_had_const_pragma; def.destructuring = in_destructuring; def.init = tw.parent().value; } @@ -271,6 +275,10 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ 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" }); + var def = new SymbolDef(this, this.variables.size(), symbol); + this.variables.set(symbol.name, def); }); AST_SymbolRef.DEFMETHOD("reference", function() { @@ -388,6 +396,12 @@ AST_Symbol.DEFMETHOD("global", function(){ return this.definition().global; }); +AST_Var.DEFMETHOD("has_const_pragma", function() { + var comments_before = this.start && this.start.comments_before; + var lastComment = comments_before && comments_before[comments_before.length - 1]; + return lastComment && /@const\b/.test(lastComment.value); +}); + AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { except : [], @@ -402,6 +416,10 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ options = this._default_mangler_options(options); + + // Never mangle arguments + options.except.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 diff --git a/package.json b/package.json index 6b0d2f40..da0f835e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "acorn": "~0.6.0", "escodegen": "~1.3.3", "esfuzz": "~0.3.1", - "estraverse": "~1.5.1" + "estraverse": "~1.5.1", + "mocha": "~2.3.4" }, "browserify": { "transform": [ @@ -48,5 +49,6 @@ "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/dead-code.js b/test/compress/dead-code.js index 5009ae1e..fa4b37d6 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -87,3 +87,120 @@ dead_code_constant_boolean_should_warn_more: { var moo; } } + +dead_code_const_declaration: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + const CONST_FOO = false; + if (CONST_FOO) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + const CONST_FOO = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + /** @const */ var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation_regex: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + // @constraint this shouldn't be a constant + var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("reachable"); + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + CONST_FOO_ANN && console.log('reachable'); + } +} + +dead_code_const_annotation_complex_scope: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused_var; + /** @const */ var test = 'test'; + // @const + var CONST_FOO_ANN = false; + var unused_var_2; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + if (test === 'test') { + var beef = 'good'; + /** @const */ var meat = 'beef'; + var pork = 'bad'; + if (meat === 'pork') { + console.log('also unreachable'); + } else if (pork === 'good') { + console.log('reached, not const'); + } + } + } + expect: { + var unused_var; + var test = 'test'; + var CONST_FOO_ANN = !1; + var unused_var_2; + var moo; + function bar() {} + var beef = 'good'; + var meat = 'beef'; + var pork = 'bad'; + 'good' === pork && console.log('reached, not const'); + } +} diff --git a/test/compress/issue-12.js b/test/compress/issue-12.js index bf87d5c0..e2d8bda7 100644 --- a/test/compress/issue-12.js +++ b/test/compress/issue-12.js @@ -9,3 +9,50 @@ keep_name_of_setter: { input: { a = { set foo () {} } } expect: { a = { set foo () {} } } } + +setter_with_operator_keys: { + input: { + var tokenCodes = { + get instanceof(){ + return test0; + }, + set instanceof(value){ + test0 = value; + }, + set typeof(value){ + test1 = value; + }, + get typeof(){ + return test1; + }, + set else(value){ + test2 = value; + }, + get else(){ + return test2; + } + }; + } + expect: { + var tokenCodes = { + get instanceof(){ + return test0; + }, + set instanceof(value){ + test0 = value; + }, + set typeof(value){ + test1 = value; + }, + get typeof(){ + return test1; + }, + set else(value){ + test2 = value; + }, + get else(){ + return test2; + } + }; + } +} \ No newline at end of file diff --git a/test/compress/issue-892.js b/test/compress/issue-892.js new file mode 100644 index 00000000..b5a3e64d --- /dev/null +++ b/test/compress/issue-892.js @@ -0,0 +1,33 @@ +dont_mangle_arguments: { + mangle = { + compute_char_frequency: true + }; + options = { + sequences : true, + properties : true, + dead_code : true, + drop_debugger : true, + conditionals : true, + comparisons : true, + evaluate : true, + booleans : true, + loops : true, + unused : true, + hoist_funs : true, + keep_fargs : true, + keep_fnames : false, + hoist_vars : true, + if_return : true, + join_vars : true, + cascade : true, + side_effects : true, + negate_iife : false + }; + input: { + (function(){ + var arguments = arguments, not_arguments = 9; + console.log(not_arguments, arguments); + })(5,6,7); + } + expect_exact: "(function(){var arguments=arguments,o=9;console.log(o,arguments)})(5,6,7);" +} diff --git a/test/compress/issue-913.js b/test/compress/issue-913.js new file mode 100644 index 00000000..9d34d9d9 --- /dev/null +++ b/test/compress/issue-913.js @@ -0,0 +1,20 @@ +keep_var_for_in: { + options = { + hoist_vars: true, + unused: true + }; + input: { + (function(obj){ + var foo = 5; + for (var i in obj) + return foo; + })(); + } + expect: { + (function(obj){ + var i, foo = 5; + for (i in obj) + return foo; + })(); + } +} diff --git a/test/mocha.js b/test/mocha.js new file mode 100644 index 00000000..411f52c5 --- /dev/null +++ b/test/mocha.js @@ -0,0 +1,29 @@ +var Mocha = require('mocha'), + fs = require('fs'), + path = require('path'); + +// Instantiate a Mocha instance. +var mocha = new Mocha({}); + +var testDir = __dirname + '/mocha/'; + +// Add each .js file to the mocha instance +fs.readdirSync(testDir).filter(function(file){ + // Only keep the .js files + return file.substr(-3) === '.js'; + +}).forEach(function(file){ + mocha.addFile( + path.join(testDir, file) + ); +}); + +module.exports = function() { + mocha.run(function(failures) { + if (failures !== 0) { + process.on('exit', function () { + process.exit(failures); + }); + } + }); +}; \ No newline at end of file diff --git a/test/mocha/arguments.js b/test/mocha/arguments.js new file mode 100644 index 00000000..089826fc --- /dev/null +++ b/test/mocha/arguments.js @@ -0,0 +1,22 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("arguments", function() { + it("Should known that arguments in functions are local scoped", function() { + var ast = UglifyJS.parse("var arguments; var f = function() {arguments.length}"); + ast.figure_out_scope(); + + // Test scope of `var arguments` + assert.strictEqual(ast.find_variable("arguments").global, true); + + // Select arguments symbol in function + var symbol = ast.body[1].definitions[0].value.find_variable("arguments"); + + assert.strictEqual(symbol.global, false); + assert.strictEqual(symbol.scope, ast. // From ast + body[1]. // Select 2nd statement (equals to `var f ...`) + definitions[0]. // First definition of selected statement + value // Select function as scope + ); + }); +}); \ No newline at end of file diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js new file mode 100644 index 00000000..ea2ec2eb --- /dev/null +++ b/test/mocha/comment-filter.js @@ -0,0 +1,45 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("comment filters", function() { + it("Should be able to filter comments by passing regex", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + assert.strictEqual(ast.print_to_string({comments: /^!/}), "/*!test1*/\n//!test3\n//!test6\n//!test8\n"); + }); + + it("Should be able to filter comments by passing a function", function() { + var ast = UglifyJS.parse("/*TEST 123*/\n//An other comment\n//8 chars."); + var f = function(node, comment) { + return comment.value.length === 8; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "/*TEST 123*/\n//8 chars.\n"); + }); + + it("Should be able to get the comment and comment type when using a function", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + var f = function(node, comment) { + return comment.type == "comment1" || comment.type == "comment3"; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "//!test3\n//test4\n//test5\n//!test6\n"); + }); + + it("Should be able to filter comments by passing a boolean", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + + assert.strictEqual(ast.print_to_string({comments: true}), "/*!test1*/\n/*test2*/\n//!test3\n//test4\n//test5\n//!test6\n//test7\n//!test8\n"); + assert.strictEqual(ast.print_to_string({comments: false}), ""); + }); + + it("Should never be able to filter comment5 (shebangs)", function() { + var ast = UglifyJS.parse("#!Random comment\n//test1\n/*test2*/"); + var f = function(node, comment) { + assert.strictEqual(comment.type === "comment5", false); + + return true; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "#!Random comment\n//test1\n/*test2*/\n"); + }); +}); diff --git a/test/mocha/getter-setter.js b/test/mocha/getter-setter.js new file mode 100644 index 00000000..641a2026 --- /dev/null +++ b/test/mocha/getter-setter.js @@ -0,0 +1,89 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("Getters and setters", function() { + it("Should not accept operator symbols as getter/setter name", function() { + var illegalOperators = [ + "++", + "--", + "+", + "-", + "!", + "~", + "&", + "|", + "^", + "*", + "/", + "%", + ">>", + "<<", + ">>>", + "<", + ">", + "<=", + ">=", + "==", + "===", + "!=", + "!==", + "?", + "=", + "+=", + "-=", + "/=", + "*=", + "%=", + ">>=", + "<<=", + ">>>=", + "|=", + "^=", + "&=", + "&&", + "||" + ]; + var generator = function() { + var results = []; + + for (var i in illegalOperators) { + results.push({ + code: "var obj = { get " + illegalOperators[i] + "() { return test; }};", + operator: illegalOperators[i], + method: "get" + }); + results.push({ + code: "var obj = { set " + illegalOperators[i] + "(value) { test = value}};", + operator: illegalOperators[i], + method: "set" + }); + } + + return results; + }; + + var testCase = function(data) { + return function() { + UglifyJS.parse(data.code); + }; + }; + + var fail = function(data) { + return function (e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Invalid getter/setter name: " + data.operator; + }; + }; + + var errorMessage = function(data) { + return "Expected but didn't get a syntax error while parsing following line:\n" + data.code; + }; + + var tests = generator(); + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + assert.throws(testCase(test), fail(test), errorMessage(test)); + } + }); + +}); diff --git a/test/mocha/string-literal.js b/test/mocha/string-literal.js new file mode 100644 index 00000000..84aaad7e --- /dev/null +++ b/test/mocha/string-literal.js @@ -0,0 +1,34 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("String literals", function() { + it("Should throw syntax error if a string literal contains a newline", function() { + var inputs = [ + "'\n'", + "'\r'", + '"\r\n"', + "'\u2028'", + '"\u2029"' + ]; + + var test = function(input) { + return function() { + var ast = UglifyJS.parse(input); + }; + }; + + var error = function(e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Unterminated string constant"; + }; + + for (var input in inputs) { + assert.throws(test(inputs[input]), error); + } + }); + + it("Should not throw syntax error if a string has a line continuation", function() { + var output = UglifyJS.parse('var a = "a\\\nb";').print_to_string(); + assert.equal(output, 'var a="ab";'); + }); +}); \ No newline at end of file diff --git a/test/run-tests.js b/test/run-tests.js index c1530109..db3e35c9 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -16,6 +16,9 @@ if (failures) { process.exit(1); } +var mocha_tests = require("./mocha.js"); +mocha_tests(); + var run_sourcemaps_tests = require('./sourcemaps'); run_sourcemaps_tests(); @@ -104,9 +107,19 @@ function run_compress_tests() { } var output = input.transform(cmp); output.figure_out_scope(); + + var mangle = U.defaults(test.mangle, { + compute_char_frequency: false + }); + + if (mangle.compute_char_frequency) { + output.compute_char_frequency(test.mangle); + } + if (test.mangle) { output.mangle_names(test.mangle); } + 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", {