From b0799105c224926da5ca0b6a6dfa18270e83afd0 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 5 Dec 2021 06:58:52 +0000 Subject: [PATCH] improve `Dictionary` performance (#5202) - workaround `__proto__` quirks on v8 --- bin/uglifyjs | 2 +- lib/compress.js | 297 +++++++++++++++++++------------------- lib/propmangle.js | 26 ++-- lib/scope.js | 31 ++-- lib/sourcemap.js | 20 +-- lib/utils.js | 74 +++++----- test/compress/evaluate.js | 22 ++- test/compress/objects.js | 4 +- 8 files changed, 255 insertions(+), 221 deletions(-) diff --git a/bin/uglifyjs b/bin/uglifyjs index 08e3d495..6ee968f8 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -519,7 +519,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 diff --git a/lib/compress.js b/lib/compress.js index f7232e9a..1a1d969f 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) { @@ -1872,9 +1871,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) { @@ -2403,7 +2402,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 +2417,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 +2451,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 +2543,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 +2759,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 +2791,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 +3000,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 +3625,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 +3646,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 +4617,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 +4628,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; } @@ -7268,7 +7271,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 +7279,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 +7288,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 +7300,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 +7310,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 +7321,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 +7350,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; } @@ -7443,7 +7446,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 +7472,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 +7491,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 +7653,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 +7677,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 +7686,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; }); @@ -9142,7 +9145,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 +9161,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, }); @@ -9959,30 +9962,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 (!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 +10003,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 +10031,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,7 +10080,7 @@ 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); @@ -10153,7 +10158,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 +10168,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 +10201,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 +11325,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; diff --git a/lib/propmangle.js b/lib/propmangle.js index e7dc5097..d5e13daa 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 2219b89a..a6026e59 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -510,12 +510,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 +526,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 +597,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 94966a6a..a230a44c 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 69c2dcd1..4c465844 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; }; diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index ed58ce6f..c479a534 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/objects.js b/test/compress/objects.js index 3c7f2d18..beec4bfd 100644 --- a/test/compress/objects.js +++ b/test/compress/objects.js @@ -198,9 +198,9 @@ numeric_literal: { expect_exact: [ 'var obj = {', ' 0: 0,', - ' "-0": 1,', - ' 42: 3,', ' 37: 4,', + ' 42: 3,', + ' "-0": 1,', ' o: 5,', ' 1e42: 8,', ' b: 7',