From 1f1fccc45df9fdc8a77d2ee3f1ae36f029b31ee9 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Apr 2017 04:00:33 +0800 Subject: [PATCH 01/21] extend `test/ufuzz.js` (#1769) New expressions: - property access - array literal - object literal Miscellaneous: - reduce execution timeout - test `toplevel` and `mangleProperties` --- test/sandbox.js | 2 +- test/ufuzz.js | 107 ++++++++++++++++++++++++++++++++++++++++-------- test/ufuzz.json | 32 +++++++++++++++ 3 files changed, 123 insertions(+), 18 deletions(-) diff --git a/test/sandbox.js b/test/sandbox.js index 504e5e25..396a6e2c 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -29,7 +29,7 @@ exports.run_code = function(code) { })); } } - }, { timeout: 30000 }); + }, { timeout: 5000 }); return stdout; } catch (ex) { return ex; diff --git a/test/ufuzz.js b/test/ufuzz.js index 7aab6715..1bb3f695 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -135,6 +135,7 @@ for (var i = 2; i < process.argv.length; ++i) { } var VALUES = [ + '""', 'true', 'false', ' /[a2][^e]+$/ ', @@ -474,25 +475,31 @@ function createExpression(recurmax, noComma, stmtDepth, canThrow) { return _createExpression(recurmax, noComma, stmtDepth, canThrow); } function _createExpression(recurmax, noComma, stmtDepth, canThrow) { - switch (rng(15)) { + switch (rng(31)) { case 0: - return createUnaryOp() + (rng(2) === 1 ? 'a' : 'b'); case 1: - return 'a' + (rng(2) == 1 ? '++' : '--'); + return createUnaryOp() + (rng(2) === 1 ? 'a' : 'b'); case 2: + case 3: + return 'a' + (rng(2) == 1 ? '++' : '--'); + case 4: + case 5: // parens needed because assignments aren't valid unless they're the left-most op(s) in an expression return '(b ' + createAssignment() + ' a)'; - case 3: - return rng(2) + ' === 1 ? a : b'; - case 4: - return createNestedBinaryExpr(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma, stmtDepth, canThrow); - case 5: - return createValue(); case 6: - return '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; case 7: - return createExpression(recurmax, noComma, stmtDepth, canThrow) + '?' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':' + createExpression(recurmax, noComma, stmtDepth, canThrow); + return rng(2) + ' === 1 ? a : b'; case 8: + case 9: + return createNestedBinaryExpr(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma, stmtDepth, canThrow); + case 10: + case 11: + return createValue(); + case 12: + return '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; + case 13: + return createExpression(recurmax, noComma, stmtDepth, canThrow) + '?' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':' + createExpression(recurmax, noComma, stmtDepth, canThrow); + case 14: var nameLenBefore = VAR_NAMES.length; var name = createVarName(MAYBE); // note: this name is only accessible from _within_ the function. and immutable at that. if (name === 'c') name = 'a'; @@ -513,9 +520,10 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { } VAR_NAMES.length = nameLenBefore; return s; - case 9: + case 15: + case 16: return createTypeofExpr(recurmax, stmtDepth, canThrow); - case 10: + case 17: // you could statically infer that this is just `Math`, regardless of the other expression // I don't think Uglify does this at this time... return ''+ @@ -523,7 +531,8 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { (rng(2) === 1 ? createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + '\n' : '') + 'return Math;\n' + '}'; - case 11: + case 18: + case 19: // more like a parser test but perhaps comment nodes mess up the analysis? // note: parens not needed for post-fix (since that's the default when ambiguous) // for prefix ops we need parens to prevent accidental syntax errors. @@ -546,15 +555,79 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { default: return '(--/* ignore */b)'; } - case 12: + case 20: + case 21: return createNestedBinaryExpr(recurmax, noComma); - case 13: + case 22: return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || a || 3).toString() "; - case 14: + case 23: return " /[abc4]/.test(((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || b || 5).toString()) "; + case 24: + return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + + ") || " + rng(10) + ").toString()[" + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "] "; + case 25: + return createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow); + case 26: + return createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow); + case 27: + return '(' + createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]) "; + case 28: + return '(' + createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]) "; + case 29: + return '(' + createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + + SAFE_KEYS[rng(SAFE_KEYS.length)] + ") "; + case 30: + return '(' + createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + + SAFE_KEYS[rng(SAFE_KEYS.length)] + ") "; } } +function createArrayLiteral(recurmax, noComma, stmtDepth, canThrow) { + recurmax--; + var arr = "["; + for (var i = rng(6); --i >= 0;) { + // in rare cases produce an array hole element + var element = rng(20) ? createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) : ""; + arr += element + ", "; + } + return arr + "]"; +} + +var SAFE_KEYS = [ + "length", + "foo", + "a", + "b", + "c", + "undefined", + "null", + "NaN", + "Infinity", + "in", + "var", +]; +var KEYS = [ + "''", + '"\t"', + '"-2"', + "0", + "1.5", + "3", +].concat(SAFE_KEYS); + +function createObjectLiteral(recurmax, noComma, stmtDepth, canThrow) { + recurmax--; + var obj = "({"; + for (var i = rng(6); --i >= 0;) { + var key = KEYS[rng(KEYS.length)]; + obj += key + ":(" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "), "; + } + return obj + "})"; +} + function createNestedBinaryExpr(recurmax, noComma) { recurmax = 3; // note that this generates 2^recurmax expression parts... make sure to cap it return _createSimpleBinaryExpr(recurmax, noComma); diff --git a/test/ufuzz.json b/test/ufuzz.json index 2d871e87..214da8cd 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -4,6 +4,21 @@ "warnings": false } }, + { + "compress": { + "toplevel": true, + "warnings": false + }, + "mangle": { + "toplevel": true + } + }, + { + "compress": { + "warnings": false + }, + "mangleProperties": {} + }, { "compress": { "warnings": false @@ -22,6 +37,23 @@ "bracketize": true } }, + { + "compress": { + "passes": 3, + "properties": false, + "toplevel": true, + "warnings": false + }, + "mangle": { + "toplevel": true + }, + "mangleProperties": { + "ignore_quoted": true + }, + "output": { + "keep_quoted_props": true + } + }, { "compress": { "keep_fargs": false, From 59a4e56bc81ce96b3ee81ad4f068cfc7d89a4790 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Apr 2017 12:31:05 +0800 Subject: [PATCH 02/21] fix mangleProperties of `undefined` & `Infinity` (#1772) `NaN` already works by the happy accident of `Number.NaN` fixes #1770 --- lib/propmangle.js | 6 ++++- test/compress/issue-1770.js | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1770.js diff --git a/lib/propmangle.js b/lib/propmangle.js index cfa035d2..c1e8c7e4 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -44,7 +44,11 @@ "use strict"; function find_builtins() { - var a = []; + // NaN will be included due to Number.NaN + var a = [ + "Infinity", + "undefined", + ]; [ Object, Array, Function, Number, String, Boolean, Error, Math, Date, RegExp diff --git a/test/compress/issue-1770.js b/test/compress/issue-1770.js new file mode 100644 index 00000000..69df8960 --- /dev/null +++ b/test/compress/issue-1770.js @@ -0,0 +1,48 @@ +mangle_props: { + mangle_props = {} + input: { + var obj = { + undefined: 1, + NaN: 2, + Infinity: 3, + "-Infinity": 4, + }; + console.log( + obj[void 0], + obj[undefined], + obj["undefined"], + obj[0/0], + obj[NaN], + obj["NaN"], + obj[1/0], + obj[Infinity], + obj["Infinity"], + obj[-1/0], + obj[-Infinity], + obj["-Infinity"] + ); + } + expect: { + var obj = { + undefined: 1, + NaN: 2, + Infinity: 3, + "-Infinity": 4, + }; + console.log( + obj[void 0], + obj[void 0], + obj["undefined"], + obj[0/0], + obj[NaN], + obj["NaN"], + obj[1/0], + obj[1/0], + obj["Infinity"], + obj[-1/0], + obj[-1/0], + obj["-Infinity"] + ); + } + expect_stdout: true +} From a4007418683f55fec52b8085bac5e4d545b70a0e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Apr 2017 18:56:11 +0800 Subject: [PATCH 03/21] workaround Node.js bugs (#1775) Wrap test code in IIFE before passing to `vm` fixes #1768 fixes #1771 --- test/sandbox.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/sandbox.js b/test/sandbox.js index 396a6e2c..ea3a60a5 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -12,7 +12,6 @@ var FUNC_TOSTRING = [ ' return "[Function: __func_" + i + "__]";', " }", "}();", - "" ].join("\n"); exports.run_code = function(code) { var stdout = ""; @@ -21,7 +20,12 @@ exports.run_code = function(code) { stdout += chunk; }; try { - new vm.Script(FUNC_TOSTRING + code).runInNewContext({ + vm.runInNewContext([ + "!function() {", + FUNC_TOSTRING, + code, + "}();", + ].join("\n"), { console: { log: function() { return console.log.apply(console, [].map.call(arguments, function(arg) { From 48b3fe99524e1e85edd30f0a326c2f1136e150e4 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 3 Apr 2017 23:17:47 +0800 Subject: [PATCH 04/21] fix `mangleProperties` on identifiers (#1776) - fix handling of "-Infinity" - add test case for "-0" reverts #1481 --- lib/propmangle.js | 2 +- test/compress/issue-1770.js | 148 +++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/lib/propmangle.js b/lib/propmangle.js index c1e8c7e4..4e43bb63 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -47,6 +47,7 @@ function find_builtins() { // NaN will be included due to Number.NaN var a = [ "Infinity", + "-Infinity", "undefined", ]; [ Object, Array, Function, Number, @@ -153,7 +154,6 @@ function mangle_properties(ast, options) { // only function declarations after this line function can_mangle(name) { - if (!is_identifier(name)) return false; if (unmangleable.indexOf(name) >= 0) return false; if (reserved.indexOf(name) >= 0) return false; if (options.only_cache) { diff --git a/test/compress/issue-1770.js b/test/compress/issue-1770.js index 69df8960..9008c6f4 100644 --- a/test/compress/issue-1770.js +++ b/test/compress/issue-1770.js @@ -6,6 +6,7 @@ mangle_props: { NaN: 2, Infinity: 3, "-Infinity": 4, + "-0": 5, }; console.log( obj[void 0], @@ -19,7 +20,10 @@ mangle_props: { obj["Infinity"], obj[-1/0], obj[-Infinity], - obj["-Infinity"] + obj["-Infinity"], + obj[-0], + obj[-""], + obj["-0"] ); } expect: { @@ -28,6 +32,7 @@ mangle_props: { NaN: 2, Infinity: 3, "-Infinity": 4, + a: 5, }; console.log( obj[void 0], @@ -41,8 +46,145 @@ mangle_props: { obj["Infinity"], obj[-1/0], obj[-1/0], - obj["-Infinity"] + obj["-Infinity"], + obj[-0], + obj[-""], + obj["a"] ); } - expect_stdout: true + expect_stdout: "1 1 1 2 2 2 3 3 3 4 4 4 undefined undefined 5" +} + +identifier: { + mangle_props = {} + input: { + var obj = { + abstract: 1, + boolean: 2, + byte: 3, + char: 4, + class: 5, + double: 6, + enum: 7, + export: 8, + extends: 9, + final: 10, + float: 11, + goto: 12, + implements: 13, + import: 14, + int: 15, + interface: 16, + let: 17, + long: 18, + native: 19, + package: 20, + private: 21, + protected: 22, + public: 23, + short: 24, + static: 25, + super: 26, + synchronized: 27, + this: 28, + throws: 29, + transient: 30, + volatile: 31, + yield: 32, + false: 33, + null: 34, + true: 35, + break: 36, + case: 37, + catch: 38, + const: 39, + continue: 40, + debugger: 41, + default: 42, + delete: 43, + do: 44, + else: 45, + finally: 46, + for: 47, + function: 48, + if: 49, + in: 50, + instanceof: 51, + new: 52, + return: 53, + switch: 54, + throw: 55, + try: 56, + typeof: 57, + var: 58, + void: 59, + while: 60, + with: 61, + }; + } + expect: { + var obj = { + a: 1, + b: 2, + c: 3, + d: 4, + e: 5, + f: 6, + g: 7, + h: 8, + i: 9, + j: 10, + k: 11, + l: 12, + m: 13, + n: 14, + o: 15, + p: 16, + q: 17, + r: 18, + s: 19, + t: 20, + u: 21, + v: 22, + w: 23, + x: 24, + y: 25, + z: 26, + A: 27, + B: 28, + C: 29, + D: 30, + F: 31, + G: 32, + H: 33, + I: 34, + J: 35, + K: 36, + L: 37, + M: 38, + N: 39, + O: 40, + P: 41, + Q: 42, + R: 43, + S: 44, + T: 45, + U: 46, + V: 47, + W: 48, + X: 49, + Y: 50, + Z: 51, + $: 52, + _: 53, + aa: 54, + ba: 55, + ca: 56, + da: 57, + ea: 58, + fa: 59, + ga: 60, + ha: 61 + }; + } } From 951770fc689c3f69679c390d31bc9f728f3e8bda Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 4 Apr 2017 03:50:19 +0800 Subject: [PATCH 05/21] exclude mangling of special property names (#1779) - `null` - `true` - `false` - numeric literals --- lib/propmangle.js | 5 +- test/compress/issue-1770.js | 125 +++++++++++++++++++++++++----------- 2 files changed, 91 insertions(+), 39 deletions(-) diff --git a/lib/propmangle.js b/lib/propmangle.js index 4e43bb63..b6222990 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -46,6 +46,9 @@ function find_builtins() { // NaN will be included due to Number.NaN var a = [ + "null", + "true", + "false", "Infinity", "-Infinity", "undefined", @@ -159,7 +162,7 @@ function mangle_properties(ast, options) { if (options.only_cache) { return cache.props.has(name); } - if (/^[0-9.]+$/.test(name)) return false; + if (/^-?[0-9]+(\.[0-9]+)?(e[+-][0-9]+)?$/.test(name)) return false; return true; } diff --git a/test/compress/issue-1770.js b/test/compress/issue-1770.js index 9008c6f4..24134f81 100644 --- a/test/compress/issue-1770.js +++ b/test/compress/issue-1770.js @@ -6,7 +6,7 @@ mangle_props: { NaN: 2, Infinity: 3, "-Infinity": 4, - "-0": 5, + null: 5, }; console.log( obj[void 0], @@ -21,9 +21,8 @@ mangle_props: { obj[-1/0], obj[-Infinity], obj["-Infinity"], - obj[-0], - obj[-""], - obj["-0"] + obj[null], + obj["null"] ); } expect: { @@ -32,7 +31,7 @@ mangle_props: { NaN: 2, Infinity: 3, "-Infinity": 4, - a: 5, + null: 5, }; console.log( obj[void 0], @@ -47,12 +46,62 @@ mangle_props: { obj[-1/0], obj[-1/0], obj["-Infinity"], - obj[-0], - obj[-""], - obj["a"] + obj[null], + obj["null"] ); } - expect_stdout: "1 1 1 2 2 2 3 3 3 4 4 4 undefined undefined 5" + expect_stdout: "1 1 1 2 2 2 3 3 3 4 4 4 5 5" +} + +numeric_literal: { + beautify = { + beautify: true, + } + mangle_props = {} + input: { + var obj = { + 0: 0, + "-0": 1, + 42: 2, + "42": 3, + 0x25: 4, + "0x25": 5, + 1E42: 6, + "1E42": 7, + "1e+42": 8, + }; + console.log(obj[-0], obj[-""], obj["-0"]); + console.log(obj[42], obj["42"]); + console.log(obj[0x25], obj["0x25"], obj[37], obj["37"]); + console.log(obj[1E42], obj["1E42"], obj["1e+42"]); + } + expect_exact: [ + 'var obj = {', + ' 0: 0,', + ' "-0": 1,', + ' 42: 2,', + ' "42": 3,', + ' 37: 4,', + ' a: 5,', + ' 1e42: 6,', + ' b: 7,', + ' "1e+42": 8', + '};', + '', + 'console.log(obj[-0], obj[-""], obj["-0"]);', + '', + 'console.log(obj[42], obj["42"]);', + '', + 'console.log(obj[37], obj["a"], obj[37], obj["37"]);', + '', + 'console.log(obj[1e42], obj["b"], obj["1e+42"]);', + ] + expect_stdout: [ + "0 0 1", + "3 3", + "4 5 4 4", + "8 7 8", + ] } identifier: { @@ -156,35 +205,35 @@ identifier: { D: 30, F: 31, G: 32, - H: 33, - I: 34, - J: 35, - K: 36, - L: 37, - M: 38, - N: 39, - O: 40, - P: 41, - Q: 42, - R: 43, - S: 44, - T: 45, - U: 46, - V: 47, - W: 48, - X: 49, - Y: 50, - Z: 51, - $: 52, - _: 53, - aa: 54, - ba: 55, - ca: 56, - da: 57, - ea: 58, - fa: 59, - ga: 60, - ha: 61 + false: 33, + null: 34, + true: 35, + H: 36, + I: 37, + J: 38, + K: 39, + L: 40, + M: 41, + N: 42, + O: 43, + P: 44, + Q: 45, + R: 46, + S: 47, + T: 48, + U: 49, + V: 50, + W: 51, + X: 52, + Y: 53, + Z: 54, + $: 55, + _: 56, + aa: 57, + ba: 58, + ca: 59, + da: 60, + ea: 61, }; } } From 4b90dc1fdb30274a7f1c2d38493fd31d6a553982 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 4 Apr 2017 16:24:16 +0800 Subject: [PATCH 06/21] remove `--mangle-props` from fuzzing (#1777) The inherently unsafe nature makes this feature unsuitable to be tested this way. fixes #1774 --- test/ufuzz.json | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/test/ufuzz.json b/test/ufuzz.json index 214da8cd..fd1084a4 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -13,12 +13,6 @@ "toplevel": true } }, - { - "compress": { - "warnings": false - }, - "mangleProperties": {} - }, { "compress": { "warnings": false @@ -37,23 +31,6 @@ "bracketize": true } }, - { - "compress": { - "passes": 3, - "properties": false, - "toplevel": true, - "warnings": false - }, - "mangle": { - "toplevel": true - }, - "mangleProperties": { - "ignore_quoted": true - }, - "output": { - "keep_quoted_props": true - } - }, { "compress": { "keep_fargs": false, From 9b6bc67c3393507d0621eb3debbe8845b0eff52d Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 4 Apr 2017 23:48:22 +0800 Subject: [PATCH 07/21] optimise `do{...}while(false)` (#1785) - better heuristics to avoid issues like #1532 - fix `TreeWalker.loopcontrol_target()` - `continue` cannot refer to `switch` blocks --- lib/ast.js | 10 +++++----- lib/compress.js | 32 ++++++++++++++++++++------------ test/compress/loops.js | 26 ++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 092a9590..ba1330f4 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1035,16 +1035,16 @@ TreeWalker.prototype = { self = p; } }, - loopcontrol_target: function(label) { + loopcontrol_target: function(node) { var stack = this.stack; - if (label) for (var i = stack.length; --i >= 0;) { + if (node.label) for (var i = stack.length; --i >= 0;) { var x = stack[i]; - if (x instanceof AST_LabeledStatement && x.label.name == label.name) { + if (x instanceof AST_LabeledStatement && x.label.name == node.label.name) return x.body; - } } else for (var i = stack.length; --i >= 0;) { var x = stack[i]; - if (x instanceof AST_Switch || x instanceof AST_IterationStatement) + if (x instanceof AST_IterationStatement + || node instanceof AST_Break && x instanceof AST_Switch) return x; } } diff --git a/lib/compress.js b/lib/compress.js index 79e7d4d2..14083fe8 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -893,7 +893,7 @@ merge(Compressor.prototype, { } var ab = aborts(stat.body); - var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab.label) : null; + var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab) : null; if (ab && ((ab instanceof AST_Return && !ab.value && in_lambda) || (ab instanceof AST_Continue && self === loop_body(lct)) || (ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct))) { @@ -915,7 +915,7 @@ merge(Compressor.prototype, { } var ab = aborts(stat.alternative); - var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab.label) : null; + var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab) : null; if (ab && ((ab instanceof AST_Return && !ab.value && in_lambda) || (ab instanceof AST_Continue && self === loop_body(lct)) || (ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct))) { @@ -964,7 +964,7 @@ merge(Compressor.prototype, { extract_declarations_from_unreachable_code(compressor, stat, a); } else { if (stat instanceof AST_LoopControl) { - var lct = compressor.loopcontrol_target(stat.label); + var lct = compressor.loopcontrol_target(stat); if ((stat instanceof AST_Break && !(lct instanceof AST_IterationStatement) && loop_body(lct) === self) || (stat instanceof AST_Continue @@ -1746,7 +1746,7 @@ merge(Compressor.prototype, { OPT(AST_LabeledStatement, function(self, compressor){ if (self.body instanceof AST_Break - && compressor.loopcontrol_target(self.body.label) === self.body) { + && compressor.loopcontrol_target(self.body) === self.body) { return make_node(AST_EmptyStatement, self); } return self.label.references.length == 0 ? self.body : self; @@ -2314,13 +2314,21 @@ merge(Compressor.prototype, { return make_node(AST_For, self, { body: self.body }); - } else if (compressor.option("dead_code") && self instanceof AST_While) { + } + if (compressor.option("dead_code") && self instanceof AST_While) { var a = []; extract_declarations_from_unreachable_code(compressor, self.body, a); return make_node(AST_BlockStatement, self, { body: a }); - } else { - cond = make_node_from_constant(cond, self.condition).transform(compressor); - self.condition = best_of_expression(cond, self.condition); + } + if (self instanceof AST_Do) { + var has_loop_control = false; + var tw = new TreeWalker(function(node) { + if (node instanceof AST_Scope || has_loop_control) return true; + if (node instanceof AST_LoopControl && tw.loopcontrol_target(node) === self) + return has_loop_control = true; + }); + self.walk(tw); + if (!has_loop_control) return self.body; } } if (self instanceof AST_While) { @@ -2346,7 +2354,7 @@ merge(Compressor.prototype, { var first = self.body instanceof AST_BlockStatement ? self.body.body[0] : self.body; if (first instanceof AST_If) { if (first.body instanceof AST_Break - && compressor.loopcontrol_target(first.body.label) === compressor.self()) { + && compressor.loopcontrol_target(first.body) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2359,7 +2367,7 @@ merge(Compressor.prototype, { drop_it(first.alternative); } else if (first.alternative instanceof AST_Break - && compressor.loopcontrol_target(first.alternative.label) === compressor.self()) { + && compressor.loopcontrol_target(first.alternative) === compressor.self()) { if (self.condition) { self.condition = make_node(AST_Binary, self.condition, { left: self.condition, @@ -2590,7 +2598,7 @@ merge(Compressor.prototype, { self.body = body; while (branch = body[body.length - 1]) { var stat = branch.body[branch.body.length - 1]; - if (stat instanceof AST_Break && compressor.loopcontrol_target(stat.label) === self) + if (stat instanceof AST_Break && compressor.loopcontrol_target(stat) === self) branch.body.pop(); if (branch.body.length || branch instanceof AST_Case && (default_branch || branch.expression.has_side_effects(compressor))) break; @@ -2609,7 +2617,7 @@ merge(Compressor.prototype, { if (has_break || node instanceof AST_Lambda || node instanceof AST_SimpleStatement) return true; - if (node instanceof AST_Break && tw.loopcontrol_target(node.label) === self) + if (node instanceof AST_Break && tw.loopcontrol_target(node) === self) has_break = true; }); self.walk(tw); diff --git a/test/compress/loops.js b/test/compress/loops.js index c8d77840..f13f5cc5 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -215,8 +215,7 @@ evaluate: { a(); for(;;) c(); - // rule disabled due to issue_1532 - do d(); while (false); + d(); } } @@ -458,3 +457,26 @@ issue_1648: { } expect_exact: "function f(){for(x();1;);}" } + +do_switch: { + options = { + evaluate: true, + loops: true, + } + input: { + do { + switch (a) { + case b: + continue; + } + } while (false); + } + expect: { + do { + switch (a) { + case b: + continue; + } + } while (false); + } +} From ff289b90a92739641dcb7fc7f6c8ecf8ee74d15f Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 5 Apr 2017 21:06:42 +0800 Subject: [PATCH 08/21] implement delayed resolution for `reduce_vars` (#1788) Although it would be nice to enforce `AST_Node` cloning during transformation, that ship has sailed a long time ago. We now get the assigned value when resolving `AST_SymbolRef` instead of `reset_opt_flags()`, which has the added advantage of improved compressor efficiency. fixes #1787 --- lib/compress.js | 50 +++++++++++++++++++++++-------------- test/compress/issue-1609.js | 7 +++--- test/compress/issue-1787.js | 19 ++++++++++++++ 3 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 test/compress/issue-1787.js diff --git a/lib/compress.js b/lib/compress.js index 14083fe8..ef7f0441 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -273,7 +273,7 @@ merge(Compressor.prototype, { var d = node.definition(); d.references.push(node); if (d.fixed === undefined || !is_safe(d) - || is_modified(node, 0, d.fixed instanceof AST_Lambda)) { + || is_modified(node, 0, node.fixed_value() instanceof AST_Lambda)) { d.fixed = false; } } @@ -283,7 +283,9 @@ merge(Compressor.prototype, { if (node instanceof AST_VarDef) { var d = node.name.definition(); if (d.fixed == null) { - d.fixed = node.value; + d.fixed = node.value && function() { + return node.value; + }; mark_as_safe(d); } else if (node.value) { d.fixed = false; @@ -314,7 +316,9 @@ merge(Compressor.prototype, { // So existing transformation rules can work on them. node.argnames.forEach(function(arg, i) { var d = arg.definition(); - d.fixed = iife.args[i] || make_node(AST_Undefined, iife); + d.fixed = function() { + return iife.args[i] || make_node(AST_Undefined, iife); + }; mark_as_safe(d); }); } @@ -409,6 +413,12 @@ merge(Compressor.prototype, { } }); + AST_SymbolRef.DEFMETHOD("fixed_value", function() { + var fixed = this.definition().fixed; + if (!fixed || fixed instanceof AST_Node) return fixed; + return fixed(); + }); + function find_variable(compressor, name) { var scope, i = 0; while (scope = compressor.parent(i++)) { @@ -1487,15 +1497,15 @@ merge(Compressor.prototype, { if (this._evaluating) throw def; this._evaluating = true; try { - var d = this.definition(); - if (compressor.option("reduce_vars") && d.fixed) { + var fixed = this.fixed_value(); + if (compressor.option("reduce_vars") && fixed) { if (compressor.option("unsafe")) { - if (!HOP(d.fixed, "_evaluated")) { - d.fixed._evaluated = ev(d.fixed, compressor); + if (!HOP(fixed, "_evaluated")) { + fixed._evaluated = ev(fixed, compressor); } - return d.fixed._evaluated; + return fixed._evaluated; } - return ev(d.fixed, compressor); + return ev(fixed, compressor); } } finally { this._evaluating = false; @@ -2689,11 +2699,12 @@ merge(Compressor.prototype, { if (compressor.option("reduce_vars") && exp instanceof AST_SymbolRef) { var def = exp.definition(); - if (def.fixed instanceof AST_Defun) { - def.fixed = make_node(AST_Function, def.fixed, def.fixed).clone(true); + var fixed = exp.fixed_value(); + if (fixed instanceof AST_Defun) { + def.fixed = fixed = make_node(AST_Function, fixed, fixed).clone(true); } - if (def.fixed instanceof AST_Function) { - exp = def.fixed; + if (fixed instanceof AST_Function) { + exp = fixed; if (compressor.option("unused") && def.references.length == 1 && !(def.scope.uses_arguments @@ -3080,7 +3091,7 @@ merge(Compressor.prototype, { && (e.operator == "*" || e.operator == "/" || e.operator == "%")) { self.expression = e.left; e.left = self; - return e.optimize(compressor); + return e; } // avoids infinite recursion of numerals if (self.operator != "-" @@ -3511,12 +3522,13 @@ merge(Compressor.prototype, { } if (compressor.option("evaluate") && compressor.option("reduce_vars")) { var d = self.definition(); - if (d.fixed) { + var fixed = self.fixed_value(); + if (fixed) { if (d.should_replace === undefined) { - var init = d.fixed.evaluate(compressor); - if (init !== d.fixed) { - init = make_node_from_constant(init, d.fixed).optimize(compressor); - init = best_of_expression(init, d.fixed); + var init = fixed.evaluate(compressor); + if (init !== fixed) { + init = make_node_from_constant(init, fixed).optimize(compressor); + init = best_of_expression(init, fixed); var value = init.print_to_string().length; var name = d.name.length; var freq = d.references.length; diff --git a/test/compress/issue-1609.js b/test/compress/issue-1609.js index 577a3ee1..da4b54a2 100644 --- a/test/compress/issue-1609.js +++ b/test/compress/issue-1609.js @@ -45,11 +45,10 @@ chained_evaluation_2: { } expect: { (function() { - var a = "long piece of string"; (function() { - var c; - c = f(a); - c.bar = a; + var c, b = "long piece of string"; + c = f(b); + c.bar = b; })(); })(); } diff --git a/test/compress/issue-1787.js b/test/compress/issue-1787.js new file mode 100644 index 00000000..43d1f1be --- /dev/null +++ b/test/compress/issue-1787.js @@ -0,0 +1,19 @@ +unary_prefix: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + console.log(function() { + var x = -(2 / 3); + return x; + }()); + } + expect: { + console.log(function() { + return -2 / 3; + }()); + } + expect_stdout: true +} From 06cdb74279d01ed9b4b625200882611482333825 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 6 Apr 2017 11:18:59 +0800 Subject: [PATCH 09/21] improve `pure_getters` (#1786) - property access to `null` & `undefined` always has side effects - utilise `reduce_vars` to determine safe property access - may-be cases treated as side effects unless `unsafe` --- lib/compress.js | 34 +++++++++++--- test/compress/collapse_vars.js | 1 + test/compress/pure_getters.js | 85 ++++++++++++++++++++++++++++++++++ test/ufuzz.json | 1 - 4 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 test/compress/pure_getters.js diff --git a/lib/compress.js b/lib/compress.js index ef7f0441..22c79b81 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1160,6 +1160,26 @@ merge(Compressor.prototype, { && !node.expression.has_side_effects(compressor); } + (function(def) { + def(AST_Node, return_false); + def(AST_Null, return_true); + def(AST_Undefined, return_true); + def(AST_UnaryPrefix, function() { + return this.operator == "void"; + }); + def(AST_PropAccess, function(compressor) { + return !compressor.option("unsafe"); + }); + def(AST_SymbolRef, function(compressor) { + if (this.is_undefined) return true; + if (compressor.option("unsafe")) return false; + var fixed = this.fixed_value(); + return !fixed || fixed.may_eq_null(compressor); + }); + })(function(node, func) { + node.DEFMETHOD("may_eq_null", func); + }); + /* -----[ boolean/negation helpers ]----- */ // methods to determine whether an expression has a boolean result type @@ -1688,7 +1708,7 @@ merge(Compressor.prototype, { || this.expression.has_side_effects(compressor); }); def(AST_SymbolRef, function(compressor){ - return this.global() && this.undeclared(); + return this.undeclared(); }); def(AST_Object, function(compressor){ return any(this.properties, compressor); @@ -1701,16 +1721,15 @@ merge(Compressor.prototype, { }); def(AST_Dot, function(compressor){ if (!compressor.option("pure_getters")) return true; - return this.expression.has_side_effects(compressor); + return this.expression.may_eq_null(compressor) + || this.expression.has_side_effects(compressor); }); def(AST_Sub, function(compressor){ if (!compressor.option("pure_getters")) return true; - return this.expression.has_side_effects(compressor) + return this.expression.may_eq_null(compressor) + || this.expression.has_side_effects(compressor) || this.property.has_side_effects(compressor); }); - def(AST_PropAccess, function(compressor){ - return !compressor.option("pure_getters"); - }); def(AST_Seq, function(compressor){ return this.car.has_side_effects(compressor) || this.cdr.has_side_effects(compressor); @@ -2275,10 +2294,12 @@ merge(Compressor.prototype, { }); def(AST_Dot, function(compressor, first_in_statement){ if (!compressor.option("pure_getters")) return this; + if (this.expression.may_eq_null(compressor)) return this; return this.expression.drop_side_effect_free(compressor, first_in_statement); }); def(AST_Sub, function(compressor, first_in_statement){ if (!compressor.option("pure_getters")) return this; + if (this.expression.may_eq_null(compressor)) return this; var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); if (!expression) return this.property.drop_side_effect_free(compressor, first_in_statement); var property = this.property.drop_side_effect_free(compressor); @@ -3509,7 +3530,6 @@ merge(Compressor.prototype, { // testing against !self.scope.uses_with first is an optimization if (compressor.option("screw_ie8") && self.undeclared() - && !isLHS(self, compressor.parent()) && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { case "undefined": diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 4107707b..2264783d 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1573,6 +1573,7 @@ var_side_effects_3: { options = { collapse_vars: true, pure_getters: true, + unsafe: true, } input: { var print = console.log.bind(console); diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js new file mode 100644 index 00000000..338f8639 --- /dev/null +++ b/test/compress/pure_getters.js @@ -0,0 +1,85 @@ +side_effects: { + options = { + pure_getters: true, + reduce_vars: false, + side_effects: true, + toplevel: true, + unsafe: false, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + (void 0).prop; + } +} + +side_effects_reduce_vars: { + options = { + pure_getters: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + unsafe: false, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + a.prop; + b.prop; + d.prop; + null.prop; + (void 0).prop; + (void 0).prop; + } +} + +side_effects_unsafe: { + options = { + pure_getters: true, + reduce_vars: false, + side_effects: true, + toplevel: true, + unsafe: true, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + d; + null.prop; + (void 0).prop; + (void 0).prop; + } +} diff --git a/test/ufuzz.json b/test/ufuzz.json index fd1084a4..4523795c 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -35,7 +35,6 @@ "compress": { "keep_fargs": false, "passes": 3, - "pure_getters": true, "warnings": false } } From e869779a988b519487da0d6e2bd8e4849cd6d9f1 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Apr 2017 00:45:51 +0800 Subject: [PATCH 10/21] enable `inline_script` by default (#1793) --- lib/output.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/output.js b/lib/output.js index d0adbddf..9ac50c08 100644 --- a/lib/output.js +++ b/lib/output.js @@ -59,7 +59,7 @@ function OutputStream(options) { comments : false, indent_level : 4, indent_start : 0, - inline_script : false, + inline_script : true, keep_quoted_props: false, max_line_len : false, preamble : null, From cc6aa3e5ac13c0da9f2481181f5b4f11275ca8c8 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Apr 2017 03:42:17 +0800 Subject: [PATCH 11/21] fix incorrect context in variable substitution (#1791) `AST_Node.optimize()` is context-aware, so don't cache its results to be used elsewhere. Also fixed a few cases of AST corruption and beef up safety of `pure_getters`. --- lib/compress.js | 80 +++++++++++++++++++++++++++--------- test/compress/reduce_vars.js | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 19 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 22c79b81..de3b4db2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -283,10 +283,16 @@ merge(Compressor.prototype, { if (node instanceof AST_VarDef) { var d = node.name.definition(); if (d.fixed == null) { - d.fixed = node.value && function() { - return node.value; - }; + if (node.value) { + d.fixed = function() { + return node.value; + }; + descend(); + } else { + d.fixed = null; + } mark_as_safe(d); + return true; } else if (node.value) { d.fixed = false; } @@ -1160,13 +1166,42 @@ merge(Compressor.prototype, { && !node.expression.has_side_effects(compressor); } + // may_eq_null() + // returns true if this node may evaluate to null or undefined (function(def) { - def(AST_Node, return_false); + def(AST_Node, return_true); def(AST_Null, return_true); def(AST_Undefined, return_true); + def(AST_Constant, return_false); + def(AST_Array, return_false); + def(AST_Object, return_false); + def(AST_Function, return_false); + def(AST_UnaryPostfix, return_false); def(AST_UnaryPrefix, function() { return this.operator == "void"; }); + def(AST_Binary, function(compressor) { + switch (this.operator) { + case "&&": + return this.left.may_eq_null(compressor); + case "||": + return this.left.may_eq_null(compressor) + && this.right.may_eq_null(compressor); + default: + return false; + } + }) + def(AST_Assign, function(compressor) { + return this.operator == "=" + && this.right.may_eq_null(compressor); + }) + def(AST_Conditional, function(compressor) { + return this.consequent.may_eq_null(compressor) + || this.alternative.may_eq_null(compressor); + }) + def(AST_Seq, function(compressor) { + return this.cdr.may_eq_null(compressor); + }); def(AST_PropAccess, function(compressor) { return !compressor.option("unsafe"); }); @@ -3055,8 +3090,9 @@ merge(Compressor.prototype, { if (this.expression instanceof AST_Seq) { var seq = this.expression; var x = seq.to_array(); - this.expression = x.pop(); - x.push(this); + var e = this.clone(); + e.expression = x.pop(); + x.push(e); seq = AST_Seq.from_array(x).transform(compressor); return seq; } @@ -3110,9 +3146,14 @@ merge(Compressor.prototype, { if (e instanceof AST_Binary && (self.operator == "+" || self.operator == "-") && (e.operator == "*" || e.operator == "/" || e.operator == "%")) { - self.expression = e.left; - e.left = self; - return e; + return make_node(AST_Binary, self, { + operator: e.operator, + left: make_node(AST_UnaryPrefix, e.left, { + operator: self.operator, + expression: e.left + }), + right: e.right + }); } // avoids infinite recursion of numerals if (self.operator != "-" @@ -3131,23 +3172,25 @@ merge(Compressor.prototype, { if (this.left instanceof AST_Seq) { var seq = this.left; var x = seq.to_array(); - this.left = x.pop(); - x.push(this); + var e = this.clone(); + e.left = x.pop(); + x.push(e); return AST_Seq.from_array(x).optimize(compressor); } if (this.right instanceof AST_Seq && !this.left.has_side_effects(compressor)) { var assign = this.operator == "=" && this.left instanceof AST_SymbolRef; - var root = this.right; + var root = this.right.clone(); var cursor, seq = root; while (assign || !seq.car.has_side_effects(compressor)) { cursor = seq; if (seq.cdr instanceof AST_Seq) { - seq = seq.cdr; + seq = seq.cdr = seq.cdr.clone(); } else break; } if (cursor) { - this.right = cursor.cdr; - cursor.cdr = this; + var e = this.clone(); + e.right = cursor.cdr; + cursor.cdr = e; return root.optimize(compressor); } } @@ -3547,9 +3590,8 @@ merge(Compressor.prototype, { if (d.should_replace === undefined) { var init = fixed.evaluate(compressor); if (init !== fixed) { - init = make_node_from_constant(init, fixed).optimize(compressor); - init = best_of_expression(init, fixed); - var value = init.print_to_string().length; + init = make_node_from_constant(init, fixed); + var value = best_of_expression(init.optimize(compressor), fixed).print_to_string().length; var name = d.name.length; var freq = d.references.length; var overhead = d.global || !freq ? 0 : (name + 2 + value) / freq; @@ -3559,7 +3601,7 @@ merge(Compressor.prototype, { } } if (d.should_replace) { - return d.should_replace.clone(true); + return best_of_expression(d.should_replace.optimize(compressor), fixed).clone(true); } } } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index cdc4ef20..fdfec99e 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1866,3 +1866,75 @@ delay_def: { } expect_stdout: true } + +booleans: { + options = { + booleans: true, + evaluate: true, + reduce_vars: true, + } + input: { + console.log(function(a) { + if (a != 0); + switch (a) { + case 0: + return "FAIL"; + case false: + return "PASS"; + } + }(false)); + } + expect: { + console.log(function(a) { + if (!1); + switch (!1) { + case 0: + return "FAIL"; + case !1: + return "PASS"; + } + }(!1)); + } + expect_stdout: "PASS" +} + +side_effects_assign: { + options = { + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + toplevel: true, + } + input: { + var a = typeof void (a && a.in == 1, 0); + console.log(a); + } + expect: { + var a = typeof void (a && a.in); + console.log(a); + } + expect_stdout: "undefined" +} + +pure_getters: { + options = { + pure_getters: true, + reduce_vars: true, + side_effects: true, + toplevel: true, + } + input: { + try { + var a = (a.b, 2); + } catch (e) {} + console.log(a); + } + expect: { + try { + var a = (a.b, 2); + } catch (e) {} + console.log(a); + } + expect_stdout: "undefined" +} From 281e882d27ace48c5c415f19292e2590dd4473dc Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Apr 2017 12:32:56 +0800 Subject: [PATCH 12/21] fix `reduce_vars` on catch variable (#1794) Improved catch handling in `figure_out_scope()` means special case treatment of IE8 is no longer valid in `reset_opt_flags()`. Also fixed recursive assignment in variable definition. --- lib/compress.js | 38 ++++++++++++++----------------- test/compress/reduce_vars.js | 44 +++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index de3b4db2..8df6e58f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -251,9 +251,7 @@ merge(Compressor.prototype, { AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan){ var reduce_vars = rescan && compressor.option("reduce_vars"); var toplevel = compressor.option("toplevel"); - var ie8 = !compressor.option("screw_ie8"); - var safe_ids = []; - push(); + var safe_ids = Object.create(null); var suppressor = new TreeWalker(function(node) { if (node instanceof AST_Symbol) { var d = node.definition(); @@ -277,7 +275,7 @@ merge(Compressor.prototype, { d.fixed = false; } } - if (ie8 && node instanceof AST_SymbolCatch) { + if (node instanceof AST_SymbolCatch) { node.definition().fixed = false; } if (node instanceof AST_VarDef) { @@ -287,11 +285,12 @@ merge(Compressor.prototype, { d.fixed = function() { return node.value; }; + mark(d, false); descend(); } else { d.fixed = null; } - mark_as_safe(d); + mark(d, true); return true; } else if (node.value) { d.fixed = false; @@ -303,11 +302,10 @@ merge(Compressor.prototype, { d.fixed = false; } else { d.fixed = node; - mark_as_safe(d); + mark(d, true); } var save_ids = safe_ids; - safe_ids = []; - push(); + safe_ids = Object.create(null); descend(); safe_ids = save_ids; return true; @@ -325,7 +323,7 @@ merge(Compressor.prototype, { d.fixed = function() { return iife.args[i] || make_node(AST_Undefined, iife); }; - mark_as_safe(d); + mark(d, true); }); } if (node instanceof AST_If || node instanceof AST_DWLoop) { @@ -373,29 +371,27 @@ merge(Compressor.prototype, { }); this.walk(tw); - function mark_as_safe(def) { - safe_ids[safe_ids.length - 1][def.id] = true; + function mark(def, safe) { + safe_ids[def.id] = safe; } function is_safe(def) { - for (var i = safe_ids.length, id = def.id; --i >= 0;) { - if (safe_ids[i][id]) { - if (def.fixed == null) { - var orig = def.orig[0]; - if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; - def.fixed = make_node(AST_Undefined, orig); - } - return true; + if (safe_ids[def.id]) { + if (def.fixed == null) { + var orig = def.orig[0]; + if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; + def.fixed = make_node(AST_Undefined, orig); } + return true; } } function push() { - safe_ids.push(Object.create(null)); + safe_ids = Object.create(safe_ids); } function pop() { - safe_ids.pop(); + safe_ids = Object.getPrototypeOf(safe_ids); } function reset_def(def) { diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index fdfec99e..842d8de4 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1917,7 +1917,7 @@ side_effects_assign: { expect_stdout: "undefined" } -pure_getters: { +pure_getters_1: { options = { pure_getters: true, reduce_vars: true, @@ -1938,3 +1938,45 @@ pure_getters: { } expect_stdout: "undefined" } + +pure_getters_2: { + options = { + pure_getters: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a; + var a = a && a.b; + } + expect: { + var a; + var a = a && a.b; + } +} + +catch_var: { + options = { + booleans: true, + evaluate: true, + reduce_vars: true, + } + input: { + try { + throw {}; + } catch (e) { + var e; + console.log(!!e); + } + } + expect: { + try { + throw {}; + } catch (e) { + var e; + console.log(!!e); + } + } + expect_stdout: "true" +} From 0f4cd73dcc5d8a936ae3630a7992dbcb44274136 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Apr 2017 13:31:58 +0800 Subject: [PATCH 13/21] introduce "strict" to `pure_getters` (#1795) --- README.md | 2 ++ lib/compress.js | 15 ++++++++------ test/compress/pure_getters.js | 39 ++++++++++++++++++++++++++++------- test/compress/reduce_vars.js | 19 +++++++++++++++-- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d57a15ce..d15f114c 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,8 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `pure_getters` -- the default is `false`. If you pass `true` for this, UglifyJS will assume that object property access (e.g. `foo.bar` or `foo["bar"]`) doesn't have any side effects. + Specify `"strict"` to treat `foo.bar` as side-effect-free only when + `foo` is certain to not throw, i.e. not `null` or `undefined`. - `pure_funcs` -- default `null`. You can pass an array of names and UglifyJS will assume that those functions do not produce side diff --git a/lib/compress.js b/lib/compress.js index 8df6e58f..c199d13f 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -71,7 +71,7 @@ function Compressor(options, false_by_default) { negate_iife : !false_by_default, passes : 1, properties : !false_by_default, - pure_getters : false, + pure_getters : !false_by_default && "strict", pure_funcs : null, reduce_vars : !false_by_default, screw_ie8 : true, @@ -1165,7 +1165,13 @@ merge(Compressor.prototype, { // may_eq_null() // returns true if this node may evaluate to null or undefined (function(def) { - def(AST_Node, return_true); + function is_strict(compressor) { + return /strict/.test(compressor.option("pure_getters")); + } + + def(AST_Node, function(compressor) { + return !is_strict(compressor); + }); def(AST_Null, return_true); def(AST_Undefined, return_true); def(AST_Constant, return_false); @@ -1198,12 +1204,9 @@ merge(Compressor.prototype, { def(AST_Seq, function(compressor) { return this.cdr.may_eq_null(compressor); }); - def(AST_PropAccess, function(compressor) { - return !compressor.option("unsafe"); - }); def(AST_SymbolRef, function(compressor) { if (this.is_undefined) return true; - if (compressor.option("unsafe")) return false; + if (!is_strict(compressor)) return false; var fixed = this.fixed_value(); return !fixed || fixed.may_eq_null(compressor); }); diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js index 338f8639..c2dcb95b 100644 --- a/test/compress/pure_getters.js +++ b/test/compress/pure_getters.js @@ -1,10 +1,9 @@ -side_effects: { +strict: { options = { - pure_getters: true, + pure_getters: "strict", reduce_vars: false, side_effects: true, toplevel: true, - unsafe: false, } input: { var a, b = null, c = {}; @@ -28,13 +27,12 @@ side_effects: { } } -side_effects_reduce_vars: { +strict_reduce_vars: { options = { - pure_getters: true, + pure_getters: "strict", reduce_vars: true, side_effects: true, toplevel: true, - unsafe: false, } input: { var a, b = null, c = {}; @@ -57,13 +55,38 @@ side_effects_reduce_vars: { } } -side_effects_unsafe: { +unsafe: { options = { pure_getters: true, reduce_vars: false, side_effects: true, toplevel: true, - unsafe: true, + } + input: { + var a, b = null, c = {}; + a.prop; + b.prop; + c.prop; + d.prop; + null.prop; + (void 0).prop; + undefined.prop; + } + expect: { + var a, b = null, c = {}; + d; + null.prop; + (void 0).prop; + (void 0).prop; + } +} + +unsafe_reduce_vars: { + options = { + pure_getters: true, + reduce_vars: true, + side_effects: true, + toplevel: true, } input: { var a, b = null, c = {}; diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 842d8de4..b6f711ad 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1919,7 +1919,7 @@ side_effects_assign: { pure_getters_1: { options = { - pure_getters: true, + pure_getters: "strict", reduce_vars: true, side_effects: true, toplevel: true, @@ -1941,7 +1941,7 @@ pure_getters_1: { pure_getters_2: { options = { - pure_getters: true, + pure_getters: "strict", reduce_vars: true, toplevel: true, unused: true, @@ -1956,6 +1956,21 @@ pure_getters_2: { } } +pure_getters_3: { + options = { + pure_getters: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var a; + var a = a && a.b; + } + expect: { + } +} + catch_var: { options = { booleans: true, From e3c9c22c757112327b83f598b124690baf13ac52 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Apr 2017 15:39:59 +0800 Subject: [PATCH 14/21] fix corner cases with `delete` (#1796) `delete Infinity` returns `false` where as `delete (1/0)` returns `true` --- lib/compress.js | 43 +++++++++++++++++++++++++++++---------- test/compress/evaluate.js | 23 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index c199d13f..b001d35c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3620,6 +3620,11 @@ merge(Compressor.prototype, { return ref; } } + var parent = compressor.parent(); + if (parent instanceof AST_UnaryPrefix + && parent.operator == "delete" + && (parent.expression instanceof AST_SymbolRef + || parent.expression.TYPE === self.TYPE)) return self; return make_node(AST_UnaryPrefix, self, { operator: "void", expression: make_node(AST_Number, self, { @@ -3629,8 +3634,16 @@ merge(Compressor.prototype, { }); OPT(AST_Infinity, function(self, compressor){ - var retain = compressor.option("keep_infinity") && !find_variable(compressor, "Infinity"); - return retain ? self : make_node(AST_Binary, self, { + var parent = compressor.parent(); + if (parent instanceof AST_UnaryPrefix + && parent.operator == "delete" + && (parent.expression instanceof AST_SymbolRef + || parent.expression.TYPE === self.TYPE)) + return self; + if (compressor.option("keep_infinity") + && !find_variable(compressor, "Infinity")) + return self; + return make_node(AST_Binary, self, { operator: "/", left: make_node(AST_Number, self, { value: 1 @@ -3642,15 +3655,23 @@ merge(Compressor.prototype, { }); OPT(AST_NaN, function(self, compressor){ - return find_variable(compressor, "NaN") ? make_node(AST_Binary, self, { - operator: "/", - left: make_node(AST_Number, self, { - value: 0 - }), - right: make_node(AST_Number, self, { - value: 0 - }) - }) : self; + var parent = compressor.parent(); + if (parent instanceof AST_UnaryPrefix + && parent.operator == "delete" + && !(parent.expression instanceof AST_SymbolRef + || parent.expression.TYPE === self.TYPE) + || find_variable(compressor, "NaN")) { + return make_node(AST_Binary, self, { + operator: "/", + left: make_node(AST_Number, self, { + value: 0 + }), + right: make_node(AST_Number, self, { + value: 0 + }) + }); + } + return self; }); var ASSIGN_OPS = [ '+', '-', '/', '*', '%', '>>', '<<', '>>>', '|', '^', '&' ]; diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index fa432c46..e660071d 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -857,3 +857,26 @@ issue_1760_2: { } expect_stdout: "Infinity" } + +delete_expr: { + options = { + evaluate: true, + } + input: { + console.log(delete undefined); + console.log(delete void 0); + console.log(delete Infinity); + console.log(delete (1 / 0)); + console.log(delete NaN); + console.log(delete (0 / 0)); + } + expect: { + console.log(delete undefined); + console.log(delete void 0); + console.log(delete Infinity); + console.log(delete (1 / 0)); + console.log(delete NaN); + console.log(delete (0 / 0)); + } + expect_stdout: true +} From c2a1bceb773aab8875e0ffabf9f6b5199462f091 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Apr 2017 17:06:01 +0800 Subject: [PATCH 15/21] fix `pure_getters` for chained property access (#1798) --- lib/compress.js | 47 +++++++++++++++++------------------ test/compress/pure_getters.js | 13 ++++++++++ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index b001d35c..03b1fefb 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1165,13 +1165,16 @@ merge(Compressor.prototype, { // may_eq_null() // returns true if this node may evaluate to null or undefined (function(def) { - function is_strict(compressor) { - return /strict/.test(compressor.option("pure_getters")); + AST_Node.DEFMETHOD("may_eq_null", function(compressor) { + var pure_getters = compressor.option("pure_getters"); + return !pure_getters || this._eq_null(pure_getters); + }); + + function is_strict(pure_getters) { + return /strict/.test(pure_getters); } - def(AST_Node, function(compressor) { - return !is_strict(compressor); - }); + def(AST_Node, is_strict); def(AST_Null, return_true); def(AST_Undefined, return_true); def(AST_Constant, return_false); @@ -1182,36 +1185,36 @@ merge(Compressor.prototype, { def(AST_UnaryPrefix, function() { return this.operator == "void"; }); - def(AST_Binary, function(compressor) { + def(AST_Binary, function(pure_getters) { switch (this.operator) { case "&&": - return this.left.may_eq_null(compressor); + return this.left._eq_null(pure_getters); case "||": - return this.left.may_eq_null(compressor) - && this.right.may_eq_null(compressor); + return this.left._eq_null(pure_getters) + && this.right._eq_null(pure_getters); default: return false; } }) - def(AST_Assign, function(compressor) { + def(AST_Assign, function(pure_getters) { return this.operator == "=" - && this.right.may_eq_null(compressor); + && this.right._eq_null(pure_getters); }) - def(AST_Conditional, function(compressor) { - return this.consequent.may_eq_null(compressor) - || this.alternative.may_eq_null(compressor); + def(AST_Conditional, function(pure_getters) { + return this.consequent._eq_null(pure_getters) + || this.alternative._eq_null(pure_getters); }) - def(AST_Seq, function(compressor) { - return this.cdr.may_eq_null(compressor); + def(AST_Seq, function(pure_getters) { + return this.cdr._eq_null(pure_getters); }); - def(AST_SymbolRef, function(compressor) { + def(AST_SymbolRef, function(pure_getters) { if (this.is_undefined) return true; - if (!is_strict(compressor)) return false; + if (!is_strict(pure_getters)) return false; var fixed = this.fixed_value(); - return !fixed || fixed.may_eq_null(compressor); + return !fixed || fixed._eq_null(pure_getters); }); })(function(node, func) { - node.DEFMETHOD("may_eq_null", func); + node.DEFMETHOD("_eq_null", func); }); /* -----[ boolean/negation helpers ]----- */ @@ -1754,12 +1757,10 @@ merge(Compressor.prototype, { return any(this.elements, compressor); }); def(AST_Dot, function(compressor){ - if (!compressor.option("pure_getters")) return true; return this.expression.may_eq_null(compressor) || this.expression.has_side_effects(compressor); }); def(AST_Sub, function(compressor){ - if (!compressor.option("pure_getters")) return true; return this.expression.may_eq_null(compressor) || this.expression.has_side_effects(compressor) || this.property.has_side_effects(compressor); @@ -2327,12 +2328,10 @@ merge(Compressor.prototype, { return values && AST_Seq.from_array(values); }); def(AST_Dot, function(compressor, first_in_statement){ - if (!compressor.option("pure_getters")) return this; if (this.expression.may_eq_null(compressor)) return this; return this.expression.drop_side_effect_free(compressor, first_in_statement); }); def(AST_Sub, function(compressor, first_in_statement){ - if (!compressor.option("pure_getters")) return this; if (this.expression.may_eq_null(compressor)) return this; var expression = this.expression.drop_side_effect_free(compressor, first_in_statement); if (!expression) return this.property.drop_side_effect_free(compressor, first_in_statement); diff --git a/test/compress/pure_getters.js b/test/compress/pure_getters.js index c2dcb95b..846b53c3 100644 --- a/test/compress/pure_getters.js +++ b/test/compress/pure_getters.js @@ -106,3 +106,16 @@ unsafe_reduce_vars: { (void 0).prop; } } + +chained: { + options = { + pure_getters: "strict", + side_effects: true, + } + input: { + a.b.c; + } + expect: { + a.b.c; + } +} From a1532eb076d506fbf87a04c0ec4f26e1929aa902 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Fri, 7 Apr 2017 18:47:30 +0800 Subject: [PATCH 16/21] extend ufuzz generator (#1783) - property access - property assignment - allow bare expression within try-block - normalise `Error` in `console.log()` - generate more unary expressions - add parenthesis to enforce precedence - adjust variable reuse/creation - add parameters to function declaration & expression - add return expression - add trivial arguments to function call --- test/sandbox.js | 2 +- test/ufuzz.js | 294 +++++++++++++++++++++++++++++------------------- test/ufuzz.json | 35 +++--- 3 files changed, 198 insertions(+), 133 deletions(-) diff --git a/test/sandbox.js b/test/sandbox.js index ea3a60a5..894349fb 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -29,7 +29,7 @@ exports.run_code = function(code) { console: { log: function() { return console.log.apply(console, [].map.call(arguments, function(arg) { - return typeof arg == "function" ? arg.toString() : arg; + return typeof arg == "function" || arg && /Error$/.test(arg.name) ? arg.toString() : arg; })); } } diff --git a/test/ufuzz.js b/test/ufuzz.js index 1bb3f695..78943ca4 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -222,15 +222,19 @@ var ASSIGNMENTS = [ '>>>=', '%=' ]; -var UNARY_OPS = [ - '--', - '++', +var UNARY_SAFE = [ + '+', + '-', '~', '!', 'void ', - 'delete ', // should be safe, even `delete foo` and `delete f()` shouldn't crash - ' - ', - ' + ' ]; + 'delete ', +]; +var UNARY_POSTFIX = [ + '++', + '--', +]; +var UNARY_PREFIX = UNARY_POSTFIX.concat(UNARY_SAFE); var NO_COMMA = true; var COMMA_OK = false; @@ -251,26 +255,26 @@ var NO_DECL = true; var DONT_STORE = true; var VAR_NAMES = [ - 'foo', - 'bar', + 'a', + 'a', + 'a', 'a', 'b', + 'b', + 'b', + 'b', 'c', // prevent redeclaring this, avoid assigning to this - 'undefined', // fun! - 'eval', // mmmm, ok, also fun! - 'NaN', // mmmm, ok, also fun! - 'Infinity', // the fun never ends! - 'arguments', // this one is just creepy - 'Math', // since Math is assumed to be a non-constructor/function it may trip certain cases + 'foo', + 'foo', + 'bar', + 'bar', + 'undefined', + 'NaN', + 'Infinity', + 'arguments', + 'Math', 'parseInt', - 'parseFloat', - 'isNaN', - 'isFinite', - 'decodeURI', - 'decodeURIComponent', - 'encodeURI', - 'encodeURIComponent', - 'Object']; +]; var INITIAL_NAMES_LEN = VAR_NAMES.length; var TYPEOF_OUTCOMES = [ @@ -307,6 +311,22 @@ function createFunctions(n, recurmax, inGlobal, noDecl, canThrow, stmtDepth) { return s; } +function createParams() { + var params = []; + for (var n = rng(4); --n >= 0;) { + params.push(createVarName(MANDATORY)); + } + return params.join(', '); +} + +function createArgs() { + var args = []; + for (var n = rng(4); --n >= 0;) { + args.push(createValue()); + } + return args.join(', '); +} + function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { if (--recurmax < 0) { return ';'; } if (!STMT_COUNT_FROM_GLOBAL) stmtDepth = 0; @@ -317,17 +337,17 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { var s = ''; if (rng(5) === 0) { // functions with functions. lower the recursion to prevent a mess. - s = 'function ' + name + '(' + createVarName(MANDATORY) + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; + s = 'function ' + name + '(' + createParams() + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; } else { // functions with statements - s = 'function ' + name + '(' + createVarName(MANDATORY) + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; + s = 'function ' + name + '(' + createParams() + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; } VAR_NAMES.length = namesLenBefore; - if (noDecl) s = '!' + s + '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; + if (noDecl) s = 'var ' + createVarName(MANDATORY) + ' = ' + s + '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ');'; // avoid "function statements" (decl inside statements) - else if (inGlobal || rng(10) > 0) s += name + '();' + else if (inGlobal || rng(10) > 0) s += 'var ' + createVarName(MANDATORY) + ' = ' + name + '(' + createArgs() + ');'; return s; @@ -399,7 +419,8 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn if (canBreak && rng(5) === 0) return 'break;'; if (canContinue && rng(5) === 0) return 'continue;'; if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; - return '/*3*/return;'; + if (rng(3) == 0) return '/*3*/return;'; + return '/*4*/return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; case 2: // must wrap in curlies to prevent orphaned `else` statement if (canThrow && rng(5) === 0) return '{ throw ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; @@ -464,42 +485,44 @@ function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotR function createExpression(recurmax, noComma, stmtDepth, canThrow) { if (--recurmax < 0) { - return '(c = 1 + c, ' + createNestedBinaryExpr(recurmax, noComma) + ')'; // note: should return a simple non-recursing expression value! + return '(c = 1 + c, ' + createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; // note: should return a simple non-recursing expression value! } // since `a` and `b` are our canaries we want them more frequently than other expressions (1/3rd chance of a canary) - var r = rng(6); - if (r < 1) return 'a++ + ' + _createExpression(recurmax, noComma, stmtDepth, canThrow); - if (r < 2) return '(--b) + ' + _createExpression(recurmax, noComma, stmtDepth, canThrow); - if (r < 3) return '(c = c + 1) + ' + _createExpression(recurmax, noComma, stmtDepth, canThrow); // c only gets incremented - - return _createExpression(recurmax, noComma, stmtDepth, canThrow); + switch (rng(6)) { + case 0: + return '(a++ + (' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + '))'; + case 1: + return '((--b) + (' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + '))'; + case 2: + return '((c = c + 1) + (' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + '))'; // c only gets incremented + default: + return '(' + _createExpression(recurmax, noComma, stmtDepth, canThrow) + ')'; + } } function _createExpression(recurmax, noComma, stmtDepth, canThrow) { - switch (rng(31)) { - case 0: - case 1: - return createUnaryOp() + (rng(2) === 1 ? 'a' : 'b'); - case 2: - case 3: - return 'a' + (rng(2) == 1 ? '++' : '--'); - case 4: - case 5: + var p = 0; + switch (rng(_createExpression.N)) { + case p++: + case p++: + return createUnaryPrefix() + (rng(2) === 1 ? 'a' : 'b'); + case p++: + case p++: + return (rng(2) === 1 ? 'a' : 'b') + createUnaryPostfix(); + case p++: + case p++: // parens needed because assignments aren't valid unless they're the left-most op(s) in an expression - return '(b ' + createAssignment() + ' a)'; - case 6: - case 7: + return 'b ' + createAssignment() + ' a'; + case p++: + case p++: return rng(2) + ' === 1 ? a : b'; - case 8: - case 9: - return createNestedBinaryExpr(recurmax, noComma) + createBinaryOp(noComma) + createExpression(recurmax, noComma, stmtDepth, canThrow); - case 10: - case 11: + case p++: + case p++: return createValue(); - case 12: - return '(' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; - case 13: + case p++: + return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow); + case p++: return createExpression(recurmax, noComma, stmtDepth, canThrow) + '?' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':' + createExpression(recurmax, noComma, stmtDepth, canThrow); - case 14: + case p++: var nameLenBefore = VAR_NAMES.length; var name = createVarName(MAYBE); // note: this name is only accessible from _within_ the function. and immutable at that. if (name === 'c') name = 'a'; @@ -520,19 +543,18 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { } VAR_NAMES.length = nameLenBefore; return s; - case 15: - case 16: + case p++: + case p++: return createTypeofExpr(recurmax, stmtDepth, canThrow); - case 17: - // you could statically infer that this is just `Math`, regardless of the other expression - // I don't think Uglify does this at this time... - return ''+ - 'new function(){ \n' + - (rng(2) === 1 ? createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + '\n' : '') + - 'return Math;\n' + - '}'; - case 18: - case 19: + case p++: + return [ + 'new function() {', + rng(2) ? '' : createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';', + 'return ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';', + '}' + ].join('\n'); + case p++: + case p++: // more like a parser test but perhaps comment nodes mess up the analysis? // note: parens not needed for post-fix (since that's the default when ambiguous) // for prefix ops we need parens to prevent accidental syntax errors. @@ -542,47 +564,56 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { case 1: return 'b/* ignore */--'; case 2: - return '(++/* ignore */a)'; + return '++/* ignore */a'; case 3: - return '(--/* ignore */b)'; + return '--/* ignore */b'; case 4: // only groups that wrap a single variable return a "Reference", so this is still valid. // may just be a parser edge case that is invisible to uglify... - return '(--(b))'; + return '--(b)'; case 5: // classic 0.3-0.1 case; 1-0.1-0.1-0.1 is not 0.7 :) return 'b + 1-0.1-0.1-0.1'; default: - return '(--/* ignore */b)'; + return '--/* ignore */b'; } - case 20: - case 21: - return createNestedBinaryExpr(recurmax, noComma); - case 22: + case p++: + case p++: + return createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow); + case p++: + case p++: + return createUnarySafePrefix() + '(' + createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + case p++: return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || a || 3).toString() "; - case 23: + case p++: return " /[abc4]/.test(((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || b || 5).toString()) "; - case 24: + case p++: return " ((" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ") || " + rng(10) + ").toString()[" + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "] "; - case 25: + case p++: return createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow); - case 26: + case p++: return createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow); - case 27: - return '(' + createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + - createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]) "; - case 28: - return '(' + createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + - createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + "]) "; - case 29: - return '(' + createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + - SAFE_KEYS[rng(SAFE_KEYS.length)] + ") "; - case 30: - return '(' + createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + - SAFE_KEYS[rng(SAFE_KEYS.length)] + ") "; + case p++: + return createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']'; + case p++: + return createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '[' + + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']'; + case p++: + return createArrayLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + getDotKey(); + case p++: + return createObjectLiteral(recurmax, COMMA_OK, stmtDepth, canThrow) + '.' + getDotKey(); + case p++: + var name = getVarName(); + return name + ' && ' + name + '[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']'; + case p++: + var name = getVarName(); + return name + ' && ' + name + '.' + getDotKey(); } + _createExpression.N = p; + return _createExpression(recurmax, noComma, stmtDepth, canThrow); } function createArrayLiteral(recurmax, noComma, stmtDepth, canThrow) { @@ -618,6 +649,10 @@ var KEYS = [ "3", ].concat(SAFE_KEYS); +function getDotKey() { + return SAFE_KEYS[rng(SAFE_KEYS.length)]; +} + function createObjectLiteral(recurmax, noComma, stmtDepth, canThrow) { recurmax--; var obj = "({"; @@ -628,36 +663,52 @@ function createObjectLiteral(recurmax, noComma, stmtDepth, canThrow) { return obj + "})"; } -function createNestedBinaryExpr(recurmax, noComma) { +function createNestedBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { recurmax = 3; // note that this generates 2^recurmax expression parts... make sure to cap it - return _createSimpleBinaryExpr(recurmax, noComma); + return _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow); } -function _createSimpleBinaryExpr(recurmax, noComma) { +function _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { + return '(' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + + createBinaryOp(noComma) + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; +} +function _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { // intentionally generate more hardcore ops if (--recurmax < 0) return createValue(); - var r = rng(30); - if (r === 0) return '(c = c + 1, ' + _createSimpleBinaryExpr(recurmax, noComma) + ')'; - var s = _createSimpleBinaryExpr(recurmax, noComma) + createBinaryOp(noComma) + _createSimpleBinaryExpr(recurmax, noComma); - if (r === 1) { - // try to get a generated name reachable from current scope. default to just `a` - var assignee = VAR_NAMES[INITIAL_NAMES_LEN + rng(VAR_NAMES.length - INITIAL_NAMES_LEN)] || 'a'; - return '( ' + assignee + createAssignment() + s + ')'; + switch (rng(30)) { + case 0: + return '(c = c + 1, ' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + case 1: + return '(' + createUnarySafePrefix() + '(' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + '))'; + case 2: + var assignee = getVarName(); + return '(' + assignee + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + case 3: + var assignee = getVarName(); + var expr = '(' + assignee + '[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + + ']' + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + return canThrow && rng(10) == 0 ? expr : '(' + assignee + ' && ' + expr + ')'; + case 4: + var assignee = getVarName(); + var expr = '(' + assignee + '.' + getDotKey() + createAssignment() + + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; + return canThrow && rng(10) == 0 ? expr : '(' + assignee + ' && ' + expr + ')'; + default: + return _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow); } - return s; } function createTypeofExpr(recurmax, stmtDepth, canThrow) { switch (rng(8)) { case 0: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' === "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 1: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' !== "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 2: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' == "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 3: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '"'; + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ' != "' + TYPEOF_OUTCOMES[rng(TYPEOF_OUTCOMES.length)] + '")'; case 4: - return 'typeof ' + createVarName(MANDATORY, DONT_STORE); + return '(typeof ' + createVarName(MANDATORY, DONT_STORE) + ')'; default: return '(typeof ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')'; } @@ -676,16 +727,31 @@ function createAssignment() { return ASSIGNMENTS[rng(ASSIGNMENTS.length)]; } -function createUnaryOp() { - return UNARY_OPS[rng(UNARY_OPS.length)]; +function createUnarySafePrefix() { + return UNARY_SAFE[rng(UNARY_SAFE.length)]; +} + +function createUnaryPrefix() { + return UNARY_PREFIX[rng(UNARY_PREFIX.length)]; +} + +function createUnaryPostfix() { + return UNARY_POSTFIX[rng(UNARY_POSTFIX.length)]; +} + +function getVarName() { + // try to get a generated name reachable from current scope. default to just `a` + return VAR_NAMES[INITIAL_NAMES_LEN + rng(VAR_NAMES.length - INITIAL_NAMES_LEN)] || 'a'; } function createVarName(maybe, dontStore) { - if (!maybe || rng(2) === 1) { - var r = rng(VAR_NAMES.length); - var suffixed = rng(5) > 0; - var name = VAR_NAMES[r] + (suffixed ? '_' + (++loops) : ''); - if (!dontStore && suffixed) VAR_NAMES.push(name); + if (!maybe || rng(2)) { + var name = VAR_NAMES[rng(VAR_NAMES.length)]; + var suffix = rng(3); + if (suffix) { + name += '_' + suffix; + if (!dontStore) VAR_NAMES.push(name); + } return name; } return ''; diff --git a/test/ufuzz.json b/test/ufuzz.json index 4523795c..c4813470 100644 --- a/test/ufuzz.json +++ b/test/ufuzz.json @@ -1,4 +1,21 @@ [ + { + "compress": false, + "mangle": false, + "output": { + "beautify": true, + "bracketize": true + } + }, + { + "compress": false + }, + { + "compress": { + "warnings": false + }, + "mangle": false + }, { "compress": { "warnings": false @@ -13,24 +30,6 @@ "toplevel": true } }, - { - "compress": { - "warnings": false - }, - "mangle": false - }, - { - "compress": false, - "mangle": true - }, - { - "compress": false, - "mangle": false, - "output": { - "beautify": true, - "bracketize": true - } - }, { "compress": { "keep_fargs": false, From cf72fe552f5a51ccfe40c32e0fb86d549e0ca848 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 8 Apr 2017 14:25:28 +0800 Subject: [PATCH 17/21] fix `delete` corner cases (#1799) - assignment - boolean - conditional - sequence --- lib/compress.js | 62 +++++++++------ test/compress/conditionals.js | 53 +++++++++++++ test/compress/drop-unused.js | 55 +++++++++++++ test/compress/evaluate.js | 79 ++++++++++++++++++- test/compress/sequences.js | 144 ++++++++++++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 24 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 03b1fefb..de0ff387 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -481,15 +481,15 @@ merge(Compressor.prototype, { // func(something) because that changes the meaning of // the func (becomes lexical instead of global). function maintain_this_binding(parent, orig, val) { - if (parent instanceof AST_Call && parent.expression === orig) { - if (val instanceof AST_PropAccess || val instanceof AST_SymbolRef && val.name === "eval") { - return make_node(AST_Seq, orig, { - car: make_node(AST_Number, orig, { - value: 0 - }), - cdr: val - }); - } + if (parent instanceof AST_UnaryPrefix && parent.operator == "delete" + || parent instanceof AST_Call && parent.expression === orig + && (val instanceof AST_PropAccess || val instanceof AST_SymbolRef && val.name == "eval")) { + return make_node(AST_Seq, orig, { + car: make_node(AST_Number, orig, { + value: 0 + }), + cdr: val + }); } return val; } @@ -3103,11 +3103,27 @@ merge(Compressor.prototype, { }); OPT(AST_UnaryPrefix, function(self, compressor){ + var e = self.expression; + if (self.operator == "delete" + && !(e instanceof AST_SymbolRef + || e instanceof AST_PropAccess + || e instanceof AST_NaN + || e instanceof AST_Infinity + || e instanceof AST_Undefined)) { + if (e instanceof AST_Seq) { + e = e.to_array(); + e.push(make_node(AST_True, self)); + return AST_Seq.from_array(e).optimize(compressor); + } + return make_node(AST_Seq, self, { + car: e, + cdr: make_node(AST_True, self) + }).optimize(compressor); + } var seq = self.lift_sequences(compressor); if (seq !== self) { return seq; } - var e = self.expression; if (compressor.option("side_effects") && self.operator == "void") { e = e.drop_side_effect_free(compressor); if (e) { @@ -3606,6 +3622,14 @@ merge(Compressor.prototype, { return self; }); + function in_delete(parent) { + return parent instanceof AST_UnaryPrefix && parent.operator == "delete"; + } + + function is_atomic(parent, self) { + return parent.expression instanceof AST_SymbolRef || parent.expression.TYPE === self.TYPE; + } + OPT(AST_Undefined, function(self, compressor){ if (compressor.option("unsafe")) { var undef = find_variable(compressor, "undefined"); @@ -3620,10 +3644,7 @@ merge(Compressor.prototype, { } } var parent = compressor.parent(); - if (parent instanceof AST_UnaryPrefix - && parent.operator == "delete" - && (parent.expression instanceof AST_SymbolRef - || parent.expression.TYPE === self.TYPE)) return self; + if (in_delete(parent) && is_atomic(parent, self)) return self; return make_node(AST_UnaryPrefix, self, { operator: "void", expression: make_node(AST_Number, self, { @@ -3634,12 +3655,10 @@ merge(Compressor.prototype, { OPT(AST_Infinity, function(self, compressor){ var parent = compressor.parent(); - if (parent instanceof AST_UnaryPrefix - && parent.operator == "delete" - && (parent.expression instanceof AST_SymbolRef - || parent.expression.TYPE === self.TYPE)) - return self; + var del = in_delete(parent); + if (del && is_atomic(parent, self)) return self; if (compressor.option("keep_infinity") + && !(del && !is_atomic(parent, self)) && !find_variable(compressor, "Infinity")) return self; return make_node(AST_Binary, self, { @@ -3655,10 +3674,7 @@ merge(Compressor.prototype, { OPT(AST_NaN, function(self, compressor){ var parent = compressor.parent(); - if (parent instanceof AST_UnaryPrefix - && parent.operator == "delete" - && !(parent.expression instanceof AST_SymbolRef - || parent.expression.TYPE === self.TYPE) + if (in_delete(parent) && !is_atomic(parent, self) || find_variable(compressor, "NaN")) { return make_node(AST_Binary, self, { operator: "/", diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index e7ea2bb2..200b487f 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -962,3 +962,56 @@ condition_symbol_matches_consequent: { } expect_stdout: "3 7 true 4" } + +delete_conditional_1: { + options = { + booleans: true, + conditionals: true, + evaluate: true, + side_effects: true, + } + input: { + console.log(delete (1 ? undefined : x)); + console.log(delete (1 ? void 0 : x)); + console.log(delete (1 ? Infinity : x)); + console.log(delete (1 ? 1 / 0 : x)); + console.log(delete (1 ? NaN : x)); + console.log(delete (1 ? 0 / 0 : x)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } + expect_stdout: true +} + +delete_conditional_2: { + options = { + booleans: true, + conditionals: true, + evaluate: true, + keep_infinity: true, + side_effects: true, + } + input: { + console.log(delete (0 ? x : undefined)); + console.log(delete (0 ? x : void 0)); + console.log(delete (0 ? x : Infinity)); + console.log(delete (0 ? x : 1 / 0)); + console.log(delete (0 ? x : NaN)); + console.log(delete (0 ? x : 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } + expect_stdout: true +} diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 28118fc4..99d9cace 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -974,3 +974,58 @@ issue_1715_4: { } expect_stdout: "1" } + +delete_assign_1: { + options = { + booleans: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a; + console.log(delete (a = undefined)); + console.log(delete (a = void 0)); + console.log(delete (a = Infinity)); + console.log(delete (a = 1 / 0)); + console.log(delete (a = NaN)); + console.log(delete (a = 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_assign_2: { + options = { + booleans: true, + keep_infinity: true, + side_effects: true, + toplevel: true, + unused: true, + } + input: { + var a; + console.log(delete (a = undefined)); + console.log(delete (a = void 0)); + console.log(delete (a = Infinity)); + console.log(delete (a = 1 / 0)); + console.log(delete (a = NaN)); + console.log(delete (a = 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index e660071d..3c16e201 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -858,8 +858,9 @@ issue_1760_2: { expect_stdout: "Infinity" } -delete_expr: { +delete_expr_1: { options = { + booleans: true, evaluate: true, } input: { @@ -871,6 +872,23 @@ delete_expr: { console.log(delete (0 / 0)); } expect: { + console.log(delete undefined); + console.log((void 0, !0)); + console.log(delete Infinity); + console.log((1 / 0, !0)); + console.log(delete NaN); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_expr_2: { + options = { + booleans: true, + evaluate: true, + keep_infinity: true, + } + input: { console.log(delete undefined); console.log(delete void 0); console.log(delete Infinity); @@ -878,5 +896,64 @@ delete_expr: { console.log(delete NaN); console.log(delete (0 / 0)); } + expect: { + console.log(delete undefined); + console.log((void 0, !0)); + console.log(delete Infinity); + console.log((1 / 0, !0)); + console.log(delete NaN); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_binary_1: { + options = { + booleans: true, + evaluate: true, + side_effects: true, + } + input: { + console.log(delete (true && undefined)); + console.log(delete (true && void 0)); + console.log(delete (true && Infinity)); + console.log(delete (true && (1 / 0))); + console.log(delete (true && NaN)); + console.log(delete (true && (0 / 0))); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } + expect_stdout: true +} + +delete_binary_2: { + options = { + booleans: true, + evaluate: true, + keep_infinity: true, + side_effects: true, + } + input: { + console.log(delete (false || undefined)); + console.log(delete (false || void 0)); + console.log(delete (false || Infinity)); + console.log(delete (false || (1 / 0))); + console.log(delete (false || NaN)); + console.log(delete (false || (0 / 0))); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((NaN, !0)); + } expect_stdout: true } diff --git a/test/compress/sequences.js b/test/compress/sequences.js index b3c54635..699341c0 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -466,3 +466,147 @@ issue_1758: { } expect_stdout: "undefined" } + +delete_seq_1: { + options = { + booleans: true, + side_effects: true, + } + input: { + console.log(delete (1, undefined)); + console.log(delete (1, void 0)); + console.log(delete (1, Infinity)); + console.log(delete (1, 1 / 0)); + console.log(delete (1, NaN)); + console.log(delete (1, 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_seq_2: { + options = { + booleans: true, + side_effects: true, + } + input: { + console.log(delete (1, 2, undefined)); + console.log(delete (1, 2, void 0)); + console.log(delete (1, 2, Infinity)); + console.log(delete (1, 2, 1 / 0)); + console.log(delete (1, 2, NaN)); + console.log(delete (1, 2, 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((1 / 0, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_seq_3: { + options = { + booleans: true, + keep_infinity: true, + side_effects: true, + } + input: { + console.log(delete (1, 2, undefined)); + console.log(delete (1, 2, void 0)); + console.log(delete (1, 2, Infinity)); + console.log(delete (1, 2, 1 / 0)); + console.log(delete (1, 2, NaN)); + console.log(delete (1, 2, 0 / 0)); + } + expect: { + console.log((void 0, !0)); + console.log((void 0, !0)); + console.log((Infinity, !0)); + console.log((1 / 0, !0)); + console.log((NaN, !0)); + console.log((0 / 0, !0)); + } + expect_stdout: true +} + +delete_seq_4: { + options = { + booleans: true, + sequences: true, + side_effects: true, + } + input: { + function f() {} + console.log(delete (f(), undefined)); + console.log(delete (f(), void 0)); + console.log(delete (f(), Infinity)); + console.log(delete (f(), 1 / 0)); + console.log(delete (f(), NaN)); + console.log(delete (f(), 0 / 0)); + } + expect: { + function f() {} + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)); + } + expect_stdout: true +} + +delete_seq_5: { + options = { + booleans: true, + keep_infinity: true, + sequences: true, + side_effects: true, + } + input: { + function f() {} + console.log(delete (f(), undefined)); + console.log(delete (f(), void 0)); + console.log(delete (f(), Infinity)); + console.log(delete (f(), 1 / 0)); + console.log(delete (f(), NaN)); + console.log(delete (f(), 0 / 0)); + } + expect: { + function f() {} + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)), + console.log((f(), !0)); + } + expect_stdout: true +} + +delete_seq_6: { + options = { + booleans: true, + side_effects: true, + } + input: { + var a; + console.log(delete (1, a)); + } + expect: { + var a; + console.log((a, !0)); + } + expect_stdout: true +} From 0479ff0c54e8eea90ff5471bd9f77cb7f2e355c3 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 8 Apr 2017 16:46:25 +0800 Subject: [PATCH 18/21] fix a couple of bugs in `global_defs` (#1802) - `optimize()` substituted expression - compute nested property string correctly fixes #1801 Miscellaneous - reset optimisation flags on all node types --- lib/compress.js | 12 +++++------- test/compress/global_defs.js | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index de0ff387..5b405ec1 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -260,10 +260,8 @@ merge(Compressor.prototype, { } }); var tw = new TreeWalker(function(node, descend){ - if (!(node instanceof AST_Directive || node instanceof AST_Constant)) { - node._squeezed = false; - node._optimized = false; - } + node._squeezed = false; + node._optimized = false; if (reduce_vars) { if (node instanceof AST_Toplevel) node.globals.each(reset_def); if (node instanceof AST_Scope) node.variables.each(reset_def); @@ -1346,7 +1344,7 @@ merge(Compressor.prototype, { } def(AST_Node, noop); def(AST_Dot, function(compressor, suffix){ - return this.expression._find_defs(compressor, suffix + "." + this.property); + return this.expression._find_defs(compressor, "." + this.property + suffix); }); def(AST_SymbolRef, function(compressor, suffix){ if (!this.global()) return; @@ -3582,7 +3580,7 @@ merge(Compressor.prototype, { OPT(AST_SymbolRef, function(self, compressor){ var def = self.resolve_defines(compressor); if (def) { - return def; + return def.optimize(compressor); } // testing against !self.scope.uses_with first is an optimization if (compressor.option("screw_ie8") @@ -3932,7 +3930,7 @@ merge(Compressor.prototype, { OPT(AST_Dot, function(self, compressor){ var def = self.resolve_defines(compressor); if (def) { - return def; + return def.optimize(compressor); } var prop = self.property; if (RESERVED_WORDS(prop) && !compressor.option("screw_ie8")) { diff --git a/test/compress/global_defs.js b/test/compress/global_defs.js index a69d031e..f1ba8f32 100644 --- a/test/compress/global_defs.js +++ b/test/compress/global_defs.js @@ -145,3 +145,18 @@ mixed: { 'WARN: global_defs CONFIG.VALUE redefined [test/compress/global_defs.js:129,8]', ] } + +issue_1801: { + options = { + booleans: true, + global_defs: { + "CONFIG.FOO.BAR": true, + }, + } + input: { + console.log(CONFIG.FOO.BAR); + } + expect: { + console.log(!0); + } +} From 9a978843f516391495c4fd446f2e01c2f47369ff Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 9 Apr 2017 01:36:38 +0800 Subject: [PATCH 19/21] enhance `test/ufuzz.js` (#1803) - `-E` to report test cases with runtime errors - favor returning expressions rather than empty return - emit a newline upon fuzzer completion to not erase the iteration count closes #1800 --- test/ufuzz.js | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index 78943ca4..2a09e2f7 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -71,6 +71,7 @@ var STMT_COUNT_FROM_GLOBAL = true; // count statement depth from nearest functio var num_iterations = +process.argv[2] || 1/0; var verbose = false; // log every generated test var verbose_interval = false; // log every 100 generated tests +var verbose_error = false; for (var i = 2; i < process.argv.length; ++i) { switch (process.argv[i]) { case '-v': @@ -79,6 +80,9 @@ for (var i = 2; i < process.argv.length; ++i) { case '-V': verbose_interval = true; break; + case '-E': + verbose_error = true; + break; case '-t': MAX_GENERATED_TOPLEVELS_PER_RUN = +process.argv[++i]; if (!MAX_GENERATED_TOPLEVELS_PER_RUN) throw new Error('Must generate at least one toplevel per run'); @@ -118,6 +122,7 @@ for (var i = 2; i < process.argv.length; ++i) { console.log(': generate this many cases (if used must be first arg)'); console.log('-v: print every generated test case'); console.log('-V: print every 100th generated test case'); + console.log('-E: print generated test case with runtime error'); console.log('-t : generate this many toplevels per run (more take longer)'); console.log('-r : maximum recursion depth for generator (higher takes longer)'); console.log('-s1 : force the first level statement to be this one (see list below)'); @@ -414,24 +419,27 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn return 'var ' + n1 + ' = ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ', ' + n2 + ' = ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; } case STMT_RETURN_ETC: - switch (rng(3)) { + switch (rng(8)) { + case 0: case 1: + case 2: + case 3: if (canBreak && rng(5) === 0) return 'break;'; if (canContinue && rng(5) === 0) return 'continue;'; if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; if (rng(3) == 0) return '/*3*/return;'; - return '/*4*/return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; - case 2: - // must wrap in curlies to prevent orphaned `else` statement - if (canThrow && rng(5) === 0) return '{ throw ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; - if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; - return '{ /*1*/ return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; - default: + return 'return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; + case 4: // this is actually more like a parser test, but perhaps it hits some dead code elimination traps // must wrap in curlies to prevent orphaned `else` statement // note: you can't `throw` without an expression so don't put a `throw` option in this case if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; return '{ /*2*/ return\n' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; + default: + // must wrap in curlies to prevent orphaned `else` statement + if (canThrow && rng(5) === 0) return '{ throw ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; + if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; + return '{ return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + '}'; } case STMT_FUNC_EXPR: // "In non-strict mode code, functions can only be declared at top level, inside a block, or ..." @@ -894,6 +902,20 @@ for (var round = 1; round <= num_iterations; round++) { ok = sandbox.same_stdout(original_result, uglify_result); } if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(options); - if (!ok && isFinite(num_iterations)) process.exit(1); + else if (verbose_error && typeof original_result != "string") { + console.log("//============================================================="); + console.log("// original code"); + try_beautify(original_code, original_result); + console.log(); + console.log(); + console.log("original result:"); + console.log(original_result); + console.log(); + } + if (!ok && isFinite(num_iterations)) { + console.log(); + process.exit(1); + } }); } +console.log(); From d6fbc365e2f00eaaba0f1dd19e81037a64976def Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 9 Apr 2017 03:18:14 +0800 Subject: [PATCH 20/21] fix LHS cases for NaN & friends (#1804) `Infinity = beyond` should not become `1/0 = beyond` --- lib/compress.js | 35 +++++++++++++++-------------------- test/compress/evaluate.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 5b405ec1..1d9258cf 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -404,7 +404,7 @@ merge(Compressor.prototype, { function is_modified(node, level, func) { var parent = tw.parent(level); - if (isLHS(node, parent) + if (is_lhs(node, parent) || !func && parent instanceof AST_Call && parent.expression === node) { return true; } else if (parent instanceof AST_PropAccess && parent.expression === node) { @@ -697,7 +697,7 @@ merge(Compressor.prototype, { return statements; function is_lvalue(node, parent) { - return node instanceof AST_SymbolRef && isLHS(node, parent); + return node instanceof AST_SymbolRef && is_lhs(node, parent); } function replace_var(node, parent, is_constant) { if (is_lvalue(node, parent)) return node; @@ -1299,9 +1299,9 @@ merge(Compressor.prototype, { var unary_side_effects = makePredicate("delete ++ --"); - function isLHS(node, parent) { - return parent instanceof AST_Unary && unary_side_effects(parent.operator) - || parent instanceof AST_Assign && parent.left === node; + function is_lhs(node, parent) { + if (parent instanceof AST_Unary && unary_side_effects(parent.operator)) return parent.expression; + if (parent instanceof AST_Assign && parent.left === node) return node; } (function (def){ @@ -1314,7 +1314,7 @@ merge(Compressor.prototype, { node = parent; parent = compressor.parent(level++); } while (parent instanceof AST_PropAccess && parent.expression === node); - if (isLHS(node, parent)) { + if (is_lhs(node, parent)) { compressor.warn('global_defs ' + this.print_to_string() + ' redefined [{file}:{line},{col}]', this.start); } else { return def; @@ -3620,12 +3620,8 @@ merge(Compressor.prototype, { return self; }); - function in_delete(parent) { - return parent instanceof AST_UnaryPrefix && parent.operator == "delete"; - } - - function is_atomic(parent, self) { - return parent.expression instanceof AST_SymbolRef || parent.expression.TYPE === self.TYPE; + function is_atomic(lhs, self) { + return lhs instanceof AST_SymbolRef || lhs.TYPE === self.TYPE; } OPT(AST_Undefined, function(self, compressor){ @@ -3641,8 +3637,8 @@ merge(Compressor.prototype, { return ref; } } - var parent = compressor.parent(); - if (in_delete(parent) && is_atomic(parent, self)) return self; + var lhs = is_lhs(compressor.self(), compressor.parent()); + if (lhs && is_atomic(lhs, self)) return self; return make_node(AST_UnaryPrefix, self, { operator: "void", expression: make_node(AST_Number, self, { @@ -3652,11 +3648,10 @@ merge(Compressor.prototype, { }); OPT(AST_Infinity, function(self, compressor){ - var parent = compressor.parent(); - var del = in_delete(parent); - if (del && is_atomic(parent, self)) return self; + var lhs = is_lhs(compressor.self(), compressor.parent()); + if (lhs && is_atomic(lhs, self)) return self; if (compressor.option("keep_infinity") - && !(del && !is_atomic(parent, self)) + && !(lhs && !is_atomic(lhs, self)) && !find_variable(compressor, "Infinity")) return self; return make_node(AST_Binary, self, { @@ -3671,8 +3666,8 @@ merge(Compressor.prototype, { }); OPT(AST_NaN, function(self, compressor){ - var parent = compressor.parent(); - if (in_delete(parent) && !is_atomic(parent, self) + var lhs = is_lhs(compressor.self(), compressor.parent()); + if (lhs && !is_atomic(lhs, self) || find_variable(compressor, "NaN")) { return make_node(AST_Binary, self, { operator: "/", diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 3c16e201..611acf0d 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -957,3 +957,35 @@ delete_binary_2: { } expect_stdout: true } + +Infinity_NaN_undefined_LHS: { + beautify = { + beautify: true, + } + input: { + function f() { + Infinity = Infinity; + ++Infinity; + Infinity--; + NaN *= NaN; + ++NaN; + NaN--; + undefined |= undefined; + ++undefined; + undefined--; + } + } + expect_exact: [ + "function f() {", + " Infinity = 1 / 0;", + " ++Infinity;", + " Infinity--;", + " NaN *= NaN;", + " ++NaN;", + " NaN--;", + " undefined |= void 0;", + " ++undefined;", + " undefined--;", + "}", + ] +} From 04b89645058d85b8b67bb94fb9e39252160a0959 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 9 Apr 2017 11:36:57 +0800 Subject: [PATCH 21/21] v2.8.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9a97f4a..cfa8eb74 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.21", + "version": "2.8.22", "engines": { "node": ">=0.8.0" },