You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
641 lines
22 KiB
641 lines
22 KiB
/*********************************************************************** |
|
|
|
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. |
|
|
|
***********************************************************************/ |
|
|
|
import { |
|
AST_Array, |
|
AST_Assign, |
|
AST_Block, |
|
AST_Call, |
|
AST_Catch, |
|
AST_Class, |
|
AST_ClassExpression, |
|
AST_DefaultAssign, |
|
AST_DefClass, |
|
AST_Defun, |
|
AST_Destructuring, |
|
AST_EmptyStatement, |
|
AST_Expansion, |
|
AST_Export, |
|
AST_Function, |
|
AST_Infinity, |
|
AST_IterationStatement, |
|
AST_Lambda, |
|
AST_NaN, |
|
AST_Node, |
|
AST_Number, |
|
AST_Object, |
|
AST_ObjectKeyVal, |
|
AST_PropAccess, |
|
AST_Return, |
|
AST_Scope, |
|
AST_SimpleStatement, |
|
AST_Statement, |
|
AST_SymbolDefun, |
|
AST_SymbolFunarg, |
|
AST_SymbolLambda, |
|
AST_SymbolRef, |
|
AST_SymbolVar, |
|
AST_This, |
|
AST_Toplevel, |
|
AST_UnaryPrefix, |
|
AST_Undefined, |
|
AST_Var, |
|
AST_VarDef, |
|
AST_With, |
|
|
|
walk, |
|
|
|
_INLINE, |
|
_NOINLINE, |
|
_PURE |
|
} from "../ast.js"; |
|
import { make_node, has_annotation } from "../utils/index.js"; |
|
import "../size.js"; |
|
|
|
import "./evaluate.js"; |
|
import "./drop-side-effect-free.js"; |
|
import "./reduce-vars.js"; |
|
import { is_undeclared_ref, is_lhs } from "./inference.js"; |
|
import { |
|
SQUEEZED, |
|
INLINED, |
|
UNUSED, |
|
|
|
has_flag, |
|
set_flag, |
|
} from "./compressor-flags.js"; |
|
import { |
|
make_sequence, |
|
best_of, |
|
make_node_from_constant, |
|
identifier_atom, |
|
is_empty, |
|
is_func_expr, |
|
is_iife_call, |
|
is_reachable, |
|
is_recursive_ref, |
|
retain_top_func, |
|
} from "./common.js"; |
|
|
|
|
|
function within_array_or_object_literal(compressor) { |
|
var node, level = 0; |
|
while (node = compressor.parent(level++)) { |
|
if (node instanceof AST_Statement) return false; |
|
if (node instanceof AST_Array |
|
|| node instanceof AST_ObjectKeyVal |
|
|| node instanceof AST_Object) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
function scope_encloses_variables_in_this_scope(scope, pulled_scope) { |
|
for (const enclosed of pulled_scope.enclosed) { |
|
if (pulled_scope.variables.has(enclosed.name)) { |
|
continue; |
|
} |
|
const looked_up = scope.find_variable(enclosed.name); |
|
if (looked_up) { |
|
if (looked_up === enclosed) continue; |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
export function inline_into_symbolref(self, compressor) { |
|
if ( |
|
!compressor.option("ie8") |
|
&& is_undeclared_ref(self) |
|
&& !compressor.find_parent(AST_With) |
|
) { |
|
switch (self.name) { |
|
case "undefined": |
|
return make_node(AST_Undefined, self).optimize(compressor); |
|
case "NaN": |
|
return make_node(AST_NaN, self).optimize(compressor); |
|
case "Infinity": |
|
return make_node(AST_Infinity, self).optimize(compressor); |
|
} |
|
} |
|
|
|
const parent = compressor.parent(); |
|
if (compressor.option("reduce_vars") && is_lhs(self, parent) !== self) { |
|
const def = self.definition(); |
|
const nearest_scope = compressor.find_scope(); |
|
if (compressor.top_retain && def.global && compressor.top_retain(def)) { |
|
def.fixed = false; |
|
def.single_use = false; |
|
return self; |
|
} |
|
|
|
let fixed = self.fixed_value(); |
|
let single_use = def.single_use |
|
&& !(parent instanceof AST_Call |
|
&& (parent.is_callee_pure(compressor)) |
|
|| has_annotation(parent, _NOINLINE)) |
|
&& !(parent instanceof AST_Export |
|
&& fixed instanceof AST_Lambda |
|
&& fixed.name); |
|
|
|
if (single_use && fixed instanceof AST_Node) { |
|
single_use = |
|
!fixed.has_side_effects(compressor) |
|
&& !fixed.may_throw(compressor); |
|
} |
|
|
|
if (single_use && (fixed instanceof AST_Lambda || fixed instanceof AST_Class)) { |
|
if (retain_top_func(fixed, compressor)) { |
|
single_use = false; |
|
} else if (def.scope !== self.scope |
|
&& (def.escaped == 1 |
|
|| has_flag(fixed, INLINED) |
|
|| within_array_or_object_literal(compressor) |
|
|| !compressor.option("reduce_funcs"))) { |
|
single_use = false; |
|
} else if (is_recursive_ref(compressor, def)) { |
|
single_use = false; |
|
} else if (def.scope !== self.scope || def.orig[0] instanceof AST_SymbolFunarg) { |
|
single_use = fixed.is_constant_expression(self.scope); |
|
if (single_use == "f") { |
|
var scope = self.scope; |
|
do { |
|
if (scope instanceof AST_Defun || is_func_expr(scope)) { |
|
set_flag(scope, INLINED); |
|
} |
|
} while (scope = scope.parent_scope); |
|
} |
|
} |
|
} |
|
|
|
if (single_use && fixed instanceof AST_Lambda) { |
|
single_use = |
|
def.scope === self.scope |
|
&& !scope_encloses_variables_in_this_scope(nearest_scope, fixed) |
|
|| parent instanceof AST_Call |
|
&& parent.expression === self |
|
&& !scope_encloses_variables_in_this_scope(nearest_scope, fixed) |
|
&& !(fixed.name && fixed.name.definition().recursive_refs > 0); |
|
} |
|
|
|
if (single_use && fixed) { |
|
if (fixed instanceof AST_DefClass) { |
|
set_flag(fixed, SQUEEZED); |
|
fixed = make_node(AST_ClassExpression, fixed, fixed); |
|
} |
|
if (fixed instanceof AST_Defun) { |
|
set_flag(fixed, SQUEEZED); |
|
fixed = make_node(AST_Function, fixed, fixed); |
|
} |
|
if (def.recursive_refs > 0 && fixed.name instanceof AST_SymbolDefun) { |
|
const defun_def = fixed.name.definition(); |
|
let lambda_def = fixed.variables.get(fixed.name.name); |
|
let name = lambda_def && lambda_def.orig[0]; |
|
if (!(name instanceof AST_SymbolLambda)) { |
|
name = make_node(AST_SymbolLambda, fixed.name, fixed.name); |
|
name.scope = fixed; |
|
fixed.name = name; |
|
lambda_def = fixed.def_function(name); |
|
} |
|
walk(fixed, node => { |
|
if (node instanceof AST_SymbolRef && node.definition() === defun_def) { |
|
node.thedef = lambda_def; |
|
lambda_def.references.push(node); |
|
} |
|
}); |
|
} |
|
if ( |
|
(fixed instanceof AST_Lambda || fixed instanceof AST_Class) |
|
&& fixed.parent_scope !== nearest_scope |
|
) { |
|
fixed = fixed.clone(true, compressor.get_toplevel()); |
|
|
|
nearest_scope.add_child_scope(fixed); |
|
} |
|
return fixed.optimize(compressor); |
|
} |
|
|
|
// multiple uses |
|
if (fixed) { |
|
let replace; |
|
|
|
if (fixed instanceof AST_This) { |
|
if (!(def.orig[0] instanceof AST_SymbolFunarg) |
|
&& def.references.every((ref) => |
|
def.scope === ref.scope |
|
)) { |
|
replace = fixed; |
|
} |
|
} else { |
|
var ev = fixed.evaluate(compressor); |
|
if ( |
|
ev !== fixed |
|
&& (compressor.option("unsafe_regexp") || !(ev instanceof RegExp)) |
|
) { |
|
replace = make_node_from_constant(ev, fixed); |
|
} |
|
} |
|
|
|
if (replace) { |
|
const name_length = self.size(compressor); |
|
const replace_size = replace.size(compressor); |
|
|
|
let overhead = 0; |
|
if (compressor.option("unused") && !compressor.exposed(def)) { |
|
overhead = |
|
(name_length + 2 + replace_size) / |
|
(def.references.length - def.assignments); |
|
} |
|
|
|
if (replace_size <= name_length + overhead) { |
|
return replace; |
|
} |
|
} |
|
} |
|
} |
|
|
|
return self; |
|
} |
|
|
|
export function inline_into_call(self, fn, compressor) { |
|
var exp = self.expression; |
|
var simple_args = self.args.every((arg) => !(arg instanceof AST_Expansion)); |
|
|
|
if (compressor.option("reduce_vars") |
|
&& fn instanceof AST_SymbolRef |
|
&& !has_annotation(self, _NOINLINE) |
|
) { |
|
const fixed = fn.fixed_value(); |
|
if (!retain_top_func(fixed, compressor)) { |
|
fn = fixed; |
|
} |
|
} |
|
|
|
var is_func = fn instanceof AST_Lambda; |
|
|
|
var stat = is_func && fn.body[0]; |
|
var is_regular_func = is_func && !fn.is_generator && !fn.async; |
|
var can_inline = is_regular_func && compressor.option("inline") && !self.is_callee_pure(compressor); |
|
if (can_inline && stat instanceof AST_Return) { |
|
let returned = stat.value; |
|
if (!returned || returned.is_constant_expression()) { |
|
if (returned) { |
|
returned = returned.clone(true); |
|
} else { |
|
returned = make_node(AST_Undefined, self); |
|
} |
|
const args = self.args.concat(returned); |
|
return make_sequence(self, args).optimize(compressor); |
|
} |
|
|
|
// optimize identity function |
|
if ( |
|
fn.argnames.length === 1 |
|
&& (fn.argnames[0] instanceof AST_SymbolFunarg) |
|
&& self.args.length < 2 |
|
&& returned instanceof AST_SymbolRef |
|
&& returned.name === fn.argnames[0].name |
|
) { |
|
const replacement = |
|
(self.args[0] || make_node(AST_Undefined)).optimize(compressor); |
|
|
|
let parent; |
|
if ( |
|
replacement instanceof AST_PropAccess |
|
&& (parent = compressor.parent()) instanceof AST_Call |
|
&& parent.expression === self |
|
) { |
|
// identity function was being used to remove `this`, like in |
|
// |
|
// id(bag.no_this)(...) |
|
// |
|
// Replace with a larger but more effish (0, bag.no_this) wrapper. |
|
|
|
return make_sequence(self, [ |
|
make_node(AST_Number, self, { value: 0 }), |
|
replacement |
|
]); |
|
} |
|
// replace call with first argument or undefined if none passed |
|
return replacement; |
|
} |
|
} |
|
|
|
if (can_inline) { |
|
var scope, in_loop, level = -1; |
|
let def; |
|
let returned_value; |
|
let nearest_scope; |
|
if (simple_args |
|
&& !fn.uses_arguments |
|
&& !(compressor.parent() instanceof AST_Class) |
|
&& !(fn.name && fn instanceof AST_Function) |
|
&& (returned_value = can_flatten_body(stat)) |
|
&& (exp === fn |
|
|| has_annotation(self, _INLINE) |
|
|| compressor.option("unused") |
|
&& (def = exp.definition()).references.length == 1 |
|
&& !is_recursive_ref(compressor, def) |
|
&& fn.is_constant_expression(exp.scope)) |
|
&& !has_annotation(self, _PURE | _NOINLINE) |
|
&& !fn.contains_this() |
|
&& can_inject_symbols() |
|
&& (nearest_scope = compressor.find_scope()) |
|
&& !scope_encloses_variables_in_this_scope(nearest_scope, fn) |
|
&& !(function in_default_assign() { |
|
// Due to the fact function parameters have their own scope |
|
// which can't use `var something` in the function body within, |
|
// we simply don't inline into DefaultAssign. |
|
let i = 0; |
|
let p; |
|
while ((p = compressor.parent(i++))) { |
|
if (p instanceof AST_DefaultAssign) return true; |
|
if (p instanceof AST_Block) break; |
|
} |
|
return false; |
|
})() |
|
&& !(scope instanceof AST_Class) |
|
) { |
|
set_flag(fn, SQUEEZED); |
|
nearest_scope.add_child_scope(fn); |
|
return make_sequence(self, flatten_fn(returned_value)).optimize(compressor); |
|
} |
|
} |
|
|
|
if (can_inline && has_annotation(self, _INLINE)) { |
|
set_flag(fn, SQUEEZED); |
|
fn = make_node(fn.CTOR === AST_Defun ? AST_Function : fn.CTOR, fn, fn); |
|
fn = fn.clone(true); |
|
fn.figure_out_scope({}, { |
|
parent_scope: compressor.find_scope(), |
|
toplevel: compressor.get_toplevel() |
|
}); |
|
|
|
return make_node(AST_Call, self, { |
|
expression: fn, |
|
args: self.args, |
|
}).optimize(compressor); |
|
} |
|
|
|
const can_drop_this_call = is_regular_func && compressor.option("side_effects") && fn.body.every(is_empty); |
|
if (can_drop_this_call) { |
|
var args = self.args.concat(make_node(AST_Undefined, self)); |
|
return make_sequence(self, args).optimize(compressor); |
|
} |
|
|
|
if (compressor.option("negate_iife") |
|
&& compressor.parent() instanceof AST_SimpleStatement |
|
&& is_iife_call(self)) { |
|
return self.negate(compressor, true); |
|
} |
|
|
|
var ev = self.evaluate(compressor); |
|
if (ev !== self) { |
|
ev = make_node_from_constant(ev, self).optimize(compressor); |
|
return best_of(compressor, ev, self); |
|
} |
|
|
|
return self; |
|
|
|
function return_value(stat) { |
|
if (!stat) return make_node(AST_Undefined, self); |
|
if (stat instanceof AST_Return) { |
|
if (!stat.value) return make_node(AST_Undefined, self); |
|
return stat.value.clone(true); |
|
} |
|
if (stat instanceof AST_SimpleStatement) { |
|
return make_node(AST_UnaryPrefix, stat, { |
|
operator: "void", |
|
expression: stat.body.clone(true) |
|
}); |
|
} |
|
} |
|
|
|
function can_flatten_body(stat) { |
|
var body = fn.body; |
|
var len = body.length; |
|
if (compressor.option("inline") < 3) { |
|
return len == 1 && return_value(stat); |
|
} |
|
stat = null; |
|
for (var i = 0; i < len; i++) { |
|
var line = body[i]; |
|
if (line instanceof AST_Var) { |
|
if (stat && !line.definitions.every((var_def) => |
|
!var_def.value |
|
)) { |
|
return false; |
|
} |
|
} else if (stat) { |
|
return false; |
|
} else if (!(line instanceof AST_EmptyStatement)) { |
|
stat = line; |
|
} |
|
} |
|
return return_value(stat); |
|
} |
|
|
|
function can_inject_args(block_scoped, safe_to_inject) { |
|
for (var i = 0, len = fn.argnames.length; i < len; i++) { |
|
var arg = fn.argnames[i]; |
|
if (arg instanceof AST_DefaultAssign) { |
|
if (has_flag(arg.left, UNUSED)) continue; |
|
return false; |
|
} |
|
if (arg instanceof AST_Destructuring) return false; |
|
if (arg instanceof AST_Expansion) { |
|
if (has_flag(arg.expression, UNUSED)) continue; |
|
return false; |
|
} |
|
if (has_flag(arg, UNUSED)) continue; |
|
if (!safe_to_inject |
|
|| block_scoped.has(arg.name) |
|
|| identifier_atom.has(arg.name) |
|
|| scope.conflicting_def(arg.name)) { |
|
return false; |
|
} |
|
if (in_loop) in_loop.push(arg.definition()); |
|
} |
|
return true; |
|
} |
|
|
|
function can_inject_vars(block_scoped, safe_to_inject) { |
|
var len = fn.body.length; |
|
for (var i = 0; i < len; i++) { |
|
var stat = fn.body[i]; |
|
if (!(stat instanceof AST_Var)) continue; |
|
if (!safe_to_inject) return false; |
|
for (var j = stat.definitions.length; --j >= 0;) { |
|
var name = stat.definitions[j].name; |
|
if (name instanceof AST_Destructuring |
|
|| block_scoped.has(name.name) |
|
|| identifier_atom.has(name.name) |
|
|| scope.conflicting_def(name.name)) { |
|
return false; |
|
} |
|
if (in_loop) in_loop.push(name.definition()); |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
function can_inject_symbols() { |
|
var block_scoped = new Set(); |
|
do { |
|
scope = compressor.parent(++level); |
|
if (scope.is_block_scope() && scope.block_scope) { |
|
// TODO this is sometimes undefined during compression. |
|
// But it should always have a value! |
|
scope.block_scope.variables.forEach(function (variable) { |
|
block_scoped.add(variable.name); |
|
}); |
|
} |
|
if (scope instanceof AST_Catch) { |
|
// TODO can we delete? AST_Catch is a block scope. |
|
if (scope.argname) { |
|
block_scoped.add(scope.argname.name); |
|
} |
|
} else if (scope instanceof AST_IterationStatement) { |
|
in_loop = []; |
|
} else if (scope instanceof AST_SymbolRef) { |
|
if (scope.fixed_value() instanceof AST_Scope) return false; |
|
} |
|
} while (!(scope instanceof AST_Scope)); |
|
|
|
var safe_to_inject = !(scope instanceof AST_Toplevel) || compressor.toplevel.vars; |
|
var inline = compressor.option("inline"); |
|
if (!can_inject_vars(block_scoped, inline >= 3 && safe_to_inject)) return false; |
|
if (!can_inject_args(block_scoped, inline >= 2 && 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(); |
|
|
|
// Name already exists, only when a function argument had the same name |
|
const already_appended = scope.variables.has(name.name); |
|
if (!already_appended) { |
|
scope.variables.set(name.name, def); |
|
scope.enclosed.push(def); |
|
decls.push(make_node(AST_VarDef, name, { |
|
name: name, |
|
value: null |
|
})); |
|
} |
|
|
|
var sym = make_node(AST_SymbolRef, name, name); |
|
def.references.push(sym); |
|
if (value) expressions.push(make_node(AST_Assign, self, { |
|
operator: "=", |
|
logical: false, |
|
left: sym, |
|
right: value.clone() |
|
})); |
|
} |
|
|
|
function flatten_args(decls, expressions) { |
|
var len = fn.argnames.length; |
|
for (var i = self.args.length; --i >= len;) { |
|
expressions.push(self.args[i]); |
|
} |
|
for (i = len; --i >= 0;) { |
|
var name = fn.argnames[i]; |
|
var value = self.args[i]; |
|
if (has_flag(name, UNUSED) || !name.name || scope.conflicting_def(name.name)) { |
|
if (value) expressions.push(value); |
|
} else { |
|
var symbol = make_node(AST_SymbolVar, name, name); |
|
name.definition().orig.push(symbol); |
|
if (!value && in_loop) value = make_node(AST_Undefined, self); |
|
append_var(decls, expressions, symbol, value); |
|
} |
|
} |
|
decls.reverse(); |
|
expressions.reverse(); |
|
} |
|
|
|
function flatten_vars(decls, expressions) { |
|
var pos = expressions.length; |
|
for (var i = 0, lines = fn.body.length; i < lines; i++) { |
|
var stat = fn.body[i]; |
|
if (!(stat instanceof AST_Var)) continue; |
|
for (var j = 0, defs = stat.definitions.length; j < defs; j++) { |
|
var var_def = stat.definitions[j]; |
|
var name = var_def.name; |
|
append_var(decls, expressions, name, var_def.value); |
|
if (in_loop && fn.argnames.every((argname) => |
|
argname.name != name.name |
|
)) { |
|
var def = fn.variables.get(name.name); |
|
var sym = make_node(AST_SymbolRef, name, name); |
|
def.references.push(sym); |
|
expressions.splice(pos++, 0, make_node(AST_Assign, var_def, { |
|
operator: "=", |
|
logical: false, |
|
left: sym, |
|
right: make_node(AST_Undefined, name) |
|
})); |
|
} |
|
} |
|
} |
|
} |
|
|
|
function flatten_fn(returned_value) { |
|
var decls = []; |
|
var expressions = []; |
|
flatten_args(decls, expressions); |
|
flatten_vars(decls, expressions); |
|
expressions.push(returned_value); |
|
|
|
if (decls.length) { |
|
const i = scope.body.indexOf(compressor.parent(level - 1)) + 1; |
|
scope.body.splice(i, 0, make_node(AST_Var, fn, { |
|
definitions: decls |
|
})); |
|
} |
|
|
|
return expressions.map(exp => exp.clone(true)); |
|
} |
|
}
|
|
|