diff --git a/bin/uglifyjs b/bin/uglifyjs index 08e3d495d52..8e679977456 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -10,7 +10,9 @@ var info = require("../package.json"); var path = require("path"); var UglifyJS = require("../tools/node"); -var skip_keys = [ "cname", "fixed", "inlined", "parent_scope", "scope", "uses_eval", "uses_with" ]; +var skip_keys = [ "cname", "fixed", "in_arg", "inlined", "length_read", "parent_scope", "redef", "scope", "unused" ]; +var truthy_keys = [ "optional", "pure", "terminal", "uses_arguments", "uses_eval", "uses_with" ]; + var files = {}; var options = {}; var short_forms = { @@ -430,7 +432,7 @@ function run() { case "thedef": return symdef(value); } - if (skip_key(key)) return; + if (skip_property(key, value)) return; if (value instanceof UglifyJS.AST_Token) return; if (value instanceof UglifyJS.Dictionary) return; if (value instanceof UglifyJS.AST_Node) { @@ -519,7 +521,7 @@ function read_file(path, default_value) { } function parse_js(value, options, flag) { - if (!options || typeof options != "object") options = {}; + if (!options || typeof options != "object") options = Object.create(null); if (typeof value == "string") try { UglifyJS.parse(value, { expression: true @@ -559,8 +561,10 @@ function parse_js(value, options, flag) { return options; } -function skip_key(key) { - return skip_keys.indexOf(key) >= 0; +function skip_property(key, value) { + return skip_keys.indexOf(key) >= 0 + // only skip truthy_keys if their value is falsy + || truthy_keys.indexOf(key) >= 0 && !value; } function symdef(def) { diff --git a/lib/ast.js b/lib/ast.js index 344d2eedbc1..b8732a7814a 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -50,6 +50,8 @@ function DEFNODE(type, props, methods, base) { if (base && base.PROPS) props = props.concat(base.PROPS); var code = [ "return function AST_", type, "(props){", + // not essential, but speeds up compress by a few percent + "this._bits=0;", "if(props){", ]; props.forEach(function(prop) { @@ -135,6 +137,52 @@ var AST_Node = DEFNODE("Node", "start end", { }, }, null); +DEF_BITPROPS(AST_Node, [ + "_optimized", + "_squeezed", + // AST_Call + "call_only", + "collapse_scanning", + // AST_SymbolRef + "defined", + "evaluating", + "falsy", + // AST_SymbolRef + "in_arg", + // AST_Return + "in_bool", + // AST_SymbolRef + "is_undefined", + // AST_LambdaExpression + // AST_LambdaDefinition + "inlined", + // AST_Lambda + "length_read", + // AST_Yield + "nested", + // AST_Lambda + "new", + // AST_Call + // AST_PropAccess + "optional", + // AST_ClassProperty + "private", + // AST_Call + "pure", + // AST_Assign + "redundant", + // AST_ClassProperty + "static", + // AST_Call + // AST_PropAccess + "terminal", + "truthy", + // AST_Scope + "uses_eval", + // AST_Scope + "uses_with", +]); + (AST_Node.log_function = function(fn, verbose) { if (typeof fn != "function") { AST_Node.info = AST_Node.warn = noop; @@ -253,7 +301,7 @@ var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { }, }, AST_Statement); -var AST_BlockScope = DEFNODE("BlockScope", "enclosed functions make_def parent_scope variables", { +var AST_BlockScope = DEFNODE("BlockScope", "_var_names enclosed functions make_def parent_scope variables", { $documentation: "Base class for all statements introducing a lexical scope", $propdoc: { enclosed: "[SymbolDef*/S] a list of all symbol definitions that are accessed from this scope or any subscopes", @@ -484,7 +532,7 @@ var AST_With = DEFNODE("With", "expression", { /* -----[ scope and functions ]----- */ -var AST_Scope = DEFNODE("Scope", "uses_eval uses_with", { +var AST_Scope = DEFNODE("Scope", "fn_defs may_call_this uses_eval uses_with", { $documentation: "Base class for all statements introducing a lexical scope", $propdoc: { uses_eval: "[boolean/S] tells whether this scope contains a direct call to the global `eval`", @@ -543,13 +591,13 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); -var AST_Lambda = DEFNODE("Lambda", "argnames length_read rest uses_arguments", { +var AST_Lambda = DEFNODE("Lambda", "argnames length_read rest safe_ids uses_arguments", { $documentation: "Base class for functions", $propdoc: { argnames: "[(AST_DefaultValue|AST_Destructured|AST_SymbolFunarg)*] array of function arguments and/or destructured literals", length_read: "[boolean/S] whether length property of this function is accessed", rest: "[(AST_Destructured|AST_SymbolFunarg)?] rest parameter, or null if absent", - uses_arguments: "[boolean/S] whether this function accesses the arguments array", + uses_arguments: "[boolean|number/S] whether this function accesses the arguments array", }, each_argname: function(visit) { var tw = new TreeWalker(function(node) { @@ -1295,7 +1343,7 @@ var AST_Call = DEFNODE("Call", "args expression optional pure terminal", { args: "[AST_Node*] array of arguments", expression: "[AST_Node] expression to invoke as function", optional: "[boolean] whether the expression is optional chaining", - pure: "[string/S] marker for side-effect-free call expression", + pure: "[boolean/S] marker for side-effect-free call expression", terminal: "[boolean] whether the chain has ended", }, walk: function(visitor) { @@ -1747,7 +1795,7 @@ var AST_SymbolVar = DEFNODE("SymbolVar", null, { $documentation: "Symbol defining a variable", }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "unused", { $documentation: "Symbol naming a function argument", }, AST_SymbolVar); diff --git a/lib/compress.js b/lib/compress.js index f7232e9ae93..b5c67c2d9f3 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -185,90 +185,89 @@ function Compressor(options, false_by_default) { }; } -Compressor.prototype = new TreeTransformer; -merge(Compressor.prototype, { - option: function(key) { return this.options[key] }, - exposed: function(def) { - if (def.exported) return true; - if (def.undeclared) return true; - if (!(def.global || def.scope.resolve() instanceof AST_Toplevel)) return false; - var toplevel = this.toplevel; - return !all(def.orig, function(sym) { - return toplevel[sym instanceof AST_SymbolDefun ? "funcs" : "vars"]; - }); - }, - compress: function(node) { - node = node.resolve_defines(this); - node.hoist_exports(this); - if (this.option("expression")) { - node.process_expression(true); - } - var merge_vars = this.options.merge_vars; - var passes = +this.options.passes || 1; - var min_count = 1 / 0; - var stopping = false; - var mangle = { ie: this.option("ie") }; - for (var pass = 0; pass < passes; pass++) { - node.figure_out_scope(mangle); - if (pass > 0 || this.option("reduce_vars")) - node.reset_opt_flags(this); - this.options.merge_vars = merge_vars && (stopping || pass == passes - 1); - node = node.transform(this); - if (passes > 1) { - var count = 0; - node.walk(new TreeWalker(function() { - count++; - })); - AST_Node.info("pass {pass}: last_count: {min_count}, count: {count}", { - pass: pass, - min_count: min_count, - count: count, - }); - if (count < min_count) { - min_count = count; - stopping = false; - } else if (stopping) { - break; - } else { - stopping = true; - } +Compressor.prototype = new TreeTransformer(function(node, descend, in_list) { + if (node._squeezed) return node; + var is_scope = node instanceof AST_Scope; + if (is_scope) { + node.hoist_properties(this); + node.hoist_declarations(this); + node.process_boolean_returns(this); + } + // Before https://github.com/mishoo/UglifyJS/pull/1602 AST_Node.optimize() + // would call AST_Node.transform() if a different instance of AST_Node is + // produced after OPT(). + // This corrupts TreeWalker.stack, which cause AST look-ups to malfunction. + // Migrate and defer all children's AST_Node.transform() to below, which + // will now happen after this parent AST_Node has been properly substituted + // thus gives a consistent AST snapshot. + descend(node, this); + // Existing code relies on how AST_Node.optimize() worked, and omitting the + // following replacement call would result in degraded efficiency of both + // output and performance. + descend(node, this); + var opt = node.optimize(this); + if (is_scope && opt === node && !this.has_directive("use asm") && !opt.pinned()) { + opt.drop_unused(this); + if (opt.merge_variables(this)) opt.drop_unused(this); + descend(opt, this); + } + if (opt === node) opt._squeezed = true; + return opt; +}); +Compressor.prototype.option = function(key) { + return this.options[key]; +}; +Compressor.prototype.exposed = function(def) { + if (def.exported) return true; + if (def.undeclared) return true; + if (!(def.global || def.scope.resolve() instanceof AST_Toplevel)) return false; + var toplevel = this.toplevel; + return !all(def.orig, function(sym) { + return toplevel[sym instanceof AST_SymbolDefun ? "funcs" : "vars"]; + }); +}; +Compressor.prototype.compress = function(node) { + node = node.resolve_defines(this); + node.hoist_exports(this); + if (this.option("expression")) { + node.process_expression(true); + } + var merge_vars = this.options.merge_vars; + var passes = +this.options.passes || 1; + var min_count = 1 / 0; + var stopping = false; + var mangle = { ie: this.option("ie") }; + for (var pass = 0; pass < passes; pass++) { + node.figure_out_scope(mangle); + if (pass > 0 || this.option("reduce_vars")) + node.reset_opt_flags(this); + this.options.merge_vars = merge_vars && (stopping || pass == passes - 1); + node = node.transform(this); + if (passes > 1) { + var count = 0; + node.walk(new TreeWalker(function() { + count++; + })); + AST_Node.info("pass {pass}: last_count: {min_count}, count: {count}", { + pass: pass, + min_count: min_count, + count: count, + }); + if (count < min_count) { + min_count = count; + stopping = false; + } else if (stopping) { + break; + } else { + stopping = true; } } - if (this.option("expression")) { - node.process_expression(false); - } - return node; - }, - before: function(node, descend, in_list) { - if (node._squeezed) return node; - var is_scope = node instanceof AST_Scope; - if (is_scope) { - node.hoist_properties(this); - node.hoist_declarations(this); - node.process_boolean_returns(this); - } - // Before https://github.com/mishoo/UglifyJS/pull/1602 AST_Node.optimize() - // would call AST_Node.transform() if a different instance of AST_Node is - // produced after OPT(). - // This corrupts TreeWalker.stack, which cause AST look-ups to malfunction. - // Migrate and defer all children's AST_Node.transform() to below, which - // will now happen after this parent AST_Node has been properly substituted - // thus gives a consistent AST snapshot. - descend(node, this); - // Existing code relies on how AST_Node.optimize() worked, and omitting the - // following replacement call would result in degraded efficiency of both - // output and performance. - descend(node, this); - var opt = node.optimize(this); - if (is_scope && opt === node && !this.has_directive("use asm") && !opt.pinned()) { - opt.drop_unused(this); - if (opt.merge_variables(this)) opt.drop_unused(this); - descend(opt, this); - } - if (opt === node) opt._squeezed = true; - return opt; } -}); + if (this.option("expression")) { + node.process_expression(false); + } + return node; +}; (function(OPT) { OPT(AST_Node, function(self, compressor) { @@ -532,6 +531,11 @@ merge(Compressor.prototype, { }); } + function safe_to_visit(tw, fn) { + var marker = fn.safe_ids; + return marker === undefined || marker === tw.safe_ids; + } + function walk_fn_def(tw, fn) { var was_scanning = tw.fn_scanning; tw.fn_scanning = fn; @@ -546,14 +550,14 @@ merge(Compressor.prototype, { d.single_use = false; var fixed = d.fixed; if (typeof fixed == "function") fixed = fixed(); - if (fixed instanceof AST_Lambda && HOP(fixed, "safe_ids")) return; + if (fixed instanceof AST_Lambda && fixed.safe_ids !== undefined) return; d.fixed = false; }); } function mark_fn_def(tw, def, fn) { - if (!HOP(fn, "safe_ids")) return; var marker = fn.safe_ids; + if (marker === undefined) return; if (marker === false) return; if (fn.parent_scope.resolve().may_call_this === return_true) { if (member(fn, tw.fn_visited)) revisit_fn_def(tw, fn); @@ -588,10 +592,10 @@ merge(Compressor.prototype, { walk_fn_def(tw, fn); }); fn_defs.forEach(function(fn) { - delete fn.safe_ids; + fn.safe_ids = undefined; }); - delete scope.fn_defs; - delete scope.may_call_this; + scope.fn_defs = undefined; + scope.may_call_this = undefined; } function push(tw) { @@ -622,7 +626,7 @@ merge(Compressor.prototype, { if (def.global && def.name == "arguments") return false; tw.loop_ids[def.id] = null; def.fixed = make_node(AST_Undefined, def.orig[0]); - if (in_order) delete def.safe_ids; + if (in_order) def.safe_ids = undefined; return true; } return !safe.assign || safe.assign === tw.safe_ids; @@ -644,7 +648,7 @@ merge(Compressor.prototype, { var safe = tw.safe_ids[def.id]; if (def.safe_ids) { def.safe_ids[def.id] = false; - delete def.safe_ids; + def.safe_ids = undefined; return def.fixed === null || HOP(tw.safe_ids, def.id) && !safe.read; } if (!HOP(tw.safe_ids, def.id)) { @@ -899,7 +903,7 @@ merge(Compressor.prototype, { if (left.equivalent_to(right) && !left.has_side_effects(compressor)) { right.walk(tw); walk_prop(left); - node.__drop = true; + node.redundant = true; return true; } if (ld && right instanceof AST_LambdaExpression) { @@ -1207,7 +1211,7 @@ merge(Compressor.prototype, { }); def(AST_Lambda, function(tw, descend, compressor) { var fn = this; - if (HOP(fn, "safe_ids") && fn.safe_ids !== tw.safe_ids) return true; + if (!safe_to_visit(tw, fn)) return true; if (!push_uniq(tw.fn_visited, fn)) return true; fn.inlined = false; push(tw); @@ -1222,7 +1226,7 @@ merge(Compressor.prototype, { var def = fn.name.definition(); var parent = tw.parent(); if (parent instanceof AST_ExportDeclaration || parent instanceof AST_ExportDefault) def.single_use = false; - if (HOP(fn, "safe_ids") && fn.safe_ids !== tw.safe_ids) return true; + if (!safe_to_visit(tw, fn)) return true; if (!push_uniq(tw.fn_visited, fn)) return true; fn.inlined = false; push(tw); @@ -1470,8 +1474,8 @@ merge(Compressor.prototype, { function reset_flags(node) { node._squeezed = false; node._optimized = false; - delete node.fixed; - if (node instanceof AST_Scope) delete node._var_names; + if (node instanceof AST_BlockScope) node._var_names = undefined; + if (node instanceof AST_SymbolRef) node.fixed = undefined; } AST_Toplevel.DEFMETHOD("reset_opt_flags", function(compressor) { @@ -1872,9 +1876,9 @@ merge(Compressor.prototype, { function collapse(statements, compressor) { if (scope.pinned()) return statements; var args; - var assignments = Object.create(null); + var assignments = new Dictionary(); var candidates = []; - var declare_only = Object.create(null); + var declare_only = new Dictionary(); var force_single; var stat_index = statements.length; var scanner = new TreeTransformer(function(node, descend) { @@ -2275,7 +2279,7 @@ merge(Compressor.prototype, { stop_if_hit = if_hit; stop_after = after; can_replace = replace; - delete fn.collapse_scanning; + fn.collapse_scanning = false; if (!abort) return false; abort = false; return true; @@ -2403,7 +2407,7 @@ merge(Compressor.prototype, { }); args = iife.args.slice(); var len = args.length; - var names = Object.create(null); + var names = new Dictionary(); for (var i = fn.argnames.length; --i >= 0;) { var sym = fn.argnames[i]; var arg = args[i]; @@ -2418,8 +2422,8 @@ merge(Compressor.prototype, { candidates.length = 0; break; } - if (sym.name in names) continue; - names[sym.name] = true; + if (names.has(sym.name)) continue; + names.set(sym.name, true); if (value) arg = !arg || is_undefined(arg) ? value : null; if (!arg && !value) { arg = make_node(AST_Undefined, sym).transform(compressor); @@ -2452,7 +2456,7 @@ merge(Compressor.prototype, { extract_candidates(lhs); extract_candidates(expr.right); if (lhs instanceof AST_SymbolRef && expr.operator == "=") { - assignments[lhs.name] = (assignments[lhs.name] || 0) + 1; + assignments.set(lhs.name, (assignments.get(lhs.name) || 0) + 1); } } else if (expr instanceof AST_Await) { extract_candidates(expr.expression, unused); @@ -2544,7 +2548,7 @@ merge(Compressor.prototype, { candidates.push(hit_stack.slice()); } } else { - declare_only[expr.name.name] = (declare_only[expr.name.name] || 0) + 1; + declare_only.set(expr.name.name, (declare_only.get(expr.name.name) || 0) + 1); } } if (expr.value) extract_candidates(expr.value); @@ -2760,7 +2764,7 @@ merge(Compressor.prototype, { } function remaining_refs(def) { - return def.references.length - def.replaced - (assignments[def.name] || 0); + return def.references.length - def.replaced - (assignments.get(def.name) || 0); } function get_lhs(expr) { @@ -2792,7 +2796,7 @@ merge(Compressor.prototype, { if (def.const_redefs) return; if (!member(lhs, def.orig)) return; if (scope.uses_arguments && is_funarg(def)) return; - var declared = def.orig.length - def.eliminated - (declare_only[def.name] || 0); + var declared = def.orig.length - def.eliminated - (declare_only.get(def.name) || 0); remaining = remaining_refs(def); if (def.fixed) remaining = Math.min(remaining, def.references.filter(function(ref) { if (!ref.fixed) return true; @@ -3001,7 +3005,7 @@ merge(Compressor.prototype, { if (hit_index <= end) return handle_custom_scan_order(node, tt); hit = true; if (node instanceof AST_VarDef) { - declare_only[node.name.name] = (declare_only[node.name.name] || 0) + 1; + declare_only.set(node.name.name, (declare_only.get(node.name.name) || 0) + 1); if (value_def) value_def.replaced++; node = node.clone(); node.value = null; @@ -3626,10 +3630,10 @@ merge(Compressor.prototype, { } function trim_assigns(name, value, exprs) { - var names = Object.create(null); - names[name.name] = true; + var names = new Dictionary(); + names.set(name.name, true); while (value instanceof AST_Assign && value.operator == "=") { - if (value.left instanceof AST_SymbolRef) names[value.left.name] = true; + if (value.left instanceof AST_SymbolRef) names.set(value.left.name, true); value = value.right; } if (!(value instanceof AST_Object)) return; @@ -3647,7 +3651,7 @@ merge(Compressor.prototype, { if (!(node.left instanceof AST_PropAccess)) return; var sym = node.left.expression; if (!(sym instanceof AST_SymbolRef)) return; - if (!(sym.name in names)) return; + if (!names.has(sym.name)) return; if (!node.right.is_constant_expression(scope)) return; var prop = node.left.property; if (prop instanceof AST_Node) { @@ -4618,7 +4622,6 @@ merge(Compressor.prototype, { } return this; }); - var nonsafe_props = makePredicate("__proto__ toString valueOf"); def(AST_Object, function(compressor, ignore_side_effects, cached, depth) { if (compressor.option("unsafe")) { var val = {}; @@ -4630,7 +4633,12 @@ merge(Compressor.prototype, { key = key._eval(compressor, ignore_side_effects, cached, depth); if (key === prop.key) return this; } - if (nonsafe_props[key]) return this; + switch (key) { + case "__proto__": + case "toString": + case "valueOf": + return this; + } val[key] = prop.value._eval(compressor, ignore_side_effects, cached, depth); if (val[key] === prop.value) return this; } @@ -4916,7 +4924,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Scope && node !== fn) return true; })); - delete fn.evaluating; + fn.evaluating = false; if (!found) return; } return this; @@ -4932,7 +4940,7 @@ merge(Compressor.prototype, { }); fn.evaluating = true; val = val._eval(compressor, ignore_side_effects, cached, depth); - delete fn.evaluating; + fn.evaluating = false; } cached_args.forEach(function(node) { delete node._eval; @@ -6589,7 +6597,7 @@ merge(Compressor.prototype, { argnames.pop(); } else if (i > default_length) { log(sym.name, "Dropping unused default argument assignment {name}"); - sym.name.__unused = true; + sym.name.unused = true; argnames[i] = sym.name; } else { log(sym.name, "Dropping unused default argument value {name}"); @@ -6600,12 +6608,12 @@ merge(Compressor.prototype, { var def = sym.definition(); if (def.id in in_use_ids) { trim = false; - if (indexOf_assign(def, sym) < 0) sym.__unused = null; + if (indexOf_assign(def, sym) < 0) sym.unused = null; } else if (trim) { log(sym, "Dropping unused function argument {name}"); argnames.pop(); } else { - sym.__unused = true; + sym.unused = true; } } fns_with_marked_args.push(node); @@ -6651,7 +6659,7 @@ merge(Compressor.prototype, { if (value && indexOf_assign(sym, def) < 0) { value = value.drop_side_effect_free(compressor); if (value) { - AST_Node.warn("Side effects in last use of variable {name} [{file}:{line},{col}]", template(def.name)); + AST_Node.warn("Side effects in definition of variable {name} [{file}:{line},{col}]", template(def.name)); side_effects.push(value); } value = null; @@ -6815,7 +6823,7 @@ merge(Compressor.prototype, { var assign = make_node(AST_Assign, def, { operator: "=", left: ref, - right: def.value + right: def.value, }); var index = indexOf_assign(sym, def); if (index >= 0) assign_in_use[sym.id][index] = assign; @@ -7159,7 +7167,7 @@ merge(Compressor.prototype, { var value = node.value.drop_side_effect_free(compressor); if (!value) return null; log(node.name, "Side effects in default value of unused variable {name}"); - node.name.__unused = null; + node.name.unused = null; node.value = value; } return node; @@ -7268,7 +7276,7 @@ merge(Compressor.prototype, { var prop_keys, prop_map; if (value instanceof AST_Object) { prop_keys = []; - prop_map = Object.create(null); + prop_map = new Dictionary(); value.properties.forEach(function(prop, index) { if (prop instanceof AST_Spread) return prop_map = false; var key = prop.key; @@ -7276,7 +7284,7 @@ merge(Compressor.prototype, { if (key instanceof AST_Node) { prop_map = false; } else if (prop_map && !(prop instanceof AST_ObjectSetter)) { - prop_map[key] = prop; + prop_map.set(key, prop); } prop_keys[index] = key; }); @@ -7285,8 +7293,8 @@ merge(Compressor.prototype, { value = false; node.rest = node.rest.transform(compressor.option("rests") ? trimmer : tt); } - var can_drop = Object.create(null); - var drop_keys = drop && Object.create(null); + var can_drop = new Dictionary(); + var drop_keys = drop && new Dictionary(); var properties = []; node.properties.map(function(prop) { var key = prop.key; @@ -7297,7 +7305,7 @@ merge(Compressor.prototype, { if (key instanceof AST_Node) { drop_keys = false; } else { - can_drop[key] = !(key in can_drop); + can_drop.set(key, !can_drop.has(key)); } return key; }).forEach(function(key, index) { @@ -7307,8 +7315,8 @@ merge(Compressor.prototype, { value = false; trimmed = prop.value.transform(trimmer) || retain_lhs(prop.value); } else { - drop = drop_keys && can_drop[key]; - var mapped = prop_map && prop_map[key]; + drop = drop_keys && can_drop.get(key); + var mapped = prop_map && prop_map.get(key); if (mapped) { value = mapped.value; if (value instanceof AST_Accessor) value = false; @@ -7318,21 +7326,21 @@ merge(Compressor.prototype, { trimmed = prop.value.transform(trimmer); if (!trimmed) { if (node.rest || retain_key(prop)) trimmed = retain_lhs(prop.value); - if (drop_keys && !(key in drop_keys)) { + if (drop_keys && !drop_keys.has(key)) { if (mapped) { - drop_keys[key] = mapped; + drop_keys.set(key, mapped); if (value === null) { - prop_map[key] = retain_key(mapped) && make_node(AST_ObjectKeyVal, mapped, { + prop_map.set(key, retain_key(mapped) && make_node(AST_ObjectKeyVal, mapped, { key: mapped.key, value: make_node(AST_Number, mapped, { value: 0 }), - }); + })); } } else { - drop_keys[key] = true; + drop_keys.set(key, true); } } } else if (drop_keys) { - drop_keys[key] = false; + drop_keys.set(key, false); } if (value) mapped.value = value; } @@ -7347,10 +7355,10 @@ merge(Compressor.prototype, { if (prop instanceof AST_Spread) return prop; var key = prop_keys[index]; if (key instanceof AST_Node) return prop; - if (key in drop_keys) { - var mapped = drop_keys[key]; + if (drop_keys.has(key)) { + var mapped = drop_keys.get(key); if (!mapped) return prop; - if (mapped === prop) return prop_map[key] || List.skip; + if (mapped === prop) return prop_map.get(key) || List.skip; } else if (node.rest) { return prop; } @@ -7416,7 +7424,7 @@ merge(Compressor.prototype, { } return make_node(AST_DestructuredObject, node, { properties: [] }); } - node.__unused = null; + node.unused = null; return node; } } @@ -7443,7 +7451,7 @@ merge(Compressor.prototype, { if (var_decl <= 1) hoist_vars = false; } if (!hoist_funs && !hoist_vars) return; - var consts = Object.create(null); + var consts = new Dictionary(); var dirs = []; var hoisted = []; var vars = new Dictionary(), vars_found = 0; @@ -7469,7 +7477,7 @@ merge(Compressor.prototype, { if (!all(node.definitions, function(defn) { var sym = defn.name; return sym instanceof AST_SymbolVar - && !consts[sym.name] + && !consts.has(sym.name) && self.find_variable(sym.name) === sym.definition(); })) return node; node.definitions.forEach(function(def) { @@ -7488,7 +7496,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Scope) return node; if (node instanceof AST_SymbolConst) { - consts[node.name] = true; + consts.set(node.name, true); return node; } }); @@ -7650,12 +7658,12 @@ merge(Compressor.prototype, { AST_BlockScope.DEFMETHOD("var_names", function() { var var_names = this._var_names; if (!var_names) { - this._var_names = var_names = Object.create(null); + this._var_names = var_names = new Dictionary(); this.enclosed.forEach(function(def) { - var_names[def.name] = true; + var_names.set(def.name, true); }); this.variables.each(function(def, name) { - var_names[name] = true; + var_names.set(name, true); }); } return var_names; @@ -7674,7 +7682,7 @@ merge(Compressor.prototype, { prefix = prefix.replace(/(?:^[^a-z_$]|[^a-z0-9_$])/ig, "_"); var name = prefix; for (var i = 0; !all(scopes, function(scope) { - return !scope.var_names()[name]; + return !scope.var_names().has(name); }); i++) name = prefix + "$" + i; var sym = make_node(type, orig, { name: name, @@ -7683,7 +7691,7 @@ merge(Compressor.prototype, { var def = this.def_variable(sym); scopes.forEach(function(scope) { scope.enclosed.push(def); - scope.var_names()[name] = true; + scope.var_names().set(name, true); }); return sym; }); @@ -8052,7 +8060,7 @@ merge(Compressor.prototype, { }) && all(fn.argnames, function(argname) { return !argname.match_symbol(return_false); }) && !(fn.rest && fn.rest.match_symbol(return_false)); - delete fn.new; + fn.new = false; return result; } function drop_class(self, compressor, first_in_statement) { @@ -9142,7 +9150,7 @@ merge(Compressor.prototype, { var scope = def.scope.resolve(); for (var s = def.scope; s !== scope;) { s = s.parent_scope; - if (s.var_names()[def.name]) return true; + if (s.var_names().has(def.name)) return true; } } @@ -9158,7 +9166,7 @@ merge(Compressor.prototype, { def.scope = scope; scope.variables.set(def.name, def); scope.enclosed.push(def); - scope.var_names()[def.name] = true; + scope.var_names().set(def.name, true); }), value: defn.value, }); @@ -9244,7 +9252,7 @@ merge(Compressor.prototype, { if (argname instanceof AST_DestructuredObject) { return argname.properties.length == 0 && !argname.rest && arg && !arg.may_throw_on_access(compressor); } - return argname.__unused; + return argname.unused; } : return_false; var side_effects = []; for (var i = 0; i < args.length; i++) { @@ -9252,7 +9260,7 @@ merge(Compressor.prototype, { if (drop_defaults && argname instanceof AST_DefaultValue && args[i].is_defined(compressor)) { argnames[i] = argname = argname.name; } - if (!argname || "__unused" in argname) { + if (!argname || argname.unused !== undefined) { var node = args[i].drop_side_effect_free(compressor); if (drop_fargs(argname)) { if (argname) argnames.splice(i, 1); @@ -9959,30 +9967,33 @@ merge(Compressor.prototype, { } function var_exists(defined, name) { - return defined[name] || identifier_atom[name] || scope.var_names()[name]; + return defined.has(name) || identifier_atom[name] || scope.var_names().has(name); } - function can_inject_args(defined, used, safe_to_inject) { + function can_inject_args(defined, safe_to_inject) { var abort = false; fn.each_argname(function(arg) { if (abort) return; - if (arg.__unused) return; + if (arg.unused) return; if (!safe_to_inject || var_exists(defined, arg.name)) return abort = true; - used[arg.name] = true; + arg_used.set(arg.name, true); if (in_loop) in_loop.push(arg.definition()); }); return !abort; } - function can_inject_vars(defined, used, safe_to_inject) { + function can_inject_vars(defined, safe_to_inject) { for (var i = 0; i < fn.body.length; i++) { var stat = fn.body[i]; if (stat instanceof AST_LambdaDefinition) { - if (!safe_to_inject || var_exists(used, stat.name.name)) return false; + var name = stat.name; + if (!safe_to_inject) return false; + if (arg_used.has(name.name)) return false; + if (var_exists(defined, name.name)) return false; if (!all(stat.enclosed, function(def) { - return def.scope === stat || !defined[def.name]; + return def.scope === stat || !defined.has(def.name); })) return false; - if (in_loop) in_loop.push(stat.name.definition()); + if (in_loop) in_loop.push(name.definition()); continue; } if (!(stat instanceof AST_Var)) continue; @@ -9997,12 +10008,12 @@ merge(Compressor.prototype, { } function can_inject_symbols() { - var defined = Object.create(null); + var defined = new Dictionary(); var level = 0, child; scope = current; do { if (scope.variables) scope.variables.each(function(def) { - defined[def.name] = true; + defined.set(def.name, true); }); child = scope; scope = compressor.parent(level++); @@ -10025,23 +10036,22 @@ merge(Compressor.prototype, { var safe_to_inject = exp !== fn || fn.parent_scope.resolve() === scope; if (scope instanceof AST_Toplevel) { if (compressor.toplevel.vars) { - defined["arguments"] = true; + defined.set("arguments", true); } else { safe_to_inject = false; } } + arg_used = new Dictionary(); var inline = compressor.option("inline"); - arg_used = Object.create(defined); - if (!can_inject_args(defined, arg_used, inline >= 2 && safe_to_inject)) return false; - var used = Object.create(arg_used); - if (!can_inject_vars(defined, used, inline >= 3 && safe_to_inject)) return false; + if (!can_inject_args(defined, inline >= 2 && safe_to_inject)) return false; + if (!can_inject_vars(defined, inline >= 3 && safe_to_inject)) return false; return !in_loop || in_loop.length == 0 || !is_reachable(fn, in_loop); } function append_var(decls, expressions, name, value) { var def = name.definition(); - if (!scope.var_names()[name.name]) { - scope.var_names()[name.name] = true; + if (!scope.var_names().has(name.name)) { + scope.var_names().set(name.name, true); decls.push(make_node(AST_VarDef, name, { name: name, value: null, @@ -10075,12 +10085,12 @@ merge(Compressor.prototype, { name = argname; } var value = self.args[i]; - if (name.__unused || scope.var_names()[name.name]) { + if (name.unused || scope.var_names().has(name.name)) { if (value) expressions.push(value); } else { var symbol = make_node(AST_SymbolVar, name, name); name.definition().orig.push(symbol); - if ("__unused" in name) { + if (name.unused !== undefined) { append_var(decls, expressions, symbol); if (value) expressions.push(value); } else { @@ -10093,7 +10103,7 @@ merge(Compressor.prototype, { expressions.reverse(); for (i = default_args.length; --i >= 0;) { var node = default_args[i]; - if ("__unused" in node.name) { + if (node.name.unused !== undefined) { expressions.push(node.value); } else { var sym = make_node(AST_SymbolRef, node.name, node.name); @@ -10112,7 +10122,7 @@ merge(Compressor.prototype, { operator: "=", left: make_node(AST_DestructuredArray, self, { elements: fn.argnames.map(function(argname) { - if (argname.__unused) return make_node(AST_Hole, argname); + if (argname.unused) return make_node(AST_Hole, argname); return argname.convert_symbol(AST_SymbolRef, process); }), rest: fn.rest && fn.rest.convert_symbol(AST_SymbolRef, process), @@ -10153,7 +10163,7 @@ merge(Compressor.prototype, { scope.functions.set(def.name, def); scope.variables.set(def.name, def); scope.enclosed.push(def); - scope.var_names()[def.name] = true; + scope.var_names().set(def.name, true); args.push(stat); } continue; @@ -10163,7 +10173,7 @@ merge(Compressor.prototype, { var var_def = stat.definitions[j]; var name = flatten_var(var_def.name); append_var(decl_var, expr_var, name, var_def.value); - if (in_loop && !HOP(arg_used, name.name)) { + if (in_loop && !arg_used.has(name.name)) { var def = fn.variables.get(name.name); var sym = make_node(AST_SymbolRef, name, name); def.references.push(sym); @@ -10196,9 +10206,9 @@ merge(Compressor.prototype, { })); [].splice.apply(scope.body, args); fn.enclosed.forEach(function(def) { - if (scope.var_names()[def.name]) return; + if (scope.var_names().has(def.name)) return; scope.enclosed.push(def); - scope.var_names()[def.name] = true; + scope.var_names().set(def.name, true); }); return expressions; } @@ -11320,9 +11330,9 @@ merge(Compressor.prototype, { var scope = self.scope.resolve(); fixed.enclosed.forEach(function(def) { if (fixed.variables.has(def.name)) return; - if (scope.var_names()[def.name]) return; + if (scope.var_names().has(def.name)) return; scope.enclosed.push(def); - scope.var_names()[def.name] = true; + scope.var_names().set(def.name, true); }); } var value; @@ -11635,7 +11645,7 @@ merge(Compressor.prototype, { if (compressor.option("dead_code")) { if (self.left instanceof AST_PropAccess) { if (self.operator == "=") { - if (self.__drop) { + if (self.redundant) { var exprs = [ self.left.expression ]; if (self.left instanceof AST_Sub) exprs.push(self.left.property); exprs.push(self.right); @@ -12302,7 +12312,7 @@ merge(Compressor.prototype, { if (assigned) def.reassigned--; var sym = make_node(AST_SymbolRef, self, argname); sym.reference(); - delete argname.__unused; + argname.unused = undefined; return sym; } } @@ -12380,10 +12390,8 @@ merge(Compressor.prototype, { } }); - AST_Arrow.DEFMETHOD("contains_super", return_false); - AST_AsyncArrow.DEFMETHOD("contains_super", return_false); - AST_Lambda.DEFMETHOD("contains_super", function() { - var result; + AST_LambdaExpression.DEFMETHOD("contains_super", function() { + var result = false; var self = this; self.walk(new TreeWalker(function(node) { if (result) return true; @@ -12392,20 +12400,26 @@ merge(Compressor.prototype, { })); return result; }); - AST_LambdaDefinition.DEFMETHOD("contains_super", return_false); - AST_Scope.DEFMETHOD("contains_super", return_false); - AST_Arrow.DEFMETHOD("contains_this", return_false); - AST_AsyncArrow.DEFMETHOD("contains_this", return_false); - AST_Node.DEFMETHOD("contains_this", function() { - var result; - var self = this; - self.walk(new TreeWalker(function(node) { - if (result) return true; - if (node instanceof AST_This) return result = true; - if (node !== self && node instanceof AST_Scope && !is_arrow(node)) return true; - })); - return result; + // contains_this() + // returns false only if context bound by the specified scope (or scope + // containing the specified expression) is not referenced by `this` + (function(def) { + // scope of arrow function cannot bind to any context + def(AST_Arrow, return_false); + def(AST_AsyncArrow, return_false); + def(AST_Node, function() { + var result = false; + var self = this; + self.walk(new TreeWalker(function(node) { + if (result) return true; + if (node instanceof AST_This) return result = true; + if (node !== self && node instanceof AST_Scope && !is_arrow(node)) return true; + })); + return result; + }); + })(function(node, func) { + node.DEFMETHOD("contains_this", func); }); function can_hoist_property(prop) { @@ -12546,7 +12560,8 @@ merge(Compressor.prototype, { var found = false; var generated = false; var keep_duplicate = compressor.has_directive("use strict"); - var keys = new Dictionary(); + var keys = []; + var map = new Dictionary(); var values = []; self.properties.forEach(function(prop) { if (!(prop instanceof AST_Spread)) return process(prop); @@ -12589,19 +12604,27 @@ merge(Compressor.prototype, { return make_node(AST_Object, self, { properties: values }); function flush() { - keys.each(function(props) { - if (props.length == 1) return values.push(props[0]); + keys.forEach(function(key) { + var props = map.get(key); + switch (props.length) { + case 0: + return; + case 1: + return values.push(props[0]); + } changed = true; var tail = keep_duplicate && !generated && props.pop(); values.push(props.length == 1 ? props[0] : make_node(AST_ObjectKeyVal, self, { key: props[0].key, value: make_sequence(self, props.map(function(prop) { return prop.value; - })) + })), })); if (tail) values.push(tail); + props.length = 0; }); - keys = new Dictionary(); + keys = []; + map = new Dictionary(); } function process(prop) { @@ -12617,14 +12640,15 @@ merge(Compressor.prototype, { } if (can_hoist_property(prop)) { if (prop.value.has_side_effects(compressor)) flush(); - keys.add(key, prop); + keys.push(key); + map.add(key, prop); } else { flush(); values.push(prop); } if (found && !generated && typeof key == "string" && RE_POSITIVE_INTEGER.test(key)) { generated = true; - if (keys.has(key)) prop = keys.get(key)[0]; + if (map.has(key)) prop = map.get(key)[0]; prop.key = make_node(AST_Number, prop, { value: +key }); } } diff --git a/lib/output.js b/lib/output.js index d7927dbc567..5c8f5838ddc 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1465,7 +1465,7 @@ function OutputStream(options) { parent = output.parent(level++); if (parent instanceof AST_Call && parent.expression === node) return; } while (parent instanceof AST_PropAccess && parent.expression === node); - output.print(typeof self.pure == "string" ? "/*" + self.pure + "*/" : "/*@__PURE__*/"); + output.print("/*@__PURE__*/"); } function print_call_args(self, output) { if (self.expression instanceof AST_Call || self.expression instanceof AST_Lambda) { diff --git a/lib/parse.js b/lib/parse.js index 1b23dc2eec5..982d9d48cc4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -2316,9 +2316,8 @@ function parse($TEXT, options) { var comments = start.comments_before; var i = HOP(start, "comments_before_length") ? start.comments_before_length : comments.length; while (--i >= 0) { - var match = /[@#]__PURE__/.exec(comments[i].value); - if (match) { - expr.pure = match[0]; + if (/[@#]__PURE__/.test(comments[i].value)) { + expr.pure = true; break; } } diff --git a/lib/propmangle.js b/lib/propmangle.js index e7dc5097712..d5e13daa2fb 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -44,7 +44,7 @@ "use strict"; var builtins = function() { - var names = []; + var names = new Dictionary(); // NaN will be included due to Number.NaN [ "null", @@ -72,10 +72,10 @@ var builtins = function() { Object.getOwnPropertyNames(ctor.prototype).map(add); } }); - return makePredicate(names); + return names; function add(name) { - names.push(name); + names.set(name, true); } }(); @@ -116,9 +116,9 @@ function mangle_properties(ast, options) { reserved: null, }, true); - var reserved = Object.create(options.builtins ? null : builtins); + var reserved = options.builtins ? new Dictionary() : builtins.clone(); if (Array.isArray(options.reserved)) options.reserved.forEach(function(name) { - reserved[name] = true; + reserved.set(name, true); }); var cname = -1; @@ -126,7 +126,7 @@ function mangle_properties(ast, options) { if (options.cache) { cache = options.cache.props; cache.each(function(name) { - reserved[name] = true; + reserved.set(name, true); }); } else { cache = new Dictionary(); @@ -141,8 +141,8 @@ function mangle_properties(ast, options) { var debug_suffix; if (debug) debug_suffix = options.debug === true ? "" : options.debug; - var names_to_mangle = Object.create(null); - var unmangleable = Object.create(reserved); + var names_to_mangle = new Dictionary(); + var unmangleable = reserved.clone(); // step 1: find candidates to mangle ast.walk(new TreeWalker(function(node) { @@ -211,20 +211,20 @@ function mangle_properties(ast, options) { // only function declarations after this line function can_mangle(name) { - if (unmangleable[name]) return false; + if (unmangleable.has(name)) return false; if (/^-?[0-9]+(\.[0-9]+)?(e[+-][0-9]+)?$/.test(name)) return false; return true; } function should_mangle(name) { - if (reserved[name]) return false; + if (reserved.has(name)) return false; if (regex && !regex.test(name)) return false; - return cache.has(name) || names_to_mangle[name]; + return cache.has(name) || names_to_mangle.has(name); } function add(name) { - if (can_mangle(name)) names_to_mangle[name] = true; - if (!should_mangle(name)) unmangleable[name] = true; + if (can_mangle(name)) names_to_mangle.set(name, true); + if (!should_mangle(name)) unmangleable.set(name, true); } function mangle(name) { diff --git a/lib/scope.js b/lib/scope.js index 2219b89af20..89c66714d8a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -44,9 +44,9 @@ "use strict"; function SymbolDef(id, scope, orig, init) { + this._bits = 0; + this.defun = undefined; this.eliminated = 0; - this.exported = false; - this.global = false; this.id = id; this.init = init; this.mangled_name = null; @@ -54,8 +54,8 @@ function SymbolDef(id, scope, orig, init) { this.orig = [ orig ]; this.references = []; this.replaced = 0; + this.safe_ids = undefined; this.scope = scope; - this.undeclared = false; } SymbolDef.prototype = { @@ -104,6 +104,15 @@ SymbolDef.prototype = { }, }; +DEF_BITPROPS(SymbolDef, [ + "const_redefs", + "cross_loop", + "direct_access", + "exported", + "global", + "undeclared", +]); + var unary_side_effects = makePredicate("delete ++ --"); function is_lhs(node, parent) { @@ -363,7 +372,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) { if (node instanceof AST_SymbolLambda) { var def = node.thedef; if (!redefine(node, node.scope.parent_scope.resolve())) { - delete def.defun; + def.defun = undefined; } else if (typeof node.thedef.init !== "undefined") { node.thedef.init = false; } else if (def.init) { @@ -465,7 +474,7 @@ AST_Symbol.DEFMETHOD("mark_enclosed", function(options) { for (var s = this.scope; s; s = s.parent_scope) { push_uniq(s.enclosed, def); if (!options) { - delete s._var_names; + s._var_names = undefined; } else if (options.keep_fnames) { s.functions.each(function(d) { push_uniq(def.scope.enclosed, d); @@ -510,12 +519,12 @@ function names_in_use(scope, options) { if (!names) { scope.cname = -1; scope.cname_holes = []; - scope.names_in_use = names = Object.create(null); + scope.names_in_use = names = new Dictionary(); var cache = options.cache && options.cache.props; scope.enclosed.forEach(function(def) { - if (def.unmangleable(options)) names[def.name] = true; + if (def.unmangleable(options)) names.set(def.name, true); if (def.global && cache && cache.has(def.name)) { - names[cache.get(def.name)] = true; + names.set(cache.get(def.name), true); } }); } @@ -526,34 +535,33 @@ function next_mangled_name(def, options) { var scope = def.scope; var in_use = names_in_use(scope, options); var holes = scope.cname_holes; - var names = Object.create(null); + var names = new Dictionary(); var scopes = [ scope ]; def.forEach(function(sym) { var scope = sym.scope; do { - if (scopes.indexOf(scope) < 0) { - for (var name in names_in_use(scope, options)) { - names[name] = true; - } - scopes.push(scope); - } else break; + if (member(scope, scopes)) break; + names_in_use(scope, options).each(function(marker, name) { + names.set(name, marker); + }); + scopes.push(scope); } while (scope = scope.parent_scope); }); var name; for (var i = 0; i < holes.length; i++) { name = base54(holes[i]); - if (names[name]) continue; + if (names.has(name)) continue; holes.splice(i, 1); - in_use[name] = true; + in_use.set(name, true); return name; } while (true) { name = base54(++scope.cname); - if (in_use[name] || RESERVED_WORDS[name] || options.reserved.has[name]) continue; - if (!names[name]) break; + if (in_use.has(name) || RESERVED_WORDS[name] || options.reserved.has[name]) continue; + if (!names.has(name)) break; holes.push(scope.cname); } - in_use[name] = true; + in_use.set(name, true); return name; } @@ -598,7 +606,7 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options) { if (options.cache && options.cache.props) { var mangled_names = names_in_use(this, options); options.cache.props.each(function(mangled_name) { - mangled_names[mangled_name] = true; + mangled_names.set(mangled_name, true); }); } diff --git a/lib/sourcemap.js b/lib/sourcemap.js index 94966a6ac77..a230a44ced2 100644 --- a/lib/sourcemap.js +++ b/lib/sourcemap.js @@ -77,21 +77,23 @@ function vlq_encode(num) { } function create_array_map() { - var map = Object.create(null); + var map = new Dictionary(); var array = []; array.index = function(name) { - if (!HOP(map, name)) { - map[name] = array.length; + var index = map.get(name); + if (!(index >= 0)) { + index = array.length; array.push(name); + map.set(name, index); } - return map[name]; + return index; }; return array; } function SourceMap(options) { var sources = create_array_map(); - var sources_content = options.includeSources && Object.create(null); + var sources_content = options.includeSources && new Dictionary(); var names = create_array_map(); var mappings = ""; if (options.orig) Object.keys(options.orig).forEach(function(name) { @@ -110,7 +112,7 @@ function SourceMap(options) { if (!sources_content || !map.sourcesContent) return; for (var i = 0; i < map.sources.length; i++) { var content = map.sourcesContent[i]; - if (content) sources_content[map.sources[i]] = content; + if (content) sources_content.set(map.sources[i], content); } }); var prev_source; @@ -144,8 +146,8 @@ function SourceMap(options) { add(source, gen_line, gen_col, orig_line, orig_col, name); } : add, setSourceContent: sources_content ? function(source, content) { - if (!(source in sources_content)) { - sources_content[source] = content; + if (!sources_content.has(source)) { + sources_content.set(source, content); } } : noop, toString: function() { @@ -155,7 +157,7 @@ function SourceMap(options) { sourceRoot: options.root || undefined, sources: sources, sourcesContent: sources_content ? sources.map(function(source) { - return sources_content[source] || null; + return sources_content.get(source) || null; }) : undefined, names: names, mappings: mappings, diff --git a/lib/utils.js b/lib/utils.js index 69c2dcd18e5..57122575479 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -96,15 +96,6 @@ function defaults(args, defs, croak) { return defs; } -function merge(obj, ext) { - var count = 0; - for (var i in ext) if (HOP(ext, i)) { - obj[i] = ext[i]; - count++; - } - return count; -} - function noop() {} function return_false() { return false; } function return_true() { return true; } @@ -171,63 +162,80 @@ function all(array, predicate) { } function Dictionary() { - this._values = Object.create(null); - this._size = 0; + this.values = Object.create(null); } Dictionary.prototype = { set: function(key, val) { - if (!this.has(key)) ++this._size; - this._values["$" + key] = val; + if (key == "__proto__") { + this.proto_value = val; + } else { + this.values[key] = val; + } return this; }, add: function(key, val) { - if (this.has(key)) { - this.get(key).push(val); + var list = this.get(key); + if (list) { + list.push(val); } else { this.set(key, [ val ]); } return this; }, - get: function(key) { return this._values["$" + key] }, + get: function(key) { + return key == "__proto__" ? this.proto_value : this.values[key]; + }, del: function(key) { - if (this.has(key)) { - --this._size; - delete this._values["$" + key]; + if (key == "__proto__") { + delete this.proto_value; + } else { + delete this.values[key]; } return this; }, - has: function(key) { return ("$" + key) in this._values }, + has: function(key) { + return key == "__proto__" ? "proto_value" in this : key in this.values; + }, all: function(predicate) { - for (var i in this._values) - if (!predicate(this._values[i], i.substr(1))) - return false; + for (var i in this.values) + if (!predicate(this.values[i], i)) return false; + if ("proto_value" in this && !predicate(this.proto_value, "__proto__")) return false; return true; }, each: function(f) { - for (var i in this._values) - f(this._values[i], i.substr(1)); + for (var i in this.values) + f(this.values[i], i); + if ("proto_value" in this) f(this.proto_value, "__proto__"); }, size: function() { - return this._size; + return Object.keys(this.values).length + ("proto_value" in this); }, map: function(f) { var ret = []; - for (var i in this._values) - ret.push(f(this._values[i], i.substr(1))); + for (var i in this.values) + ret.push(f(this.values[i], i)); + if ("proto_value" in this) ret.push(f(this.proto_value, "__proto__")); return ret; }, clone: function() { var ret = new Dictionary(); - for (var i in this._values) - ret._values[i] = this._values[i]; - ret._size = this._size; + this.each(function(value, i) { + ret.set(i, value); + }); return ret; }, - toObject: function() { return this._values } + toObject: function() { + var obj = {}; + this.each(function(value, i) { + obj["$" + i] = value; + }); + return obj; + }, }; Dictionary.fromObject = function(obj) { var dict = new Dictionary(); - dict._size = merge(dict._values, obj); + for (var i in obj) + if (HOP(obj, i)) dict.set(i.slice(1), obj[i]); return dict; }; @@ -265,3 +273,21 @@ function first_in_statement(stack, arrow, export_default) { return false; } } + +function DEF_BITPROPS(ctor, props) { + if (props.length > 31) throw new Error("Too many properties: " + props.length + "\n" + props.join(", ")); + props.forEach(function(name, pos) { + var mask = 1 << pos; + Object.defineProperty(ctor.prototype, name, { + get: function() { + return !!(this._bits & mask); + }, + set: function(val) { + if (val) + this._bits |= mask; + else + this._bits &= ~mask; + }, + }); + }); +} diff --git a/package.json b/package.json index bccfd72c414..063638472e9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "JavaScript parser, mangler/compressor and beautifier toolkit", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "3.14.4", + "version": "3.14.5", "engines": { "node": ">=0.8.0" }, diff --git a/test/compress/annotations.js b/test/compress/annotations.js index e85c8d81918..4fdbbd8af5a 100644 --- a/test/compress/annotations.js +++ b/test/compress/annotations.js @@ -442,9 +442,9 @@ compress_annotations_disabled_output_annotations_enabled: { } expect_exact: [ "/*@__PURE__*/a(3),", - "/*#__PURE__*/b(5),", + "/*@__PURE__*/b(5),", "c(side_effect),", - "/*#__PURE__*/d(effect());", + "/*@__PURE__*/d(effect());", ] } diff --git a/test/compress/arrows.js b/test/compress/arrows.js index 5cbf56bc331..54886d58402 100644 --- a/test/compress/arrows.js +++ b/test/compress/arrows.js @@ -363,6 +363,28 @@ negate: { } inline_this: { + options = { + inline: true, + } + input: { + var p = "PASS"; + console.log({ + p: "FAIL", + q: (() => this.p)(), + }.q); + } + expect: { + var p = "PASS"; + console.log({ + p: "FAIL", + q: this.p, + }.q); + } + expect_stdout: "PASS" + node_version: ">=4" +} + +dont_inline_this: { options = { inline: true, } diff --git a/test/compress/awaits.js b/test/compress/awaits.js index 7d79fe15476..34479d4890b 100644 --- a/test/compress/awaits.js +++ b/test/compress/awaits.js @@ -340,6 +340,33 @@ inline_await_3_trim: { node_version: ">=8" } +inline_await_this: { + options = { + awaits: true, + inline: true, + } + input: { + var p = "FAIL"; + ({ + p: "PASS", + async f() { + return await (async () => this.p)(); + }, + }).f().then(console.log); + } + expect: { + var p = "FAIL"; + ({ + p: "PASS", + async f() { + return await this.p; + }, + }).f().then(console.log); + } + expect_stdout: "PASS" + node_version: ">=8" +} + await_unary: { options = { awaits: true, diff --git a/test/compress/bigint.js b/test/compress/bigint.js index b417268c3d3..aab50ca9bd8 100644 --- a/test/compress/bigint.js +++ b/test/compress/bigint.js @@ -4,7 +4,7 @@ arithmetic: { } expect_exact: "console.log((1n+0x2n)*(0o3n- -4n)>>5n-6n);" expect_stdout: "42n" - node_version: ">=10" + node_version: ">=10.4.0" } minus_dot: { @@ -13,7 +13,7 @@ minus_dot: { } expect_exact: "console.log(typeof-42n.toString(),typeof(-42n).toString());" expect_stdout: "number string" - node_version: ">=10" + node_version: ">=10.4.0" } evaluate: { @@ -28,7 +28,7 @@ evaluate: { console.log(0xdeadbeefn.toString(16)); } expect_stdout: "deadbeef" - node_version: ">=10" + node_version: ">=10.4.0" } Number: { @@ -42,7 +42,7 @@ Number: { console.log(+("" + -0xfeed_dead_beef_badn)); } expect_stdout: "-1148098955808013200" - node_version: ">=10" + node_version: ">=10.4.0" } issue_4590: { @@ -58,7 +58,7 @@ issue_4590: { 0n || console.log("PASS"); } expect_stdout: "PASS" - node_version: ">=10" + node_version: ">=10.4.0" } issue_4801: { @@ -88,5 +88,5 @@ issue_4801: { } } expect_stdout: "PASS" - node_version: ">=10" + node_version: ">=10.4.0" } diff --git a/test/compress/default-values.js b/test/compress/default-values.js index a7c0ed27832..260a35d4310 100644 --- a/test/compress/default-values.js +++ b/test/compress/default-values.js @@ -1037,7 +1037,7 @@ mangle_arrow_1: { }); } expect_stdout: "PASS" - node_version: ">=6" + node_version: ">=6.9.3" } mangle_arrow_1_toplevel: { @@ -1073,7 +1073,7 @@ mangle_arrow_1_toplevel: { }); } expect_stdout: "PASS" - node_version: ">=6" + node_version: ">=6.9.3" } mangle_arrow_2: { @@ -1109,7 +1109,7 @@ mangle_arrow_2: { }); } expect_stdout: "PASS" - node_version: ">=6" + node_version: ">=6.9.3" } mangle_arrow_2_toplevel: { @@ -1145,7 +1145,7 @@ mangle_arrow_2_toplevel: { }); } expect_stdout: "PASS" - node_version: ">=6" + node_version: ">=6.9.3" } issue_4444: { @@ -1587,7 +1587,7 @@ issue_4510_2: { }; } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } issue_4523: { diff --git a/test/compress/destructured.js b/test/compress/destructured.js index 04067484bf2..5acc7f69a4f 100644 --- a/test/compress/destructured.js +++ b/test/compress/destructured.js @@ -220,7 +220,7 @@ funarg_side_effects_2: { } } expect_stdout: "PASS" - node_version: ">=6" + node_version: ">=6.9.2" } funarg_side_effects_3: { @@ -254,7 +254,7 @@ funarg_side_effects_3: { } } expect_stdout: "PASS" - node_version: ">=6" + node_version: ">=6.9.2" } funarg_unused_1: { diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index ed58ce6fafe..c479a5348ea 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -3203,7 +3203,7 @@ issue_4552: { expect_stdout: "NaN" } -issue_4886: { +issue_4886_1: { options = { evaluate: true, unsafe: true, @@ -3222,3 +3222,23 @@ issue_4886: { } expect_stdout: "true" } + +issue_4886_2: { + options = { + evaluate: true, + unsafe: true, + } + input: { + console.log("foo" in { + "foo": null, + __proto__: 42, + }); + } + expect: { + console.log("foo" in { + "foo": null, + __proto__: 42, + }); + } + expect_stdout: "true" +} diff --git a/test/compress/exponentiation.js b/test/compress/exponentiation.js index 084e005bcfa..702add8c4af 100644 --- a/test/compress/exponentiation.js +++ b/test/compress/exponentiation.js @@ -62,7 +62,7 @@ assignment_2: { } expect_exact: "var a=8n;a**=a;console.log(a);" expect_stdout: "16777216n" - node_version: ">=10" + node_version: ">=10.4.0" } evaluate: { diff --git a/test/compress/objects.js b/test/compress/objects.js index 3c7f2d18260..3e110fe0877 100644 --- a/test/compress/objects.js +++ b/test/compress/objects.js @@ -521,3 +521,25 @@ issue_4415: { expect_stdout: "PASS" node_version: ">=4" } + +issue_5213: { + options = { + objects: true, + } + input: { + var a = "FAIL"; + console.log({ + p: a = "PASS", + 0: a, + p: null, + }[0]); + } + expect: { + var a = "FAIL"; + console.log({ + p: (a = "PASS", null), + 0: a, + }[0]); + } + expect_stdout: "PASS" +} diff --git a/test/compress/rests.js b/test/compress/rests.js index 1a557bcbe8c..f377f637662 100644 --- a/test/compress/rests.js +++ b/test/compress/rests.js @@ -51,7 +51,7 @@ arrow_destructured_object_1: { } expect_exact: "var f=({...a})=>a,o=f({PASS:42});for(var k in o)console.log(k,o[k]);" expect_stdout: "PASS 42" - node_version: ">=8" + node_version: ">=8.3.0" } arrow_destructured_object_2: { @@ -62,7 +62,7 @@ arrow_destructured_object_2: { } expect_exact: "var f=({FAIL:a,...b})=>b,o=f({PASS:42,FAIL:null});for(var k in o)console.log(k,o[k]);" expect_stdout: "PASS 42" - node_version: ">=8" + node_version: ">=8.3.0" } arrow_destructured_object_3: { @@ -79,7 +79,7 @@ arrow_destructured_object_3: { "2 S", "3 S", ] - node_version: ">=8" + node_version: ">=8.3.0" } funarg_1: { @@ -131,7 +131,7 @@ destructured_object_1: { } expect_exact: 'var{...a}=["FAIL","PASS",42];console.log(a[1],a[2]);' expect_stdout: "PASS 42" - node_version: ">=8" + node_version: ">=8.3.0" } destructured_object_2: { @@ -141,7 +141,7 @@ destructured_object_2: { } expect_exact: 'var{0:a,...b}=["FAIL","PASS",42];console.log(b[1],b[2]);' expect_stdout: "PASS 42" - node_version: ">=8" + node_version: ">=8.3.0" } drop_fargs: { @@ -231,7 +231,7 @@ reduce_destructured_object: { console.log(a[0]); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } retain_destructured_array: { @@ -270,7 +270,7 @@ retain_destructured_object_1: { "1 PASS", "2 42", ] - node_version: ">=8" + node_version: ">=8.3.0" } retain_destructured_object_2: { @@ -292,7 +292,7 @@ retain_destructured_object_2: { "bar PASS", "baz 42", ] - node_version: ">=8" + node_version: ">=8.3.0" } retain_funarg_destructured_array_1: { @@ -344,7 +344,7 @@ retain_funarg_destructured_object_1: { console.log((({ ...a }) => a)([ "PASS" ])[0]); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } retain_funarg_destructured_object_2: { @@ -362,7 +362,7 @@ retain_funarg_destructured_object_2: { }({ p: "FAIL" }).p || "PASS"); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } drop_unused_call_args_1: { @@ -482,7 +482,7 @@ merge_funarg_destructured_object: { })([ "PASS" ]); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } keep_arguments: { @@ -992,7 +992,7 @@ issue_5089_1: { console.log(o.p); } expect_stdout: "undefined" - node_version: ">=8" + node_version: ">=8.3.0" } issue_5089_2: { @@ -1019,7 +1019,7 @@ issue_5089_2: { console.log(o.p); } expect_stdout: "undefined" - node_version: ">=8" + node_version: ">=8.3.0" } issue_5100_1: { @@ -1054,7 +1054,7 @@ issue_5100_1: { console.log(a.r); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } issue_5100_2: { @@ -1085,7 +1085,7 @@ issue_5100_2: { } ][0]); } expect_stdout: "PASS" - node_version: ">=10" + node_version: ">=10.22.0" } issue_5108: { diff --git a/test/compress/spreads.js b/test/compress/spreads.js index 3b1ec4615c7..8c34a0daad7 100644 --- a/test/compress/spreads.js +++ b/test/compress/spreads.js @@ -341,7 +341,7 @@ convert_setter: { console.log(k, o[k]); } expect_stdout: "PASS undefined" - node_version: ">=8" + node_version: ">=8.3.0" } keep_getter_1: { @@ -370,7 +370,7 @@ keep_getter_1: { }); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } keep_getter_2: { @@ -399,7 +399,7 @@ keep_getter_2: { "foo", "bar", ] - node_version: ">=8" + node_version: ">=8.3.0" } keep_getter_3: { @@ -429,7 +429,7 @@ keep_getter_3: { }); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } keep_getter_4: { @@ -460,7 +460,7 @@ keep_getter_4: { }); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } keep_accessor: { @@ -508,7 +508,7 @@ keep_accessor: { "q undefined", "r null", ] - node_version: ">=8" + node_version: ">=8.3.0" } object_key_order_1: { @@ -538,7 +538,7 @@ object_key_order_1: { "a 3", "b 2", ] - node_version: ">=8 <=10" + node_version: ">=8.3.0 <=10" } object_key_order_2: { @@ -568,7 +568,7 @@ object_key_order_2: { "a 3", "b 2", ] - node_version: ">=8" + node_version: ">=8.3.0" } object_key_order_3: { @@ -598,7 +598,7 @@ object_key_order_3: { "a 3", "b 2", ] - node_version: ">=8" + node_version: ">=8.3.0" } object_key_order_4: { @@ -628,7 +628,7 @@ object_key_order_4: { "a 3", "b 2", ] - node_version: ">=8" + node_version: ">=8.3.0" } object_spread_array: { @@ -654,7 +654,7 @@ object_spread_array: { "0 foo", "1 bar", ] - node_version: ">=8" + node_version: ">=8.3.0" } object_spread_string: { @@ -681,7 +681,7 @@ object_spread_string: { "1 o", "2 o", ] - node_version: ">=8" + node_version: ">=8.3.0" } unused_var_side_effects: { @@ -711,7 +711,7 @@ unused_var_side_effects: { }); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } unsafe_join_1: { @@ -793,7 +793,7 @@ issue_4329: { }[0]); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } issue_4331: { @@ -871,7 +871,7 @@ issue_4345: { }[42]); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } issue_4361: { @@ -901,7 +901,7 @@ issue_4361: { "foo", "undefined", ] - node_version: ">=8" + node_version: ">=8.3.0" } issue_4363: { @@ -922,7 +922,7 @@ issue_4363: { }); } expect_stdout: "PASS" - node_version: ">=8" + node_version: ">=8.3.0" } issue_4556: { @@ -994,7 +994,7 @@ issue_4849: { }())); } expect_stdout: "object" - node_version: ">=8" + node_version: ">=8.3.0" } issue_4882_1: { @@ -1026,7 +1026,7 @@ issue_4882_1: { "PASS", "undefined", ] - node_version: ">=8" + node_version: ">=8.3.0" } issue_4882_2: { @@ -1052,7 +1052,7 @@ issue_4882_2: { "42", "PASS", ] - node_version: ">=8" + node_version: ">=8.3.0" } issue_4882_3: { @@ -1082,7 +1082,7 @@ issue_4882_3: { "PASS", "42", ] - node_version: ">=8" + node_version: ">=8.3.0" } issue_5006: { diff --git a/test/compress/templates.js b/test/compress/templates.js index a50470dfa1e..db3ab203c96 100644 --- a/test/compress/templates.js +++ b/test/compress/templates.js @@ -337,7 +337,7 @@ malformed_evaluate_4: { console.log("\\u00b5"); } expect_stdout: "\\u00b5" - node_version: ">=8" + node_version: ">=8.10.0" } unsafe_evaluate: { @@ -353,7 +353,7 @@ unsafe_evaluate: { console.log("\\uFo"); } expect_stdout: "\\uFo" - node_version: ">=8" + node_version: ">=8.10.0" } side_effects_1: { diff --git a/test/compress/unicode.js b/test/compress/unicode.js index b08b22a9b9e..abbd417df5b 100644 --- a/test/compress/unicode.js +++ b/test/compress/unicode.js @@ -67,7 +67,8 @@ unicode_escaped_identifier_2: { } expect_exact: 'var a="foo";var \u{10000}="bar";console.log(a,\u{10000});' expect_stdout: "foo bar" - node_version: ">=4" + // non-BMP support is platform-dependent on Node.js v4 + node_version: ">=6" } unicode_identifier_ascii_only: { @@ -200,7 +201,8 @@ surrogate_pair: { } expect_exact: 'var \ud87e\udc00={"\ud87e\udc01":"\udbc0\udc00"};\ud87e\udc00.\ud87e\udc02="\udbc0\udc01";console.log(typeof \ud87e\udc00,\ud87e\udc00.\ud87e\udc01,\ud87e\udc00["\ud87e\udc02"]);' expect_stdout: "object \udbc0\udc00 \udbc0\udc01" - node_version: ">=4" + // non-BMP support is platform-dependent on Node.js v4 + node_version: ">=6" } surrogate_pair_ascii: { @@ -216,5 +218,6 @@ surrogate_pair_ascii: { } expect_exact: 'var \\u{2f800}={"\\ud87e\\udc01":"\\udbc0\\udc00"};\\u{2f800}.\\u{2f802}="\\udbc0\\udc01";console.log(typeof \\u{2f800},\\u{2f800}.\\u{2f801},\\u{2f800}["\\ud87e\\udc02"]);' expect_stdout: "object \udbc0\udc00 \udbc0\udc01" - node_version: ">=4" + // non-BMP support is platform-dependent on Node.js v4 + node_version: ">=6" } diff --git a/test/compress/yields.js b/test/compress/yields.js index d10321e272c..f0ffded3e12 100644 --- a/test/compress/yields.js +++ b/test/compress/yields.js @@ -970,7 +970,7 @@ issue_4639_1: { }().next().value()); } expect_stdout: "PASS" - node_version: ">=4" + node_version: ">=4 <7 || >=8.7.0" } issue_4639_2: { diff --git a/test/sandbox.js b/test/sandbox.js index 355b9c0a56f..bc02114f87c 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -252,8 +252,11 @@ function run_code_vm(code, toplevel, timeout) { var ctx = vm.createContext({ console: console }); // for Node.js v6 vm.runInContext(setup_code, ctx); - vm.runInContext(toplevel ? "(function(){" + code + "})();" : code, ctx, { timeout: timeout }); - return strip_color_codes(stdout); + vm.runInContext(toplevel ? "(function(){\n" + code + "\n})();" : code, ctx, { timeout: timeout }); + // for Node.js v4 + return strip_color_codes(stdout.replace(/\b(Array \[|Object {)/g, function(match) { + return match.slice(-1); + })); } catch (ex) { return ex; } finally { @@ -263,7 +266,7 @@ function run_code_vm(code, toplevel, timeout) { function run_code_exec(code, toplevel, timeout) { if (toplevel) { - code = setup_code + "(function(){" + code + "})();"; + code = setup_code + "(function(){\n" + code + "\n})();"; } else { code = code.replace(/^((["'])[^"']*\2(;|$))?/, function(directive) { return directive + setup_code; @@ -292,7 +295,7 @@ function run_code_exec(code, toplevel, timeout) { } catch (e) {} } var match = /\n([^:\s]*Error)(?:: ([\s\S]+?))?\n( at [\s\S]+)\n$/.exec(msg); - if (!match) return details; + if (!match) return details || new Error("Script execution aborted."); var ex = new global[match[1]](match[2]); ex.stack = ex.stack.slice(0, ex.stack.indexOf(" at ")) + match[3]; if (typeof details == "object") { diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index f8812806b4e..8747418d4d2 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -2469,8 +2469,10 @@ for (var round = 1; round <= num_iterations; round++) { println("original result:"); println(result); println(); - // ignore v8 parser bug - return bug_async_arrow_rest(result); + // ignore v8 parser bug + return bug_async_arrow_rest(result) + // ignore runtime platform bugs + || result.message == "Script execution aborted."; })) continue; minify_options.forEach(function(options) { var o = JSON.parse(options); @@ -2485,6 +2487,8 @@ for (var round = 1; round <= num_iterations; round++) { ok = sandbox.same_stdout(original_result, uglify_result); // ignore v8 parser bug if (!ok && bug_async_arrow_rest(uglify_result)) ok = true; + // ignore runtime platform bugs + if (!ok && uglify_result.message == "Script execution aborted.") ok = true; // handle difference caused by time-outs if (!ok && errored && is_error_timeout(original_result)) { if (is_error_timeout(uglify_result)) {