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.
374 lines
12 KiB
374 lines
12 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_Arrow,
|
|
AST_BigInt,
|
|
AST_BlockStatement,
|
|
AST_Call,
|
|
AST_Chain,
|
|
AST_Class,
|
|
AST_Const,
|
|
AST_Constant,
|
|
AST_DefClass,
|
|
AST_Defun,
|
|
AST_EmptyStatement,
|
|
AST_Export,
|
|
AST_False,
|
|
AST_Function,
|
|
AST_Import,
|
|
AST_Infinity,
|
|
AST_LabeledStatement,
|
|
AST_Lambda,
|
|
AST_Let,
|
|
AST_LoopControl,
|
|
AST_NaN,
|
|
AST_Node,
|
|
AST_Null,
|
|
AST_Number,
|
|
AST_Object,
|
|
AST_ObjectKeyVal,
|
|
AST_PropAccess,
|
|
AST_RegExp,
|
|
AST_Scope,
|
|
AST_Sequence,
|
|
AST_SimpleStatement,
|
|
AST_Statement,
|
|
AST_String,
|
|
AST_SymbolRef,
|
|
AST_True,
|
|
AST_UnaryPrefix,
|
|
AST_Undefined,
|
|
|
|
TreeWalker,
|
|
walk,
|
|
walk_abort,
|
|
walk_parent,
|
|
} from "../ast.js";
|
|
import { make_node, regexp_source_fix, string_template, makePredicate } from "../utils/index.js";
|
|
import { first_in_statement } from "../utils/first_in_statement.js";
|
|
import { has_flag, TOP } from "./compressor-flags.js";
|
|
|
|
export function merge_sequence(array, node) {
|
|
if (node instanceof AST_Sequence) {
|
|
array.push(...node.expressions);
|
|
} else {
|
|
array.push(node);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
export 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, [])
|
|
});
|
|
}
|
|
|
|
export function make_empty_function(self) {
|
|
return make_node(AST_Function, self, {
|
|
uses_arguments: false,
|
|
argnames: [],
|
|
body: [],
|
|
is_generator: false,
|
|
async: false,
|
|
variables: new Map(),
|
|
uses_with: false,
|
|
uses_eval: false,
|
|
parent_scope: null,
|
|
enclosed: [],
|
|
cname: 0,
|
|
block_scope: undefined,
|
|
});
|
|
}
|
|
|
|
export 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 "bigint":
|
|
return make_node(AST_BigInt, orig, { value: val.toString() });
|
|
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
|
|
}));
|
|
}
|
|
}
|
|
|
|
export function best_of_expression(ast1, ast2) {
|
|
return ast1.size() > ast2.size() ? ast2 : ast1;
|
|
}
|
|
|
|
export function best_of_statement(ast1, ast2) {
|
|
return best_of_expression(
|
|
make_node(AST_SimpleStatement, ast1, {
|
|
body: ast1
|
|
}),
|
|
make_node(AST_SimpleStatement, ast2, {
|
|
body: ast2
|
|
})
|
|
).body;
|
|
}
|
|
|
|
/** Find which node is smaller, and return that */
|
|
export function best_of(compressor, ast1, ast2) {
|
|
if (first_in_statement(compressor)) {
|
|
return best_of_statement(ast1, ast2);
|
|
} else {
|
|
return best_of_expression(ast1, ast2);
|
|
}
|
|
}
|
|
|
|
/** Simplify an object property's key, if possible */
|
|
export function get_simple_key(key) {
|
|
if (key instanceof AST_Constant) {
|
|
return key.getValue();
|
|
}
|
|
if (key instanceof AST_UnaryPrefix
|
|
&& key.operator == "void"
|
|
&& key.expression instanceof AST_Constant) {
|
|
return;
|
|
}
|
|
return key;
|
|
}
|
|
|
|
export function read_property(obj, key) {
|
|
key = get_simple_key(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;
|
|
}
|
|
|
|
export function has_break_or_continue(loop, parent) {
|
|
var found = false;
|
|
var tw = new TreeWalker(function(node) {
|
|
if (found || node instanceof AST_Scope) return true;
|
|
if (node instanceof AST_LoopControl && tw.loopcontrol_target(node) === loop) {
|
|
return found = true;
|
|
}
|
|
});
|
|
if (parent instanceof AST_LabeledStatement) tw.push(parent);
|
|
tw.push(loop);
|
|
loop.body.walk(tw);
|
|
return found;
|
|
}
|
|
|
|
// we shouldn't compress (1,func)(something) to
|
|
// func(something) because that changes the meaning of
|
|
// the func (becomes lexical instead of global).
|
|
export function maintain_this_binding(parent, orig, val) {
|
|
if (requires_sequence_to_maintain_binding(parent, orig, val)) {
|
|
const zero = make_node(AST_Number, orig, { value: 0 });
|
|
return make_sequence(orig, [ zero, val ]);
|
|
} else {
|
|
return val;
|
|
}
|
|
}
|
|
|
|
/** Detect (1, x.noThis)(), (0, eval)(), which need sequences */
|
|
export function requires_sequence_to_maintain_binding(parent, orig, val) {
|
|
return (
|
|
parent instanceof AST_UnaryPrefix && parent.operator == "delete"
|
|
|| parent instanceof AST_Call && parent.expression === orig
|
|
&& (
|
|
val instanceof AST_Chain
|
|
|| val instanceof AST_PropAccess
|
|
|| val instanceof AST_SymbolRef && val.name == "eval"
|
|
)
|
|
);
|
|
}
|
|
|
|
export function is_func_expr(node) {
|
|
return node instanceof AST_Arrow || node instanceof AST_Function;
|
|
}
|
|
|
|
/**
|
|
* Used to determine whether the node can benefit from negation.
|
|
* Not the case with arrow functions (you need an extra set of parens). */
|
|
export function is_iife_call(node) {
|
|
if (node.TYPE != "Call") return false;
|
|
return node.expression instanceof AST_Function || is_iife_call(node.expression);
|
|
}
|
|
|
|
export 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;
|
|
}
|
|
|
|
export const identifier_atom = makePredicate("Infinity NaN undefined");
|
|
export function is_identifier_atom(node) {
|
|
return node instanceof AST_Infinity
|
|
|| node instanceof AST_NaN
|
|
|| node instanceof AST_Undefined;
|
|
}
|
|
|
|
/** Check if this is a SymbolRef node which has one def of a certain AST type */
|
|
export 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;
|
|
}
|
|
}
|
|
|
|
/**Can we turn { block contents... } into just the block contents ?
|
|
* Not if one of these is inside.
|
|
**/
|
|
export 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
|
|
);
|
|
}
|
|
|
|
export 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");
|
|
}
|
|
|
|
export function is_reachable(scope_node, defs) {
|
|
const find_ref = node => {
|
|
if (node instanceof AST_SymbolRef && defs.includes(node.definition())) {
|
|
return walk_abort;
|
|
}
|
|
};
|
|
|
|
return walk_parent(scope_node, (node, info) => {
|
|
if (node instanceof AST_Scope && node !== scope_node) {
|
|
var parent = info.parent();
|
|
|
|
if (
|
|
parent instanceof AST_Call
|
|
&& parent.expression === node
|
|
// Async/Generators aren't guaranteed to sync evaluate all of
|
|
// their body steps, so it's possible they close over the variable.
|
|
&& !(node.async || node.is_generator)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (walk(node, find_ref)) return walk_abort;
|
|
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Check if a ref refers to the name of a function/class it's defined within */
|
|
export function is_recursive_ref(tw, def) {
|
|
var node;
|
|
for (var i = 0; node = tw.parent(i); i++) {
|
|
if (node instanceof AST_Lambda || node instanceof AST_Class) {
|
|
var name = node.name;
|
|
if (name && name.definition() === def) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// TODO this only works with AST_Defun, shouldn't it work for other ways of defining functions?
|
|
export function retain_top_func(fn, compressor) {
|
|
return compressor.top_retain
|
|
&& fn instanceof AST_Defun
|
|
&& has_flag(fn, TOP)
|
|
&& fn.name
|
|
&& compressor.top_retain(fn.name.definition());
|
|
}
|