630 lines
21 KiB
JavaScript
630 lines
21 KiB
JavaScript
/***********************************************************************
|
|
|
|
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_IterationStatement,
|
|
AST_Lambda,
|
|
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,
|
|
|
|
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 {
|
|
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";
|
|
|
|
/**
|
|
* Module that contains the inlining logic.
|
|
*
|
|
* @module
|
|
*
|
|
* The stars of the show are `inline_into_symbolref` and `inline_into_call`.
|
|
*/
|
|
|
|
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) {
|
|
const parent = compressor.parent();
|
|
|
|
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 || fixed instanceof AST_Class)) {
|
|
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
|
|
&& !(self.args[0] instanceof AST_Expansion)
|
|
&& 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));
|
|
}
|
|
}
|