UNPKG

jointjs

Version:

JavaScript diagramming library

1,470 lines (1,364 loc) 296 kB
/*********************************************************************** A JavaScript tokenizer / parser / beautifier / compressor. https://github.com/mishoo/UglifyJS2 -------------------------------- (C) --------------------------------- Author: Mihai Bazon <mihai.bazon@gmail.com> http://mihai.bazon.net/blog Distributed under the BSD license: Copyright 2012 (c) Mihai Bazon <mihai.bazon@gmail.com> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ***********************************************************************/ "use strict"; import { defaults, HOP, keep_name, make_node, makePredicate, map_add, MAP, member, noop, remove, return_false, return_null, return_this, return_true, string_template, regexp_source_fix, has_annotation } from "../utils/index.js"; import { first_in_statement, } from "../utils/first_in_statement.js"; import { AST_Accessor, AST_Array, AST_Arrow, AST_Assign, AST_Await, AST_BigInt, AST_Binary, AST_Block, AST_BlockStatement, AST_Boolean, AST_Break, AST_Call, AST_Case, AST_Catch, AST_Chain, AST_Class, AST_ClassExpression, AST_ClassProperty, AST_ConciseMethod, AST_Conditional, AST_Const, AST_Constant, AST_Continue, AST_Debugger, AST_Default, AST_DefaultAssign, AST_DefClass, AST_Definitions, AST_Defun, AST_Destructuring, AST_Directive, AST_Do, AST_Dot, AST_DWLoop, AST_EmptyStatement, AST_Exit, AST_Expansion, AST_Export, AST_False, AST_Finally, AST_For, AST_ForIn, AST_Function, AST_Hole, AST_If, AST_Import, AST_Infinity, AST_IterationStatement, AST_Jump, AST_LabeledStatement, AST_Lambda, AST_Let, AST_LoopControl, AST_NaN, AST_New, AST_Node, AST_Null, AST_Number, AST_Object, AST_ObjectGetter, AST_ObjectKeyVal, AST_ObjectProperty, AST_ObjectSetter, AST_PrefixedTemplateString, AST_PropAccess, AST_RegExp, AST_Return, AST_Scope, AST_Sequence, AST_SimpleStatement, AST_Statement, AST_String, AST_Sub, AST_Switch, AST_SwitchBranch, AST_Symbol, AST_SymbolBlockDeclaration, AST_SymbolCatch, AST_SymbolClassProperty, AST_SymbolConst, AST_SymbolDeclaration, AST_SymbolDefun, AST_SymbolExport, AST_SymbolFunarg, AST_SymbolLambda, AST_SymbolLet, AST_SymbolMethod, AST_SymbolRef, AST_SymbolVar, AST_TemplateSegment, AST_TemplateString, AST_This, AST_Toplevel, AST_True, AST_Try, AST_Unary, AST_UnaryPostfix, AST_UnaryPrefix, AST_Undefined, AST_Var, AST_VarDef, AST_While, AST_With, AST_Yield, TreeTransformer, TreeWalker, walk, walk_abort, walk_body, walk_parent, _INLINE, _NOINLINE, _PURE } from "../ast.js"; import { equivalent_to } from "../equivalent-to.js"; import { is_basic_identifier_string, JS_Parse_Error, parse, PRECEDENCE, } from "../parse.js"; import { OutputStream } from "../output.js"; import { base54, SymbolDef, } from "../scope.js"; import "../size.js"; const UNUSED = 0b00000001; const TRUTHY = 0b00000010; const FALSY = 0b00000100; const UNDEFINED = 0b00001000; const INLINED = 0b00010000; // Nodes to which values are ever written. Used when keep_assign is part of the unused option string. const WRITE_ONLY= 0b00100000; // information specific to a single compression pass const SQUEEZED = 0b0000000100000000; const OPTIMIZED = 0b0000001000000000; const TOP = 0b0000010000000000; const CLEAR_BETWEEN_PASSES = SQUEEZED | OPTIMIZED | TOP; /*@__INLINE__*/ const has_flag = (node, flag) => node.flags & flag; /*@__INLINE__*/ const set_flag = (node, flag) => { node.flags |= flag; }; /*@__INLINE__*/ const clear_flag = (node, flag) => { node.flags &= ~flag; }; class Compressor extends TreeWalker { constructor(options, { false_by_default = false, mangle_options = false }) { super(); if (options.defaults !== undefined && !options.defaults) false_by_default = true; this.options = defaults(options, { arguments : false, arrows : !false_by_default, booleans : !false_by_default, booleans_as_integers : false, collapse_vars : !false_by_default, comparisons : !false_by_default, computed_props: !false_by_default, conditionals : !false_by_default, dead_code : !false_by_default, defaults : true, directives : !false_by_default, drop_console : false, drop_debugger : !false_by_default, ecma : 5, evaluate : !false_by_default, expression : false, global_defs : false, hoist_funs : false, hoist_props : !false_by_default, hoist_vars : false, ie8 : false, if_return : !false_by_default, inline : !false_by_default, join_vars : !false_by_default, keep_classnames: false, keep_fargs : true, keep_fnames : false, keep_infinity : false, loops : !false_by_default, module : false, negate_iife : !false_by_default, passes : 1, properties : !false_by_default, pure_getters : !false_by_default && "strict", pure_funcs : null, reduce_funcs : null, // legacy reduce_vars : !false_by_default, sequences : !false_by_default, side_effects : !false_by_default, switches : !false_by_default, top_retain : null, toplevel : !!(options && options["top_retain"]), typeofs : !false_by_default, unsafe : false, unsafe_arrows : false, unsafe_comps : false, unsafe_Function: false, unsafe_math : false, unsafe_symbols: false, unsafe_methods: false, unsafe_proto : false, unsafe_regexp : false, unsafe_undefined: false, unused : !false_by_default, warnings : false // legacy }, true); var global_defs = this.options["global_defs"]; if (typeof global_defs == "object") for (var key in global_defs) { if (key[0] === "@" && HOP(global_defs, key)) { global_defs[key.slice(1)] = parse(global_defs[key], { expression: true }); } } if (this.options["inline"] === true) this.options["inline"] = 3; var pure_funcs = this.options["pure_funcs"]; if (typeof pure_funcs == "function") { this.pure_funcs = pure_funcs; } else { this.pure_funcs = pure_funcs ? function(node) { return !pure_funcs.includes(node.expression.print_to_string()); } : return_true; } var top_retain = this.options["top_retain"]; if (top_retain instanceof RegExp) { this.top_retain = function(def) { return top_retain.test(def.name); }; } else if (typeof top_retain == "function") { this.top_retain = top_retain; } else if (top_retain) { if (typeof top_retain == "string") { top_retain = top_retain.split(/,/); } this.top_retain = function(def) { return top_retain.includes(def.name); }; } if (this.options["module"]) { this.directives["use strict"] = true; this.options["toplevel"] = true; } var toplevel = this.options["toplevel"]; this.toplevel = typeof toplevel == "string" ? { funcs: /funcs/.test(toplevel), vars: /vars/.test(toplevel) } : { funcs: toplevel, vars: toplevel }; var sequences = this.options["sequences"]; this.sequences_limit = sequences == 1 ? 800 : sequences | 0; this.evaluated_regexps = new Map(); this._toplevel = undefined; this.mangle_options = mangle_options; } option(key) { return this.options[key]; } exposed(def) { if (def.export) return true; if (def.global) for (var i = 0, len = def.orig.length; i < len; i++) if (!this.toplevel[def.orig[i] instanceof AST_SymbolDefun ? "funcs" : "vars"]) return true; return false; } in_boolean_context() { if (!this.option("booleans")) return false; var self = this.self(); for (var i = 0, p; p = this.parent(i); i++) { if (p instanceof AST_SimpleStatement || p instanceof AST_Conditional && p.condition === self || p instanceof AST_DWLoop && p.condition === self || p instanceof AST_For && p.condition === self || p instanceof AST_If && p.condition === self || p instanceof AST_UnaryPrefix && p.operator == "!" && p.expression === self) { return true; } if ( p instanceof AST_Binary && ( p.operator == "&&" || p.operator == "||" || p.operator == "??" ) || p instanceof AST_Conditional || p.tail_node() === self ) { self = p; } else { return false; } } } get_toplevel() { return this._toplevel; } compress(toplevel) { toplevel = toplevel.resolve_defines(this); this._toplevel = toplevel; if (this.option("expression")) { this._toplevel.process_expression(true); } var passes = +this.options.passes || 1; var min_count = 1 / 0; var stopping = false; var mangle = { ie8: this.option("ie8") }; for (var pass = 0; pass < passes; pass++) { this._toplevel.figure_out_scope(mangle); if (pass === 0 && this.option("drop_console")) { // must be run before reduce_vars and compress pass this._toplevel = this._toplevel.drop_console(); } if (pass > 0 || this.option("reduce_vars")) { this._toplevel.reset_opt_flags(this); } this._toplevel = this._toplevel.transform(this); if (passes > 1) { let count = 0; walk(this._toplevel, () => { count++; }); if (count < min_count) { min_count = count; stopping = false; } else if (stopping) { break; } else { stopping = true; } } } if (this.option("expression")) { this._toplevel.process_expression(false); } toplevel = this._toplevel; this._toplevel = undefined; return toplevel; } before(node, descend) { if (has_flag(node, SQUEEZED)) return node; var was_scope = false; if (node instanceof AST_Scope) { node = node.hoist_properties(this); node = node.hoist_declarations(this); was_scope = true; } // Before https://github.com/mishoo/UglifyJS2/pull/1602 AST_Node.optimize() // would call AST_Node.transform() if a different instance of AST_Node is // produced after def_optimize(). // 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 (was_scope && opt instanceof AST_Scope) { opt.drop_unused(this); descend(opt, this); } if (opt === node) set_flag(opt, SQUEEZED); return opt; } } function def_optimize(node, optimizer) { node.DEFMETHOD("optimize", function(compressor) { var self = this; if (has_flag(self, OPTIMIZED)) return self; if (compressor.has_directive("use asm")) return self; var opt = optimizer(self, compressor); set_flag(opt, OPTIMIZED); return opt; }); } def_optimize(AST_Node, function(self) { return self; }); AST_Toplevel.DEFMETHOD("drop_console", function() { return this.transform(new TreeTransformer(function(self) { if (self.TYPE == "Call") { var exp = self.expression; if (exp instanceof AST_PropAccess) { var name = exp.expression; while (name.expression) { name = name.expression; } if (is_undeclared_ref(name) && name.name == "console") { return make_node(AST_Undefined, self); } } } })); }); AST_Node.DEFMETHOD("equivalent_to", function(node) { return equivalent_to(this, node); }); AST_Scope.DEFMETHOD("process_expression", function(insert, compressor) { var self = this; var tt = new TreeTransformer(function(node) { if (insert && node instanceof AST_SimpleStatement) { return make_node(AST_Return, node, { value: node.body }); } if (!insert && node instanceof AST_Return) { if (compressor) { var value = node.value && node.value.drop_side_effect_free(compressor, true); return value ? make_node(AST_SimpleStatement, node, { body: value }) : make_node(AST_EmptyStatement, node); } return make_node(AST_SimpleStatement, node, { body: node.value || make_node(AST_UnaryPrefix, node, { operator: "void", expression: make_node(AST_Number, node, { value: 0 }) }) }); } if (node instanceof AST_Class || node instanceof AST_Lambda && node !== self) { return node; } if (node instanceof AST_Block) { var index = node.body.length - 1; if (index >= 0) { node.body[index] = node.body[index].transform(tt); } } else if (node instanceof AST_If) { node.body = node.body.transform(tt); if (node.alternative) { node.alternative = node.alternative.transform(tt); } } else if (node instanceof AST_With) { node.body = node.body.transform(tt); } return node; }); self.transform(tt); }); function read_property(obj, key) { key = get_value(key); if (key instanceof AST_Node) return; var value; if (obj instanceof AST_Array) { var elements = obj.elements; if (key == "length") return make_node_from_constant(elements.length, obj); if (typeof key == "number" && key in elements) value = elements[key]; } else if (obj instanceof AST_Object) { key = "" + key; var props = obj.properties; for (var i = props.length; --i >= 0;) { var prop = props[i]; if (!(prop instanceof AST_ObjectKeyVal)) return; if (!value && props[i].key === key) value = props[i].value; } } return value instanceof AST_SymbolRef && value.fixed_value() || value; } function is_modified(compressor, tw, node, value, level, immutable) { var parent = tw.parent(level); var lhs = is_lhs(node, parent); if (lhs) return lhs; if (!immutable && parent instanceof AST_Call && parent.expression === node && !(value instanceof AST_Arrow) && !(value instanceof AST_Class) && !parent.is_expr_pure(compressor) && (!(value instanceof AST_Function) || !(parent instanceof AST_New) && value.contains_this())) { return true; } if (parent instanceof AST_Array) { return is_modified(compressor, tw, parent, parent, level + 1); } if (parent instanceof AST_ObjectKeyVal && node === parent.value) { var obj = tw.parent(level + 1); return is_modified(compressor, tw, obj, obj, level + 2); } if (parent instanceof AST_PropAccess && parent.expression === node) { var prop = read_property(value, parent.property); return !immutable && is_modified(compressor, tw, parent, prop, level + 1); } } (function(def_reduce_vars) { def_reduce_vars(AST_Node, noop); function reset_def(compressor, def) { def.assignments = 0; def.chained = false; def.direct_access = false; def.escaped = 0; def.recursive_refs = 0; def.references = []; def.single_use = undefined; if (def.scope.pinned()) { def.fixed = false; } else if (def.orig[0] instanceof AST_SymbolConst || !compressor.exposed(def)) { def.fixed = def.init; } else { def.fixed = false; } } function reset_variables(tw, compressor, node) { node.variables.forEach(function(def) { reset_def(compressor, def); if (def.fixed === null) { tw.defs_to_safe_ids.set(def.id, tw.safe_ids); mark(tw, def, true); } else if (def.fixed) { tw.loop_ids.set(def.id, tw.in_loop); mark(tw, def, true); } }); } function reset_block_variables(compressor, node) { if (node.block_scope) node.block_scope.variables.forEach((def) => { reset_def(compressor, def); }); } function push(tw) { tw.safe_ids = Object.create(tw.safe_ids); } function pop(tw) { tw.safe_ids = Object.getPrototypeOf(tw.safe_ids); } function mark(tw, def, safe) { tw.safe_ids[def.id] = safe; } function safe_to_read(tw, def) { if (def.single_use == "m") return false; if (tw.safe_ids[def.id]) { if (def.fixed == null) { var orig = def.orig[0]; if (orig instanceof AST_SymbolFunarg || orig.name == "arguments") return false; def.fixed = make_node(AST_Undefined, orig); } return true; } return def.fixed instanceof AST_Defun; } function safe_to_assign(tw, def, scope, value) { if (def.fixed === undefined) return true; let def_safe_ids; if (def.fixed === null && (def_safe_ids = tw.defs_to_safe_ids.get(def.id)) ) { def_safe_ids[def.id] = false; tw.defs_to_safe_ids.delete(def.id); return true; } if (!HOP(tw.safe_ids, def.id)) return false; if (!safe_to_read(tw, def)) return false; if (def.fixed === false) return false; if (def.fixed != null && (!value || def.references.length > def.assignments)) return false; if (def.fixed instanceof AST_Defun) { return value instanceof AST_Node && def.fixed.parent_scope === scope; } return def.orig.every((sym) => { return !(sym instanceof AST_SymbolConst || sym instanceof AST_SymbolDefun || sym instanceof AST_SymbolLambda); }); } function ref_once(tw, compressor, def) { return compressor.option("unused") && !def.scope.pinned() && def.references.length - def.recursive_refs == 1 && tw.loop_ids.get(def.id) === tw.in_loop; } function is_immutable(value) { if (!value) return false; return value.is_constant() || value instanceof AST_Lambda || value instanceof AST_This; } // A definition "escapes" when its value can leave the point of use. // Example: `a = b || c` // In this example, "b" and "c" are escaping, because they're going into "a" // // def.escaped is != 0 when it escapes. // // When greater than 1, it means that N chained properties will be read off // of that def before an escape occurs. This is useful for evaluating // property accesses, where you need to know when to stop. function mark_escaped(tw, d, scope, node, value, level = 0, depth = 1) { var parent = tw.parent(level); if (value) { if (value.is_constant()) return; if (value instanceof AST_ClassExpression) return; } if ( parent instanceof AST_Assign && (parent.operator === "=" || parent.logical) && node === parent.right || parent instanceof AST_Call && (node !== parent.expression || parent instanceof AST_New) || parent instanceof AST_Exit && node === parent.value && node.scope !== d.scope || parent instanceof AST_VarDef && node === parent.value || parent instanceof AST_Yield && node === parent.value && node.scope !== d.scope ) { if (depth > 1 && !(value && value.is_constant_expression(scope))) depth = 1; if (!d.escaped || d.escaped > depth) d.escaped = depth; return; } else if ( parent instanceof AST_Array || parent instanceof AST_Await || parent instanceof AST_Binary && lazy_op.has(parent.operator) || parent instanceof AST_Conditional && node !== parent.condition || parent instanceof AST_Expansion || parent instanceof AST_Sequence && node === parent.tail_node() ) { mark_escaped(tw, d, scope, parent, parent, level + 1, depth); } else if (parent instanceof AST_ObjectKeyVal && node === parent.value) { var obj = tw.parent(level + 1); mark_escaped(tw, d, scope, obj, obj, level + 2, depth); } else if (parent instanceof AST_PropAccess && node === parent.expression) { value = read_property(value, parent.property); mark_escaped(tw, d, scope, parent, value, level + 1, depth + 1); if (value) return; } if (level > 0) return; if (parent instanceof AST_Sequence && node !== parent.tail_node()) return; if (parent instanceof AST_SimpleStatement) return; d.direct_access = true; } const suppress = node => walk(node, node => { if (!(node instanceof AST_Symbol)) return; var d = node.definition(); if (!d) return; if (node instanceof AST_SymbolRef) d.references.push(node); d.fixed = false; }); def_reduce_vars(AST_Accessor, function(tw, descend, compressor) { push(tw); reset_variables(tw, compressor, this); descend(); pop(tw); return true; }); def_reduce_vars(AST_Assign, function(tw, descend, compressor) { var node = this; if (node.left instanceof AST_Destructuring) { suppress(node.left); return; } const finish_walk = () => { if (node.logical) { node.left.walk(tw); push(tw); node.right.walk(tw); pop(tw); return true; } }; var sym = node.left; if (!(sym instanceof AST_SymbolRef)) return finish_walk(); var def = sym.definition(); var safe = safe_to_assign(tw, def, sym.scope, node.right); def.assignments++; if (!safe) return finish_walk(); var fixed = def.fixed; if (!fixed && node.operator != "=" && !node.logical) return finish_walk(); var eq = node.operator == "="; var value = eq ? node.right : node; if (is_modified(compressor, tw, node, value, 0)) return finish_walk(); def.references.push(sym); if (!node.logical) { if (!eq) def.chained = true; def.fixed = eq ? function() { return node.right; } : function() { return make_node(AST_Binary, node, { operator: node.operator.slice(0, -1), left: fixed instanceof AST_Node ? fixed : fixed(), right: node.right }); }; } if (node.logical) { mark(tw, def, false); push(tw); node.right.walk(tw); pop(tw); return true; } mark(tw, def, false); node.right.walk(tw); mark(tw, def, true); mark_escaped(tw, def, sym.scope, node, value, 0, 1); return true; }); def_reduce_vars(AST_Binary, function(tw) { if (!lazy_op.has(this.operator)) return; this.left.walk(tw); push(tw); this.right.walk(tw); pop(tw); return true; }); def_reduce_vars(AST_Block, function(tw, descend, compressor) { reset_block_variables(compressor, this); }); def_reduce_vars(AST_Case, function(tw) { push(tw); this.expression.walk(tw); pop(tw); push(tw); walk_body(this, tw); pop(tw); return true; }); def_reduce_vars(AST_Class, function(tw, descend) { clear_flag(this, INLINED); push(tw); descend(); pop(tw); return true; }); def_reduce_vars(AST_Conditional, function(tw) { this.condition.walk(tw); push(tw); this.consequent.walk(tw); pop(tw); push(tw); this.alternative.walk(tw); pop(tw); return true; }); def_reduce_vars(AST_Chain, function(tw, descend) { // Chains' conditions apply left-to-right, cumulatively. // If we walk normally we don't go in that order because we would pop before pushing again // Solution: AST_PropAccess and AST_Call push when they are optional, and never pop. // Then we pop everything when they are done being walked. const safe_ids = tw.safe_ids; descend(); // Unroll back to start tw.safe_ids = safe_ids; return true; }); def_reduce_vars(AST_Call, function (tw) { // TODO this block should just be { return } but // for some reason the _walk function of AST_Call walks the callee last this.expression.walk(tw); if (this.optional) { // Never pop -- it's popped at AST_Chain above push(tw); } for (const arg of this.args) arg.walk(tw); return true; }); def_reduce_vars(AST_PropAccess, function (tw) { if (!this.optional) return; this.expression.walk(tw); // Never pop -- it's popped at AST_Chain above push(tw); if (this.property instanceof AST_Node) this.property.walk(tw); return true; }); def_reduce_vars(AST_Default, function(tw, descend) { push(tw); descend(); pop(tw); return true; }); function mark_lambda(tw, descend, compressor) { clear_flag(this, INLINED); push(tw); reset_variables(tw, compressor, this); if (this.uses_arguments) { descend(); pop(tw); return; } var iife; if (!this.name && (iife = tw.parent()) instanceof AST_Call && iife.expression === this && !iife.args.some(arg => arg instanceof AST_Expansion) && this.argnames.every(arg_name => arg_name instanceof AST_Symbol) ) { // Virtually turn IIFE parameters into variable definitions: // (function(a,b) {...})(c,d) => (function() {var a=c,b=d; ...})() // So existing transformation rules can work on them. this.argnames.forEach((arg, i) => { if (!arg.definition) return; var d = arg.definition(); // Avoid setting fixed when there's more than one origin for a variable value if (d.orig.length > 1) return; if (d.fixed === undefined && (!this.uses_arguments || tw.has_directive("use strict"))) { d.fixed = function() { return iife.args[i] || make_node(AST_Undefined, iife); }; tw.loop_ids.set(d.id, tw.in_loop); mark(tw, d, true); } else { d.fixed = false; } }); } descend(); pop(tw); return true; } def_reduce_vars(AST_Lambda, mark_lambda); def_reduce_vars(AST_Do, function(tw, descend, compressor) { reset_block_variables(compressor, this); const saved_loop = tw.in_loop; tw.in_loop = this; push(tw); this.body.walk(tw); if (has_break_or_continue(this)) { pop(tw); push(tw); } this.condition.walk(tw); pop(tw); tw.in_loop = saved_loop; return true; }); def_reduce_vars(AST_For, function(tw, descend, compressor) { reset_block_variables(compressor, this); if (this.init) this.init.walk(tw); const saved_loop = tw.in_loop; tw.in_loop = this; push(tw); if (this.condition) this.condition.walk(tw); this.body.walk(tw); if (this.step) { if (has_break_or_continue(this)) { pop(tw); push(tw); } this.step.walk(tw); } pop(tw); tw.in_loop = saved_loop; return true; }); def_reduce_vars(AST_ForIn, function(tw, descend, compressor) { reset_block_variables(compressor, this); suppress(this.init); this.object.walk(tw); const saved_loop = tw.in_loop; tw.in_loop = this; push(tw); this.body.walk(tw); pop(tw); tw.in_loop = saved_loop; return true; }); def_reduce_vars(AST_If, function(tw) { this.condition.walk(tw); push(tw); this.body.walk(tw); pop(tw); if (this.alternative) { push(tw); this.alternative.walk(tw); pop(tw); } return true; }); def_reduce_vars(AST_LabeledStatement, function(tw) { push(tw); this.body.walk(tw); pop(tw); return true; }); def_reduce_vars(AST_SymbolCatch, function() { this.definition().fixed = false; }); def_reduce_vars(AST_SymbolRef, function(tw, descend, compressor) { var d = this.definition(); d.references.push(this); if (d.references.length == 1 && !d.fixed && d.orig[0] instanceof AST_SymbolDefun) { tw.loop_ids.set(d.id, tw.in_loop); } var fixed_value; if (d.fixed === undefined || !safe_to_read(tw, d)) { d.fixed = false; } else if (d.fixed) { fixed_value = this.fixed_value(); if ( fixed_value instanceof AST_Lambda && recursive_ref(tw, d) ) { d.recursive_refs++; } else if (fixed_value && !compressor.exposed(d) && ref_once(tw, compressor, d) ) { d.single_use = fixed_value instanceof AST_Lambda && !fixed_value.pinned() || fixed_value instanceof AST_Class || d.scope === this.scope && fixed_value.is_constant_expression(); } else { d.single_use = false; } if (is_modified(compressor, tw, this, fixed_value, 0, is_immutable(fixed_value))) { if (d.single_use) { d.single_use = "m"; } else { d.fixed = false; } } } mark_escaped(tw, d, this.scope, this, fixed_value, 0, 1); }); def_reduce_vars(AST_Toplevel, function(tw, descend, compressor) { this.globals.forEach(function(def) { reset_def(compressor, def); }); reset_variables(tw, compressor, this); }); def_reduce_vars(AST_Try, function(tw, descend, compressor) { reset_block_variables(compressor, this); push(tw); walk_body(this, tw); pop(tw); if (this.bcatch) { push(tw); this.bcatch.walk(tw); pop(tw); } if (this.bfinally) this.bfinally.walk(tw); return true; }); def_reduce_vars(AST_Unary, function(tw) { var node = this; if (node.operator !== "++" && node.operator !== "--") return; var exp = node.expression; if (!(exp instanceof AST_SymbolRef)) return; var def = exp.definition(); var safe = safe_to_assign(tw, def, exp.scope, true); def.assignments++; if (!safe) return; var fixed = def.fixed; if (!fixed) return; def.references.push(exp); def.chained = true; def.fixed = function() { return make_node(AST_Binary, node, { operator: node.operator.slice(0, -1), left: make_node(AST_UnaryPrefix, node, { operator: "+", expression: fixed instanceof AST_Node ? fixed : fixed() }), right: make_node(AST_Number, node, { value: 1 }) }); }; mark(tw, def, true); return true; }); def_reduce_vars(AST_VarDef, function(tw, descend) { var node = this; if (node.name instanceof AST_Destructuring) { suppress(node.name); return; } var d = node.name.definition(); if (node.value) { if (safe_to_assign(tw, d, node.name.scope, node.value)) { d.fixed = function() { return node.value; }; tw.loop_ids.set(d.id, tw.in_loop); mark(tw, d, false); descend(); mark(tw, d, true); return true; } else { d.fixed = false; } } }); def_reduce_vars(AST_While, function(tw, descend, compressor) { reset_block_variables(compressor, this); const saved_loop = tw.in_loop; tw.in_loop = this; push(tw); descend(); pop(tw); tw.in_loop = saved_loop; return true; }); })(function(node, func) { node.DEFMETHOD("reduce_vars", func); }); AST_Toplevel.DEFMETHOD("reset_opt_flags", function(compressor) { const self = this; const reduce_vars = compressor.option("reduce_vars"); const preparation = new TreeWalker(function(node, descend) { clear_flag(node, CLEAR_BETWEEN_PASSES); if (reduce_vars) { if (compressor.top_retain && node instanceof AST_Defun // Only functions are retained && preparation.parent() === self ) { set_flag(node, TOP); } return node.reduce_vars(preparation, descend, compressor); } }); // Stack of look-up tables to keep track of whether a `SymbolDef` has been // properly assigned before use: // - `push()` & `pop()` when visiting conditional branches preparation.safe_ids = Object.create(null); preparation.in_loop = null; preparation.loop_ids = new Map(); preparation.defs_to_safe_ids = new Map(); self.walk(preparation); }); AST_Symbol.DEFMETHOD("fixed_value", function() { var fixed = this.thedef.fixed; if (!fixed || fixed instanceof AST_Node) return fixed; return fixed(); }); AST_SymbolRef.DEFMETHOD("is_immutable", function() { var orig = this.definition().orig; return orig.length == 1 && orig[0] instanceof AST_SymbolLambda; }); function is_func_expr(node) { return node instanceof AST_Arrow || node instanceof AST_Function; } function is_lhs_read_only(lhs) { if (lhs instanceof AST_This) return true; if (lhs instanceof AST_SymbolRef) return lhs.definition().orig[0] instanceof AST_SymbolLambda; if (lhs instanceof AST_PropAccess) { lhs = lhs.expression; if (lhs instanceof AST_SymbolRef) { if (lhs.is_immutable()) return false; lhs = lhs.fixed_value(); } if (!lhs) return true; if (lhs instanceof AST_RegExp) return false; if (lhs instanceof AST_Constant) return true; return is_lhs_read_only(lhs); } return false; } function is_ref_of(ref, type) { if (!(ref instanceof AST_SymbolRef)) return false; var orig = ref.definition().orig; for (var i = orig.length; --i >= 0;) { if (orig[i] instanceof type) return true; } } function find_scope(tw) { for (let i = 0;;i++) { const p = tw.parent(i); if (p instanceof AST_Toplevel) return p; if (p instanceof AST_Lambda) return p; if (p.block_scope) return p.block_scope; } } function find_variable(compressor, name) { var scope, i = 0; while (scope = compressor.parent(i++)) { if (scope instanceof AST_Scope) break; if (scope instanceof AST_Catch && scope.argname) { scope = scope.argname.definition().scope; break; } } return scope.find_variable(name); } function make_sequence(orig, expressions) { if (expressions.length == 1) return expressions[0]; if (expressions.length == 0) throw new Error("trying to create a sequence with length zero!"); return make_node(AST_Sequence, orig, { expressions: expressions.reduce(merge_sequence, []) }); } function make_node_from_constant(val, orig) { switch (typeof val) { case "string": return make_node(AST_String, orig, { value: val }); case "number": if (isNaN(val)) return make_node(AST_NaN, orig); if (isFinite(val)) { return 1 / val < 0 ? make_node(AST_UnaryPrefix, orig, { operator: "-", expression: make_node(AST_Number, orig, { value: -val }) }) : make_node(AST_Number, orig, { value: val }); } return val < 0 ? make_node(AST_UnaryPrefix, orig, { operator: "-", expression: make_node(AST_Infinity, orig) }) : make_node(AST_Infinity, orig); case "boolean": return make_node(val ? AST_True : AST_False, orig); case "undefined": return make_node(AST_Undefined, orig); default: if (val === null) { return make_node(AST_Null, orig, { value: null }); } if (val instanceof RegExp) { return make_node(AST_RegExp, orig, { value: { source: regexp_source_fix(val.source), flags: val.flags } }); } throw new Error(string_template("Can't handle constant of type: {type}", { type: typeof val })); } } // we shouldn't compress (1,func)(something) to // func(something) because that changes the meaning of // the func (becomes lexical instead of global). function maintain_this_binding(parent, orig, val) { if (parent instanceof AST_UnaryPrefix && parent.operator == "delete" || parent instanceof AST_Call && parent.expression === orig && (val instanceof AST_PropAccess || val instanceof AST_SymbolRef && val.name == "eval")) { return make_sequence(orig, [ make_node(AST_Number, orig, { value: 0 }), val ]); } return val; } function merge_sequence(array, node) { if (node instanceof AST_Sequence) { array.push(...node.expressions); } else { array.push(node); } return array; } function as_statement_array(thing) { if (thing === null) return []; if (thing instanceof AST_BlockStatement) return thing.body; if (thing instanceof AST_EmptyStatement) return []; if (thing instanceof AST_Statement) return [ thing ]; throw new Error("Can't convert thing to statement array"); } function is_empty(thing) { if (thing === null) return true; if (thing instanceof AST_EmptyStatement) return true; if (thing instanceof AST_BlockStatement) return thing.body.length == 0; return false; } function can_be_evicted_from_block(node) { return !( node instanceof AST_DefClass || node instanceof AST_Defun || node instanceof AST_Let || node instanceof AST_Const || node instanceof AST_Export || node instanceof AST_Import ); } function loop_body(x) { if (x instanceof AST_IterationStatement) { return x.body instanceof AST_BlockStatement ? x.body : x; } return x; } function is_iife_call(node) { // Used to determine whether the node can benefit from negation. // Not the case with arrow functions (you need an extra set of parens). if (node.TYPE != "Call") return false; return node.expression instanceof AST_Function || is_iife_call(node.expression); } function is_undeclared_ref(node) { return node instanceof AST_SymbolRef && node.definition().undeclared; } var global_names = makePredicate("Array Boolean clearInterval clearTimeout console Date decodeURI decodeURIComponent encodeURI encodeURIComponent Error escape eval EvalError Function isFinite isNaN JSON Math Number parseFloat parseInt RangeError ReferenceError RegExp Object setInterval setTimeout String SyntaxError TypeError unescape URIError"); AST_SymbolRef.DEFMETHOD("is_declared", function(compressor) { return !this.definition().undeclared || compressor.option("unsafe") && global_names.has(this.name); }); var identifier_atom = makePredicate("Infinity NaN undefined"); function is_identifier_atom(node) { return node instanceof AST_Infinity || node instanceof AST_NaN || node instanceof AST_Undefined; } // Tighten a bunch of statements together. Used whenever there is a block. function tighten_body(statements, compressor) { var in_loop, in_try; var scope = compressor.find_parent(AST_Scope).get_defun_scope(); find_loop_scope_try(); var CHANGED, max_iter = 10; do { CHANGED = false; eliminate_spurious_blocks(statements); if (compressor.option("dead_code")) { eliminate_dead_code(statements, compressor); } if (compressor.option("if_return")) { handle_if_return(statements, compressor); } if (compressor.sequences_limit > 0) { sequencesize(statements, compressor); sequencesize_2(statements, compressor); } if (compressor.option("join_vars")) { join_consecutive_vars(statements); } if (compressor.option("collapse_vars")) { collapse(statements, compressor); } } while (CHANGED && max_iter-- > 0); function find_loop_scope_try() { var node = compressor.self(), level = 0; do { if (node instanceof AST_Catch || node instanceof AST_Finally) { level++; } else if (node instanceof AST_IterationStatement) { in_loop = true; } else if (node instanceof AST_Scope) { scope = node; break; } else if (node instanceof AST_Try) { in_try = true; } } while (node = compressor.parent(level++)); } // Search from right to left for assignment-like expressions: // - `var a = x;` // - `a = x;` // - `++a` // For each candidate, scan from left to right for first usage, then try // to fold assignment into the site for compression. // Will not attempt to collapse assignments into or past code blocks // which are not sequentially executed, e.g. loops and conditionals. function collapse(statements, compressor) { if (scope.pinned()) return statements; var args; var candidates = []; var stat_index = statements.length; var scanner = new TreeTransformer(function(node) { if (abort) return node; // Skip nodes before `candidate` as quickly as possible if (!hit) { if (node !== hit_stack[hit_index]) return node; hit_index++; if (hit_index < hit_stack.length) return handle_custom_scan_order(node); hit = true; stop_after = find_stop(node, 0); if (stop_after === node) abort = true; return node; } // Stop immediately if these node types are encountered var parent = scanner.parent(); if (node instanceof AST_Assign && (node.logical || node.operator != "=" && lhs.equivalent_to(node.left)) || node instanceof AST_Await || node instanceof AST_Call && lhs instanceof AST_PropAccess && lhs.equivalent_to(node.expression) || node instanceof AST_Debugger || node instanceof AST_Destructuring || node instanceof AST_Expansion && node.expression instanceof AST_Symbol && node.expression.definition().references.length > 1 || node instanceof AST_IterationStatement && !(node instanceof AST_For) || node instanceof AST_LoopControl || node instanceof AST_Try || node instanceof AST_With || node instanceof AST_Yield || node instanceof AST_Export || node instanceof AST_Class || parent instanceof AST_For && node !== parent.init || !replace_all && ( node instanceof AST_SymbolRef && !node.is_declared(compressor) && !pure_prop_access_globals.has(node)) || node instanceof AST_SymbolRef && parent instanceof AST_Call && has_annotation(parent, _NOINLINE) ) { abort = true; return node; } // Stop only if candidate is found within conditional branches if (!stop_if_hit && (!lhs_local || !replace_all) && (parent instanceof AST_Binary && lazy_op.has(parent.operator) && parent.left !== node || parent instanceof AST_Conditional && parent.condition !== node || parent instanceof AST_If && parent.condition !== node)) { stop_if_hit = parent; } // Replace variable with assignment when found if (can_replace && !(node instanceof AST_SymbolDeclaration) && lhs.equivalent_to(node) ) { if (stop_if_hit) { abort = true; return node; } if (is_lhs(node, parent)) { if (value_def) replaced++; return node; } else { replaced++; if (value_def && candidate instanceof AST_VarDef) return node; } CHANGED = abort = true; if (candidate instanceof AST_UnaryPostfix) { return make_node(AST_UnaryPrefix, candidate, candidate); } if (candidate instanceof AST_VarDef) { var def = candidate.name.definition();