diff --git a/README.md b/README.md index 041a0709..2ab9de62 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,16 @@ The available options are: --reserved-file File containing reserved names --reserve-domprops Make (most?) DOM properties reserved for --mangle-props - --mangle-props Mangle property names + --mangle-props Mangle property names (default `0`). Set to + `true` or `1` to mangle all property names. Set + to `unquoted` or `2` to only mangle unquoted + property names. Mode `2` also enables the + `keep_quoted_props` beautifier option to + preserve the quotes around property names and + disables the `properties` compressor option to + prevent rewriting quoted properties with dot + notation. You can override these by setting + them explicitly on the command line. --mangle-regex Only mangle property names matching the regex --name-cache File to hold mangled names mappings --pure-funcs List of functions that can be safely removed if @@ -291,12 +300,19 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `unsafe` (default: false) -- apply "unsafe" transformations (discussion below) +- `unsafe_comps` (default: false) -- Reverse `<` and `<=` to `>` and `>=` to + allow improved compression. This might be unsafe when an at least one of two + operands is an object with computed values due the use of methods like `get`, + or `valueOf`. This could cause change in execution order after operands in the + comparison are switching. Compression only works if both `comparisons` and + `unsafe_comps` are both set to true. + - `conditionals` -- apply optimizations for `if`-s and conditional expressions - `comparisons` -- apply certain optimizations to binary nodes, for example: - `!(a <= b) → a > b` (only when `unsafe`), attempts to negate binary nodes, - e.g. `a = !b && !c && !d && !e → a=!(b||c||d||e)` etc. + `!(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. - `evaluate` -- attempt to evaluate constant expressions @@ -415,7 +431,7 @@ them. If you are targeting < ES6 environments, use `/** @const */ var`. #### Conditional compilation, API -You can also use conditional compilation via the programmatic API. With the difference that the +You can also use conditional compilation via the programmatic API. With the difference that the property name is `global_defs` and is a compressor property: ```js @@ -445,7 +461,7 @@ can pass additional arguments that control the code output: objects - `space-colon` (default `true`) -- insert a space after the colon signs - `ascii-only` (default `false`) -- escape Unicode characters in strings and - regexps + regexps (affects directives with non-ascii characters becoming invalid) - `inline-script` (default `false`) -- escape the slash in occurrences of `" : return ev(left, c) > ev(right, c); case ">=" : return ev(left, c) >= ev(right, c); - case "in" : return ev(left, c) in ev(right, c); - case "instanceof" : return ev(left, c) instanceof ev(right, c); } throw def; }); @@ -2580,9 +2578,11 @@ merge(Compressor.prototype, { }); self = best_of(self, negated); } - switch (self.operator) { - case "<": reverse(">"); break; - case "<=": reverse(">="); break; + if (compressor.option("unsafe_comps")) { + switch (self.operator) { + case "<": reverse(">"); break; + case "<=": reverse(">="); break; + } } } if (self.operator == "+" && self.right instanceof AST_String diff --git a/lib/output.js b/lib/output.js index 99cd63f2..a048ce1a 100644 --- a/lib/output.js +++ b/lib/output.js @@ -43,6 +43,8 @@ "use strict"; +var EXPECT_DIRECTIVE = /^$|[;{][\s\n]*$/; + function OutputStream(options) { options = defaults(options, { @@ -64,7 +66,8 @@ function OutputStream(options) { preserve_line : false, screw_ie8 : false, preamble : null, - quote_style : 0 + quote_style : 0, + keep_quoted_props: false }, true); var indentation = 0; @@ -359,7 +362,18 @@ function OutputStream(options) { force_semicolon : force_semicolon, to_ascii : to_ascii, print_name : function(name) { print(make_name(name)) }, - print_string : function(str, quote) { print(encode_string(str, quote)) }, + print_string : function(str, quote, escape_directive) { + var encoded = encode_string(str, quote); + if (escape_directive === true && encoded.indexOf("\\") === -1) { + // Insert semicolons to break directive prologue + if (!EXPECT_DIRECTIVE.test(OUTPUT)) { + force_semicolon(); + } + force_semicolon(); + } + print(encoded); + }, + encode_string : encode_string, next_indent : next_indent, with_indent : with_indent, with_block : with_block, @@ -391,10 +405,11 @@ function OutputStream(options) { }; var use_asm = false; + var in_directive = false; AST_Node.DEFMETHOD("print", function(stream, force_parens){ var self = this, generator = self._codegen, prev_use_asm = use_asm; - if (self instanceof AST_Directive && self.value == "use asm") { + if (self instanceof AST_Directive && self.value == "use asm" && stream.parent() instanceof AST_Scope) { use_asm = true; } function doit() { @@ -409,7 +424,7 @@ function OutputStream(options) { doit(); } stream.pop_node(); - if (self instanceof AST_Lambda) { + if (self instanceof AST_Scope) { use_asm = prev_use_asm; } }); @@ -683,9 +698,16 @@ function OutputStream(options) { /* -----[ statements ]----- */ - function display_body(body, is_toplevel, output) { + function display_body(body, is_toplevel, output, allow_directives) { var last = body.length - 1; + in_directive = allow_directives; body.forEach(function(stmt, i){ + if (in_directive === true && !(stmt instanceof AST_Directive || + stmt instanceof AST_EmptyStatement || + (stmt instanceof AST_SimpleStatement && stmt.body instanceof AST_String) + )) { + in_directive = false; + } if (!(stmt instanceof AST_EmptyStatement)) { output.indent(); stmt.print(output); @@ -694,7 +716,14 @@ function OutputStream(options) { if (is_toplevel) output.newline(); } } + if (in_directive === true && + stmt instanceof AST_SimpleStatement && + stmt.body instanceof AST_String + ) { + in_directive = false; + } }); + in_directive = false; }; AST_StatementWithBody.DEFMETHOD("_do_print_body", function(output){ @@ -706,7 +735,7 @@ function OutputStream(options) { output.semicolon(); }); DEFPRINT(AST_Toplevel, function(self, output){ - display_body(self.body, true, output); + display_body(self.body, true, output, true); output.print(""); }); DEFPRINT(AST_LabeledStatement, function(self, output){ @@ -718,9 +747,9 @@ function OutputStream(options) { self.body.print(output); output.semicolon(); }); - function print_bracketed(body, output) { + function print_bracketed(body, output, allow_directives) { if (body.length > 0) output.with_block(function(){ - display_body(body, false, output); + display_body(body, false, output, allow_directives); }); else output.print("{}"); }; @@ -829,7 +858,7 @@ function OutputStream(options) { }); }); output.space(); - print_bracketed(self.body, output); + print_bracketed(self.body, output, true); }); DEFPRINT(AST_Lambda, function(self, output){ self._do_print(output); @@ -1363,7 +1392,11 @@ function OutputStream(options) { && parseFloat(key) >= 0) { output.print(make_num(key)); } else if (RESERVED_WORDS(key) ? output.option("screw_ie8") : is_identifier_string(key)) { - output.print_name(key); + if (quote && output.option("keep_quoted_props")) { + output.print_string(key, quote); + } else { + output.print_name(key); + } } else { output.print_string(key, quote); } @@ -1433,10 +1466,10 @@ function OutputStream(options) { output.print(self.getValue()); }); DEFPRINT(AST_String, function(self, output){ - output.print_string(self.getValue(), self.quote); + output.print_string(self.getValue(), self.quote, in_directive); }); DEFPRINT(AST_Number, function(self, output){ - if (use_asm && self.start.raw != null) { + if (use_asm && self.start && self.start.raw != null) { output.print(self.start.raw); } else { output.print(make_num(self.getValue())); @@ -1535,7 +1568,9 @@ function OutputStream(options) { // self should be AST_New. decide if we want to show parens or not. function need_constructor_parens(self, output) { // Always print parentheses with arguments - return self.args.length > 0; + if (self.args.length > 0) return true; + + return output.option("beautify"); }; function best_of(a) { diff --git a/lib/parse.js b/lib/parse.js index 4e15f7d6..4132340a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -852,9 +852,10 @@ function parse($TEXT, options) { handle_regexp(); switch (S.token.type) { case "string": - if (S.in_directives) { - if (is_token(peek(), "punc", ";") || peek().nlb) { - S.input.add_directive(S.token.raw.substr(1, S.token.raw.length - 2)); + var dir = false; + if (S.in_directives === true) { + if ((is_token(peek(), "punc", ";") || peek().nlb) && S.token.raw.indexOf("\\") === -1) { + S.input.add_directive(S.token.value); } else { S.in_directives = false; } @@ -893,6 +894,7 @@ function parse($TEXT, options) { case "`": return simple_statement(); case ";": + S.in_directives = false; next(); return new AST_EmptyStatement(); default: @@ -937,7 +939,7 @@ function parse($TEXT, options) { case "return": if (S.in_function == 0 && !options.bare_returns) - croak("'return' outside of function"); + croak("SyntaxError: 'return' outside of function"); return new AST_Return({ value: ( is("punc", ";") ? (next(), null) @@ -954,7 +956,7 @@ function parse($TEXT, options) { case "throw": if (S.token.nlb) - croak("Illegal newline after 'throw'"); + croak("SyntaxError: Illegal newline after 'throw'"); return new AST_Throw({ value: (tmp = expression(true), semicolon(), tmp) }); @@ -972,6 +974,9 @@ function parse($TEXT, options) { return tmp = const_(), semicolon(), tmp; case "with": + if (S.input.has_directive("use strict")) { + croak("SyntaxError: Strict mode may not include a with statement"); + } return new AST_With({ expression : parenthesised(), body : statement() @@ -1000,7 +1005,7 @@ function parse($TEXT, options) { // syntactically incorrect if it contains a // LabelledStatement that is enclosed by a // LabelledStatement with the same Identifier as label. - croak("Label " + label.name + " defined twice"); + croak("SyntaxError: Label " + label.name + " defined twice"); } expect(":"); S.labels.push(label); @@ -1013,7 +1018,7 @@ function parse($TEXT, options) { label.references.forEach(function(ref){ if (ref instanceof AST_Continue) { ref = ref.label.start; - croak("Continue label `" + label.name + "` refers to non-IterationStatement.", + croak("SyntaxError: Continue label `" + label.name + "` refers to non-IterationStatement.", ref.line, ref.col, ref.pos); } }); @@ -1033,11 +1038,11 @@ function parse($TEXT, options) { if (label != null) { ldef = find_if(function(l){ return l.name == label.name }, S.labels); if (!ldef) - croak("Undefined label " + label.name); + croak("SyntaxError: Undefined label " + label.name); label.thedef = ldef; } else if (S.in_loop == 0) - croak(type.TYPE + " not inside a loop or switch"); + croak("SyntaxError: " + type.TYPE + " not inside a loop or switch"); semicolon(); var stat = new type({ label: label }); if (ldef) ldef.references.push(stat); @@ -1058,7 +1063,7 @@ function parse($TEXT, options) { if (is_in || is_of) { if ((init instanceof AST_Definitions) && init.definitions.length > 1) - croak("Only one variable declaration allowed in for..in loop"); + croak("SyntaxError: Only one variable declaration allowed in for..in loop"); next(); if (is_in) { return for_in(init); @@ -1330,7 +1335,7 @@ function parse($TEXT, options) { }); } if (!bcatch && !bfinally) - croak("Missing catch/finally blocks"); + croak("SyntaxError: Missing catch/finally blocks"); return new AST_Try({ body : body, bcatch : bcatch, @@ -1497,8 +1502,8 @@ function parse($TEXT, options) { 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); + croak("SyntaxError: Invalid getter/setter name: " + tok.value, + tok.line, tok.col, tok.pos); } ret = _make_symbol(AST_SymbolRef); break; @@ -1917,7 +1922,7 @@ function parse($TEXT, options) { function as_symbol(type, noerror) { if (!is("name")) { - if (!noerror) croak("Name expected"); + if (!noerror) croak("SyntaxError: Name expected"); return null; } if (is("name", "yield") && S.input.has_directive("use strict")) { @@ -2003,7 +2008,7 @@ function parse($TEXT, options) { function make_unary(ctor, op, expr) { if ((op == "++" || op == "--") && !is_assignable(expr)) - croak("Invalid use of " + op + " operator"); + croak("SyntaxError: Invalid use of " + op + " operator"); return new ctor({ operator: op, expression: expr }); }; @@ -2087,7 +2092,7 @@ function parse($TEXT, options) { end : prev() }); } - croak("Invalid assignment"); + croak("SyntaxError: Invalid assignment"); } if (is("arrow")) { left = new AST_SymbolFunarg({ diff --git a/lib/propmangle.js b/lib/propmangle.js index b20a3815..c8bb772e 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -71,7 +71,8 @@ function mangle_properties(ast, options) { reserved : null, cache : null, only_cache : false, - regex : null + regex : null, + ignore_quoted : false }); var reserved = options.reserved; @@ -87,6 +88,7 @@ function mangle_properties(ast, options) { } var regex = options.regex; + var ignore_quoted = options.ignore_quoted; var names_to_mangle = []; var unmangleable = []; @@ -94,7 +96,8 @@ function mangle_properties(ast, options) { // step 1: find candidates to mangle ast.walk(new TreeWalker(function(node){ if (node instanceof AST_ObjectKeyVal) { - add(node.key); + if (!(ignore_quoted && node.quote)) + add(node.key); } else if (node instanceof AST_ObjectProperty) { // setter or getter, since KeyVal is handled above @@ -107,7 +110,8 @@ function mangle_properties(ast, options) { } else if (node instanceof AST_Sub) { if (this.parent() instanceof AST_Assign) { - addStrings(node.property); + if (!ignore_quoted) + addStrings(node.property); } } else if (node instanceof AST_ConciseMethod) { @@ -118,7 +122,8 @@ function mangle_properties(ast, options) { // step 2: transform the tree, renaming properties return ast.transform(new TreeTransformer(function(node){ if (node instanceof AST_ObjectKeyVal) { - node.key = mangle(node.key); + if (!(ignore_quoted && node.quote)) + node.key = mangle(node.key); } else if (node instanceof AST_ObjectProperty) { // setter or getter @@ -128,7 +133,8 @@ function mangle_properties(ast, options) { node.property = mangle(node.property); } else if (node instanceof AST_Sub) { - node.property = mangleStrings(node.property); + if (!ignore_quoted) + node.property = mangleStrings(node.property); } else if (node instanceof AST_ConciseMethod) { if (should_mangle(node.name.name)) { diff --git a/package.json b/package.json index 748e04fb..423a3e5d 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.6.2", + "version": "2.6.3", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/asm.js b/test/compress/asm.js index c3018485..f392e78e 100644 --- a/test/compress/asm.js +++ b/test/compress/asm.js @@ -92,7 +92,7 @@ asm_mixed: { function logSum(start, end) { start = 0 | start, end = 0 | end; var sum = 0, p = 0, q = 0; - for (p = start << 3, q = end << 3; (0 | q) > (0 | p); p = p + 8 | 0) sum += +log(values[p >> 3]); + for (p = start << 3, q = end << 3; (0 | p) < (0 | q); p = p + 8 | 0) sum += +log(values[p >> 3]); return +sum; } function geometricMean(start, end) { diff --git a/test/compress/comparing.js b/test/compress/comparing.js new file mode 100644 index 00000000..c51fac31 --- /dev/null +++ b/test/compress/comparing.js @@ -0,0 +1,76 @@ +keep_comparisons: { + options = { + comparisons: true, + unsafe_comps: false + } + input: { + var obj1 = { + valueOf: function() {triggeredFirst();} + } + var obj2 = { + valueOf: function() {triggeredSecond();} + } + var result1 = obj1 <= obj2; + var result2 = obj1 < obj2; + var result3 = obj1 >= obj2; + var result4 = obj1 > obj2; + } + expect: { + var obj1 = { + valueOf: function() {triggeredFirst();} + } + var obj2 = { + valueOf: function() {triggeredSecond();} + } + var result1 = obj1 <= obj2; + var result2 = obj1 < obj2; + var result3 = obj1 >= obj2; + var result4 = obj1 > obj2; + } +} + +keep_comparisons_with_unsafe_comps: { + options = { + comparisons: true, + unsafe_comps: true + } + input: { + var obj1 = { + valueOf: function() {triggeredFirst();} + } + var obj2 = { + valueOf: function() {triggeredSecond();} + } + var result1 = obj1 <= obj2; + var result2 = obj1 < obj2; + var result3 = obj1 >= obj2; + var result4 = obj1 > obj2; + } + expect: { + var obj1 = { + valueOf: function() {triggeredFirst();} + } + var obj2 = { + valueOf: function() {triggeredSecond();} + } + var result1 = obj2 >= obj1; + var result2 = obj2 > obj1; + var result3 = obj1 >= obj2; + var result4 = obj1 > obj2; + } +} + +dont_change_in_or_instanceof_expressions: { + input: { + 1 in 1; + null in null; + 1 instanceof 1; + null instanceof null; + } + expect: { + 1 in 1; + null in null; + 1 instanceof 1; + null instanceof null; + } +} \ No newline at end of file diff --git a/test/compress/properties.js b/test/compress/properties.js index 39470738..574c5142 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -72,3 +72,54 @@ evaluate_length: { a = ("foo" + b).length; } } + +mangle_properties: { + mangle_props = { + ignore_quoted: false + }; + input: { + a["foo"] = "bar"; + a.color = "red"; + x = {"bar": 10}; + } + expect: { + a["a"] = "bar"; + a.b = "red"; + x = {c: 10}; + } +} + +mangle_unquoted_properties: { + mangle_props = { + ignore_quoted: true + } + beautify = { + beautify: false, + quote_style: 3, + keep_quoted_props: true, + } + input: { + function f1() { + a["foo"] = "bar"; + a.color = "red"; + x = {"bar": 10}; + } + function f2() { + a.foo = "bar"; + a['color'] = "red"; + x = {bar: 10}; + } + } + expect: { + function f1() { + a["foo"] = "bar"; + a.a = "red"; + x = {"bar": 10}; + } + function f2() { + a.b = "bar"; + a['color'] = "red"; + x = {c: 10}; + } + } +} diff --git a/test/mocha/directives.js b/test/mocha/directives.js index 4433e429..82594758 100644 --- a/test/mocha/directives.js +++ b/test/mocha/directives.js @@ -58,4 +58,313 @@ describe("Directives", function() { assert.strictEqual(tokenizer.has_directive("use asm"), false); assert.strictEqual(tokenizer.has_directive("use thing"), false); }); -}); \ No newline at end of file + + it("Should know which strings are directive and which ones are not", function() { + var test_directive = function(tokenizer, test) { + test.directives.map(function(directive) { + assert.strictEqual(tokenizer.has_directive(directive), true, directive + " in " + test.input); + }); + test.non_directives.map(function(fake_directive) { + assert.strictEqual(tokenizer.has_directive(fake_directive), false, fake_directive + " in " + test.input); + }); + } + + var tests = [ + { + input: '"use strict"\n', + directives: ["use strict"], + non_directives: ["use asm"] + }, + { + input: '"use\\\nstrict";', + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + { + input: '"use strict"\n"use asm"\n"use bar"\n', + directives: ["use strict", "use asm", "use bar"], + non_directives: ["use foo", "use\\x20strict"] + }, + { + input: '"use \\\nstrict";"use strict";', + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + { + input: '"\\76";', + directives: [], + non_directives: [">", "\\76"] + }, + { + input: '"use strict"', // no ; or newline + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + { + input: ';"use strict"', + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + // Duplicate above code but put it in a function + { + input: 'function foo() {"use strict"\n', + directives: ["use strict"], + non_directives: ["use asm"] + }, + { + input: 'function foo() {"use\\\nstrict";', + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + { + input: 'function foo() {"use strict"\n"use asm"\n"use bar"\n', + directives: ["use strict", "use asm", "use bar"], + non_directives: ["use foo", "use\\x20strict"] + }, + { + input: 'function foo() {"use \\\nstrict";"use strict";', + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + { + input: 'var foo = function() {"\\76";', + directives: [], + non_directives: [">", "\\76"] + }, + { + input: 'var foo = function() {"use strict"', // no ; or newline + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + { + input: 'var foo = function() {;"use strict"', + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + // Special cases + { + input: '"1";"2";"3";"4";;"5"', + directives: ["1", "2", "3", "4"], + non_directives: ["5", "6", "use strict", "use asm"] + }, + { + input: 'if(1){"use strict";', + directives: [], + non_directives: ["use strict", "use\nstrict", "use \nstrict", "use asm"] + }, + { + input: '"use strict";try{"use asm";', + directives: ["use strict"], + non_directives: ["use\nstrict", "use \nstrict", "use asm"] + } + ]; + + for (var i = 0; i < tests.length; i++) { + // Fail parser deliberately to get state at failure + var tokenizer = uglify.tokenizer(tests[i].input + "]", "foo.js"); + + try { + var parser = uglify.parse(tokenizer); + throw new Error("Expected parser to fail"); + } catch (e) { + assert.strictEqual(e instanceof uglify.JS_Parse_Error, true); + assert.strictEqual(e.message, "SyntaxError: Unexpected token: punc (])"); + } + + test_directive(tokenizer, tests[i]); + } + }); + + it("Should test EXPECT_DIRECTIVE RegExp", function() { + var tests = [ + ["", true], + ["'test';", true], + ["'test';;", true], + ["'tests';\n", true], + ["'tests'", false], + ["'tests'; \n\t", true], + ["'tests';\n\n", true], + ["\n\n\"use strict\";\n\n", true] + ]; + + for (var i = 0; i < tests.length; i++) { + assert.strictEqual(uglify.EXPECT_DIRECTIVE.test(tests[i][0]), tests[i][1], tests[i][0]); + } + }); + + it("Should only print 2 semicolons spread over 2 lines in beautify mode", function() { + assert.strictEqual( + uglify.minify( + '"use strict";\'use strict\';"use strict";"use strict";;\'use strict\';console.log(\'use strict\');', + {fromString: true, output: {beautify: true, quote_style: 3}, compress: false} + ).code, + '"use strict";\n\n\'use strict\';\n\n"use strict";\n\n"use strict";\n\n;\'use strict\';\n\nconsole.log(\'use strict\');' + ); + }); + + it("Should not add double semicolons in non-scoped block statements to avoid strings becoming directives", function() { + var tests = [ + [ + '{"use\x20strict"}', + '{"use strict"}' + ], + [ + 'function foo(){"use\x20strict";}', // Valid place for directives + 'function foo(){"use strict"}' + ], + [ + 'try{"use\x20strict"}catch(e){}finally{"use\x20strict"}', + 'try{"use strict"}catch(e){}finally{"use strict"}' + ], + [ + 'if(1){"use\x20strict"} else {"use strict"}', + 'if(1){"use strict"}else{"use strict"}' + ] + ]; + + for (var i = 0; i < tests.length; i++) { + assert.strictEqual( + uglify.minify(tests[i][0], {fromString: true, quote_style: 3, compress: false, mangle: false}).code, + tests[i][1], + tests[i][0] + ); + } + }); + + it("Should add double semicolon when relying on automatic semicolon insertion", function() { + var code = uglify.minify('"use strict";"use\\x20strict";', + {fromString: true, output: {semicolons: false}, compress: false} + ).code; + assert.strictEqual(code, '"use strict";;"use strict"\n'); + }); + + it("Should check quote style of directives", function() { + var tests = [ + // 0. Prefer double quotes, unless string contains more double quotes than single quotes + [ + '"testing something";', + 0, + '"testing something";' + ], + [ + "'use strict';", + 0, + '"use strict";' + ], + [ + '"\\\'use strict\\\'";', // Not a directive as it contains quotes + 0, + ';"\'use strict\'";', + ], + [ + "'\"use strict\"';", + 0, + "'\"use strict\"';", + ], + // 1. Always use single quote + [ + '"testing something";', + 1, + "'testing something';" + ], + [ + "'use strict';", + 1, + "'use strict';" + ], + [ + '"\'use strict\'";', + 1, + // Intentionally causes directive breakage at cost of less logic, usage should be rare anyway + "'\\'use strict\\'';", + ], + [ + "'\\'use strict\\'';", // Not a valid directive + 1, + "'\\'use strict\\'';" // But no ; necessary as directive stays invalid + ], + [ + "'\"use strict\"';", + 1, + "'\"use strict\"';", + ], + // 2. Always use double quote + [ + '"testing something";', + 2, + '"testing something";' + ], + [ + "'use strict';", + 2, + '"use strict";' + ], + [ + '"\'use strict\'";', + 2, + "\"'use strict'\";", + ], + [ + "'\"use strict\"';", + 2, + // Intentionally causes directive breakage at cost of less logic, usage should be rare anyway + '"\\\"use strict\\\"";', + ], + [ + '"\\"use strict\\"";', // Not a valid directive + 2, + '"\\"use strict\\"";' // But no ; necessary as directive stays invalid + ], + // 3. Always use original + [ + '"testing something";', + 3, + '"testing something";' + ], + [ + "'use strict';", + 3, + "'use strict';", + ], + [ + '"\'use strict\'";', + 3, + '"\'use strict\'";', + ], + [ + "'\"use strict\"';", + 3, + "'\"use strict\"';", + ], + ]; + for (var i = 0; i < tests.length; i++) { + assert.strictEqual( + uglify.minify(tests[i][0], {fromString: true, output:{quote_style: tests[i][1]}, compress: false}).code, + tests[i][2], + tests[i][0] + " using mode " + tests[i][1] + ); + } + }); + it("Should be able to compress without side effects", function() { + // NOTE: the "use asm" directive disables any optimisation after being defined + var tests = [ + [ + '"use strict";"use strict";"use strict";"use foo";"use strict";;"use sloppy";doSomething("foo");', + '"use strict";"use foo";doSomething("foo");' + ], + [ + // Nothing gets optimised in the compressor because "use asm" is the first statement + '"use asm";"use\\x20strict";1+1;', + '"use asm";;"use strict";1+1;' // Yet, the parser noticed that "use strict" wasn't a directive + ] + ]; + + for (var i = 0; i < tests.length; i++) { + assert.strictEqual( + uglify.minify(tests[i][0], {fromString: true, compress: {collapse_vars: true, side_effects: true}}).code, + tests[i][1], + tests[i][0] + ); + } + }); +}); diff --git a/test/mocha/getter-setter.js b/test/mocha/getter-setter.js index 641a2026..a292fa00 100644 --- a/test/mocha/getter-setter.js +++ b/test/mocha/getter-setter.js @@ -71,7 +71,7 @@ describe("Getters and setters", function() { var fail = function(data) { return function (e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "Invalid getter/setter name: " + data.operator; + e.message === "SyntaxError: Invalid getter/setter name: " + data.operator; }; }; diff --git a/test/mocha/minify.js b/test/mocha/minify.js index bbf188c4..02d31558 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -7,5 +7,56 @@ describe("minify", function() { var result = Uglify.minify(js, {fromString: true}); assert.strictEqual(result.code, 'function foo(n){return n?3:7}'); }); -}); + describe("keep_quoted_props", function() { + it("Should preserve quotes in object literals", function() { + var js = 'var foo = {"x": 1, y: 2, \'z\': 3};'; + var result = Uglify.minify(js, { + fromString: true, output: { + keep_quoted_props: true + }}); + assert.strictEqual(result.code, 'var foo={"x":1,y:2,"z":3};'); + }); + + it("Should preserve quote styles when quote_style is 3", function() { + var js = 'var foo = {"x": 1, y: 2, \'z\': 3};'; + var result = Uglify.minify(js, { + fromString: true, output: { + keep_quoted_props: true, + quote_style: 3 + }}); + assert.strictEqual(result.code, 'var foo={"x":1,y:2,\'z\':3};'); + }); + + it("Should not preserve quotes in object literals when disabled", function() { + var js = 'var foo = {"x": 1, y: 2, \'z\': 3};'; + var result = Uglify.minify(js, { + fromString: true, output: { + keep_quoted_props: false, + quote_style: 3 + }}); + assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3};'); + }); + }); + + describe("mangleProperties", function() { + it("Shouldn't mangle quoted properties", function() { + var js = 'a["foo"] = "bar"; a.color = "red"; x = {"bar": 10};'; + var result = Uglify.minify(js, { + fromString: true, + compress: { + properties: false + }, + mangleProperties: { + ignore_quoted: true + }, + output: { + keep_quoted_props: true, + quote_style: 3 + } + }); + assert.strictEqual(result.code, + 'a["foo"]="bar",a.a="red",x={"bar":10};'); + }); + }); +}); diff --git a/test/mocha/new.js b/test/mocha/new.js index 8c0f24bc..083b9964 100644 --- a/test/mocha/new.js +++ b/test/mocha/new.js @@ -2,7 +2,49 @@ var assert = require("assert"); var uglify = require("../../"); describe("New", function() { - it("Should attach callback parens after some tokens", function() { + it("Should add trailing parentheses for new expressions with zero arguments in beautify mode", function() { + var tests = [ + "new x(1);", + "new x;", + "new new x;", + "new (function(foo){this.foo=foo;})(1);", + "new (function(foo){this.foo=foo;})();", + "new (function test(foo){this.foo=foo;})(1);", + "new (function test(foo){this.foo=foo;})();", + "new true;", + "new (0);", + "new (!0);", + "new (bar = function(foo) {this.foo=foo;})(123);", + "new (bar = function(foo) {this.foo=foo;})();" + ]; + var expected = [ + "new x(1);", + "new x();", + "new new x()();", + "new function(foo) {\n this.foo = foo;\n}(1);", + "new function(foo) {\n this.foo = foo;\n}();", + "new function test(foo) {\n this.foo = foo;\n}(1);", + "new function test(foo) {\n this.foo = foo;\n}();", + "new true();", + "new 0();", + "new (!0)();", + "new (bar = function(foo) {\n this.foo = foo;\n})(123);", + "new (bar = function(foo) {\n this.foo = foo;\n})();" + ]; + for (var i = 0; i < tests.length; i++) { + assert.strictEqual( + uglify.minify(tests[i], { + fromString: true, + output: {beautify: true}, + compress: false, + mangle: false + }).code, + expected[i] + ); + } + }); + + it("Should not add trailing parentheses for new expressions with zero arguments in non-beautify mode", function() { var tests = [ "new x(1);", "new x;", @@ -20,22 +62,22 @@ describe("New", function() { var expected = [ "new x(1);", "new x;", - "new (new x);", - "new function(foo) {\n this.foo = foo;\n}(1);", - "new function(foo) {\n this.foo = foo;\n};", - "new function test(foo) {\n this.foo = foo;\n}(1);", - "new function test(foo) {\n this.foo = foo;\n};", + "new(new x);", + "new function(foo){this.foo=foo}(1);", + "new function(foo){this.foo=foo};", + "new function test(foo){this.foo=foo}(1);", + "new function test(foo){this.foo=foo};", "new true;", "new 0;", - "new (!0);", - "new (bar = function(foo) {\n this.foo = foo;\n})(123);", - "new (bar = function(foo) {\n this.foo = foo;\n});" + "new(!0);", + "new(bar=function(foo){this.foo=foo})(123);", + "new(bar=function(foo){this.foo=foo});" ]; for (var i = 0; i < tests.length; i++) { assert.strictEqual( uglify.minify(tests[i], { fromString: true, - output: {beautify: true}, + output: {beautify: false}, compress: false, mangle: false }).code, diff --git a/test/mocha/string-literal.js b/test/mocha/string-literal.js index b363a07f..ea984213 100644 --- a/test/mocha/string-literal.js +++ b/test/mocha/string-literal.js @@ -59,13 +59,13 @@ describe("String literals", function() { it("Should not throw error outside strict mode if string contains escaped octalIntegerLiteral", function() { var tests = [ - ['"\\76";', '">";'], + ['"\\76";', ';">";'], ['"\\0"', '"\\0";'], ['"\\08"', '"\\08";'], ['"\\008"', '"\\08";'], ['"\\0008"', '"\\08";'], ['"use strict" === "use strict";\n"\\76";', '"use strict"==="use strict";">";'], - // ['"use\\\n strict";\n"\\07";', '"use\\\n strict";\n"\\u0007";'] // TODO No way to store this content literally yet as directive + ['"use\\\n strict";\n"\\07";', ';"use strict";"\07";'] ]; for (var test in tests) { diff --git a/test/mocha/with.js b/test/mocha/with.js new file mode 100644 index 00000000..2e758d1e --- /dev/null +++ b/test/mocha/with.js @@ -0,0 +1,16 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("With", function() { + it ("Should throw syntaxError when using with statement in strict mode", function() { + var code = '"use strict";\nthrow NotEarlyError;\nwith ({}) { }'; + var test = function() { + uglify.parse(code); + } + var error = function(e) { + return e instanceof uglify.JS_Parse_Error && + e.message === "SyntaxError: Strict mode may not include a with statement"; + } + assert.throws(test, error); + }); +}); \ No newline at end of file diff --git a/test/run-tests.js b/test/run-tests.js index 1c98a1ae..053d1b09 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -1,5 +1,7 @@ #! /usr/bin/env node +global.UGLIFY_DEBUG = true; + var U = require("../tools/node"); var path = require("path"); var fs = require("fs"); @@ -110,7 +112,11 @@ function run_compress_tests() { expect = test.expect_exact; } var input = as_toplevel(test.input); - var input_code = make_code(test.input, { beautify: true }); + var input_code = make_code(test.input, { + beautify: true, + quote_style: 3, + keep_quoted_props: true + }); if (test.mangle_props) { input = U.mangle_properties(input, test.mangle_props); } diff --git a/tools/exports.js b/tools/exports.js index 09acc13e..54aa23e8 100644 --- a/tools/exports.js +++ b/tools/exports.js @@ -17,3 +17,7 @@ exports["string_template"] = string_template; exports["tokenizer"] = tokenizer; exports["is_identifier"] = is_identifier; exports["SymbolDef"] = SymbolDef; + +if (typeof DEBUG !== "undefined" && DEBUG) { + exports["EXPECT_DIRECTIVE"] = EXPECT_DIRECTIVE; +} diff --git a/tools/node.js b/tools/node.js index 8cd3e4be..39976371 100644 --- a/tools/node.js +++ b/tools/node.js @@ -25,11 +25,12 @@ var FILES = exports.FILES = [ var UglifyJS = exports; -new Function("MOZ_SourceMap", "exports", FILES.map(function(file){ +new Function("MOZ_SourceMap", "exports", "DEBUG", FILES.map(function(file){ return fs.readFileSync(file, "utf8"); }).join("\n\n"))( require("source-map"), - UglifyJS + UglifyJS, + !!global.UGLIFY_DEBUG ); UglifyJS.AST_Node.warn_function = function(txt) {