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.
344 lines
11 KiB
344 lines
11 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_BlockStatement, |
|
AST_Call, |
|
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_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 |
|
})); |
|
} |
|
} |
|
|
|
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 ( |
|
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" |
|
) |
|
) { |
|
const zero = make_node(AST_Number, orig, { value: 0 }); |
|
return make_sequence(orig, [ zero, val ]); |
|
} else { |
|
return val; |
|
} |
|
} |
|
|
|
export function is_func_expr(node) { |
|
return node instanceof AST_Arrow || node instanceof AST_Function; |
|
} |
|
|
|
export 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); |
|
} |
|
|
|
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(compressor, def) { |
|
var node; |
|
for (var i = 0; node = compressor.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); |
|
}
|
|
|