574 lines
15 KiB
JavaScript
574 lines
15 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 {
|
|
HOP,
|
|
makePredicate,
|
|
return_this,
|
|
string_template,
|
|
regexp_source_fix,
|
|
regexp_is_safe,
|
|
} from '../utils/index.js';
|
|
import {
|
|
AST_Array,
|
|
AST_BigInt,
|
|
AST_Binary,
|
|
AST_Call,
|
|
AST_Chain,
|
|
AST_Class,
|
|
AST_Conditional,
|
|
AST_Constant,
|
|
AST_Dot,
|
|
AST_Expansion,
|
|
AST_Function,
|
|
AST_Lambda,
|
|
AST_New,
|
|
AST_Node,
|
|
AST_Object,
|
|
AST_PropAccess,
|
|
AST_RegExp,
|
|
AST_Statement,
|
|
AST_Symbol,
|
|
AST_SymbolRef,
|
|
AST_TemplateString,
|
|
AST_UnaryPrefix,
|
|
AST_With,
|
|
} from '../ast.js';
|
|
import { is_undeclared_ref } from './inference.js';
|
|
import {
|
|
is_pure_native_value,
|
|
is_pure_native_fn,
|
|
is_pure_native_method,
|
|
} from './native-objects.js';
|
|
|
|
// methods to evaluate a constant expression
|
|
|
|
function def_eval(node, func) {
|
|
node.DEFMETHOD('_eval', func);
|
|
}
|
|
|
|
// Used to propagate a nullish short-circuit signal upwards through the chain.
|
|
export const nullish = Symbol('This AST_Chain is nullish');
|
|
|
|
// If the node has been successfully reduced to a constant,
|
|
// then its value is returned; otherwise the element itself
|
|
// is returned.
|
|
// They can be distinguished as constant value is never a
|
|
// descendant of AST_Node.
|
|
AST_Node.DEFMETHOD('evaluate', function (compressor) {
|
|
if (!compressor.option('evaluate')) return this;
|
|
var val = this._eval(compressor, 1);
|
|
if (!val || val instanceof RegExp) return val;
|
|
if (typeof val == 'function' || typeof val == 'object' || val == nullish)
|
|
return this;
|
|
|
|
// Evaluated strings can be larger than the original expression
|
|
if (typeof val === 'string') {
|
|
const unevaluated_size = this.size(compressor);
|
|
if (val.length + 2 > unevaluated_size) return this;
|
|
}
|
|
|
|
return val;
|
|
});
|
|
|
|
var unaryPrefix = makePredicate('! ~ - + void');
|
|
AST_Node.DEFMETHOD('is_constant', function () {
|
|
// Accomodate when compress option evaluate=false
|
|
// as well as the common constant expressions !0 and -1
|
|
if (this instanceof AST_Constant) {
|
|
return !(this instanceof AST_RegExp);
|
|
} else {
|
|
return (
|
|
this instanceof AST_UnaryPrefix &&
|
|
unaryPrefix.has(this.operator) &&
|
|
// `this.expression` may be an `AST_RegExp`,
|
|
// so not only `.is_constant()`.
|
|
(this.expression instanceof AST_Constant || this.expression.is_constant())
|
|
);
|
|
}
|
|
});
|
|
|
|
def_eval(AST_Statement, function () {
|
|
throw new Error(
|
|
string_template(
|
|
'Cannot evaluate a statement [{file}:{line},{col}]',
|
|
this.start
|
|
)
|
|
);
|
|
});
|
|
|
|
def_eval(AST_Lambda, return_this);
|
|
def_eval(AST_Class, return_this);
|
|
def_eval(AST_Node, return_this);
|
|
def_eval(AST_Constant, function () {
|
|
return this.getValue();
|
|
});
|
|
|
|
const supports_bigint = typeof BigInt === 'function';
|
|
def_eval(AST_BigInt, function () {
|
|
if (supports_bigint) {
|
|
return BigInt(this.value);
|
|
} else {
|
|
return this;
|
|
}
|
|
});
|
|
|
|
def_eval(AST_RegExp, function (compressor) {
|
|
let evaluated = compressor.evaluated_regexps.get(this.value);
|
|
if (evaluated === undefined && regexp_is_safe(this.value.source)) {
|
|
try {
|
|
const { source, flags } = this.value;
|
|
evaluated = new RegExp(source, flags);
|
|
} catch (e) {
|
|
evaluated = null;
|
|
}
|
|
compressor.evaluated_regexps.set(this.value, evaluated);
|
|
}
|
|
return evaluated || this;
|
|
});
|
|
|
|
def_eval(AST_TemplateString, function () {
|
|
if (this.segments.length !== 1) return this;
|
|
return this.segments[0].value;
|
|
});
|
|
|
|
def_eval(AST_Function, function (compressor) {
|
|
if (compressor.option('unsafe')) {
|
|
var fn = function () {};
|
|
fn.node = this;
|
|
fn.toString = () => this.print_to_string();
|
|
return fn;
|
|
}
|
|
return this;
|
|
});
|
|
|
|
def_eval(AST_Array, function (compressor, depth) {
|
|
if (compressor.option('unsafe')) {
|
|
var elements = [];
|
|
for (var i = 0, len = this.elements.length; i < len; i++) {
|
|
var element = this.elements[i];
|
|
var value = element._eval(compressor, depth);
|
|
if (element === value) return this;
|
|
elements.push(value);
|
|
}
|
|
return elements;
|
|
}
|
|
return this;
|
|
});
|
|
|
|
def_eval(AST_Object, function (compressor, depth) {
|
|
if (compressor.option('unsafe')) {
|
|
var val = {};
|
|
for (var i = 0, len = this.properties.length; i < len; i++) {
|
|
var prop = this.properties[i];
|
|
if (prop instanceof AST_Expansion) return this;
|
|
var key = prop.key;
|
|
if (key instanceof AST_Symbol) {
|
|
key = key.name;
|
|
} else if (key instanceof AST_Node) {
|
|
key = key._eval(compressor, depth);
|
|
if (key === prop.key) return this;
|
|
}
|
|
if (typeof Object.prototype[key] === 'function') {
|
|
return this;
|
|
}
|
|
if (prop.value instanceof AST_Function) continue;
|
|
val[key] = prop.value._eval(compressor, depth);
|
|
if (val[key] === prop.value) return this;
|
|
}
|
|
return val;
|
|
}
|
|
return this;
|
|
});
|
|
|
|
var non_converting_unary = makePredicate('! typeof void');
|
|
def_eval(AST_UnaryPrefix, function (compressor, depth) {
|
|
var e = this.expression;
|
|
if (compressor.option('typeofs') && this.operator == 'typeof') {
|
|
// Function would be evaluated to an array and so typeof would
|
|
// incorrectly return 'object'. Hence making is a special case.
|
|
if (
|
|
e instanceof AST_Lambda ||
|
|
(e instanceof AST_SymbolRef && e.fixed_value() instanceof AST_Lambda)
|
|
) {
|
|
return typeof function () {};
|
|
}
|
|
if (
|
|
(e instanceof AST_Object ||
|
|
e instanceof AST_Array ||
|
|
(e instanceof AST_SymbolRef &&
|
|
(e.fixed_value() instanceof AST_Object ||
|
|
e.fixed_value() instanceof AST_Array))) &&
|
|
!e.has_side_effects(compressor)
|
|
) {
|
|
return typeof {};
|
|
}
|
|
}
|
|
if (!non_converting_unary.has(this.operator)) depth++;
|
|
e = e._eval(compressor, depth);
|
|
if (e === this.expression) return this;
|
|
switch (this.operator) {
|
|
case '!':
|
|
return !e;
|
|
case 'typeof':
|
|
// typeof <RegExp> returns "object" or "function" on different platforms
|
|
// so cannot evaluate reliably
|
|
if (e instanceof RegExp) return this;
|
|
return typeof e;
|
|
case 'void':
|
|
return void e;
|
|
case '~':
|
|
return ~e;
|
|
case '-':
|
|
return -e;
|
|
case '+':
|
|
return +e;
|
|
}
|
|
return this;
|
|
});
|
|
|
|
var non_converting_binary = makePredicate('&& || ?? === !==');
|
|
const identity_comparison = makePredicate('== != === !==');
|
|
const has_identity = (value) =>
|
|
typeof value === 'object' ||
|
|
typeof value === 'function' ||
|
|
typeof value === 'symbol';
|
|
|
|
def_eval(AST_Binary, function (compressor, depth) {
|
|
if (!non_converting_binary.has(this.operator)) depth++;
|
|
|
|
var left = this.left._eval(compressor, depth);
|
|
if (left === this.left) return this;
|
|
var right = this.right._eval(compressor, depth);
|
|
if (right === this.right) return this;
|
|
|
|
if (
|
|
left != null &&
|
|
right != null &&
|
|
identity_comparison.has(this.operator) &&
|
|
has_identity(left) &&
|
|
has_identity(right) &&
|
|
typeof left === typeof right
|
|
) {
|
|
// Do not compare by reference
|
|
return this;
|
|
}
|
|
|
|
// Do not mix BigInt and Number; Don't use `>>>` on BigInt or `/ 0n`
|
|
if (
|
|
(typeof left === 'bigint') !== (typeof right === 'bigint') ||
|
|
(typeof left === 'bigint' &&
|
|
(this.operator === '>>>' ||
|
|
(this.operator === '/' && Number(right) === 0)))
|
|
) {
|
|
return this;
|
|
}
|
|
|
|
var result;
|
|
switch (this.operator) {
|
|
case '&&':
|
|
result = left && right;
|
|
break;
|
|
case '||':
|
|
result = left || right;
|
|
break;
|
|
case '??':
|
|
result = left != null ? left : right;
|
|
break;
|
|
case '|':
|
|
result = left | right;
|
|
break;
|
|
case '&':
|
|
result = left & right;
|
|
break;
|
|
case '^':
|
|
result = left ^ right;
|
|
break;
|
|
case '+':
|
|
result = left + right;
|
|
break;
|
|
case '*':
|
|
result = left * right;
|
|
break;
|
|
case '**':
|
|
result = left ** right;
|
|
break;
|
|
case '/':
|
|
result = left / right;
|
|
break;
|
|
case '%':
|
|
result = left % right;
|
|
break;
|
|
case '-':
|
|
result = left - right;
|
|
break;
|
|
case '<<':
|
|
result = left << right;
|
|
break;
|
|
case '>>':
|
|
result = left >> right;
|
|
break;
|
|
case '>>>':
|
|
result = left >>> right;
|
|
break;
|
|
case '==':
|
|
result = left == right;
|
|
break;
|
|
case '===':
|
|
result = left === right;
|
|
break;
|
|
case '!=':
|
|
result = left != right;
|
|
break;
|
|
case '!==':
|
|
result = left !== right;
|
|
break;
|
|
case '<':
|
|
result = left < right;
|
|
break;
|
|
case '<=':
|
|
result = left <= right;
|
|
break;
|
|
case '>':
|
|
result = left > right;
|
|
break;
|
|
case '>=':
|
|
result = left >= right;
|
|
break;
|
|
default:
|
|
return this;
|
|
}
|
|
if (
|
|
typeof result === 'number' &&
|
|
isNaN(result) &&
|
|
compressor.find_parent(AST_With)
|
|
) {
|
|
// leave original expression as is
|
|
return this;
|
|
}
|
|
return result;
|
|
});
|
|
|
|
def_eval(AST_Conditional, function (compressor, depth) {
|
|
var condition = this.condition._eval(compressor, depth);
|
|
if (condition === this.condition) return this;
|
|
var node = condition ? this.consequent : this.alternative;
|
|
var value = node._eval(compressor, depth);
|
|
return value === node ? this : value;
|
|
});
|
|
|
|
// Set of AST_SymbolRef which are currently being evaluated.
|
|
// Avoids infinite recursion of ._eval()
|
|
const reentrant_ref_eval = new Set();
|
|
def_eval(AST_SymbolRef, function (compressor, depth) {
|
|
if (reentrant_ref_eval.has(this)) return this;
|
|
|
|
var fixed = this.fixed_value();
|
|
if (!fixed) return this;
|
|
|
|
reentrant_ref_eval.add(this);
|
|
const value = fixed._eval(compressor, depth);
|
|
reentrant_ref_eval.delete(this);
|
|
|
|
if (value === fixed) return this;
|
|
|
|
if (value && typeof value == 'object') {
|
|
var escaped = this.definition().escaped;
|
|
if (escaped && depth > escaped) return this;
|
|
}
|
|
return value;
|
|
});
|
|
|
|
const global_objs = { Array, Math, Number, Object, String };
|
|
|
|
const regexp_flags = new Set([
|
|
'dotAll',
|
|
'global',
|
|
'ignoreCase',
|
|
'multiline',
|
|
'sticky',
|
|
'unicode',
|
|
]);
|
|
|
|
def_eval(AST_PropAccess, function (compressor, depth) {
|
|
let obj = this.expression._eval(compressor, depth + 1);
|
|
if (obj === nullish || (this.optional && obj == null)) return nullish;
|
|
|
|
// `.length` of strings and arrays is always safe
|
|
if (this.property === 'length') {
|
|
if (typeof obj === 'string') {
|
|
return obj.length;
|
|
}
|
|
|
|
const is_spreadless_array =
|
|
obj instanceof AST_Array &&
|
|
obj.elements.every((el) => !(el instanceof AST_Expansion));
|
|
|
|
if (
|
|
is_spreadless_array &&
|
|
obj.elements.every((el) => !el.has_side_effects(compressor))
|
|
) {
|
|
return obj.elements.length;
|
|
}
|
|
}
|
|
|
|
if (compressor.option('unsafe')) {
|
|
var key = this.property;
|
|
if (key instanceof AST_Node) {
|
|
key = key._eval(compressor, depth);
|
|
if (key === this.property) return this;
|
|
}
|
|
|
|
var exp = this.expression;
|
|
if (is_undeclared_ref(exp)) {
|
|
var aa;
|
|
var first_arg =
|
|
exp.name === 'hasOwnProperty' &&
|
|
key === 'call' &&
|
|
(aa = compressor.parent() && compressor.parent().args) &&
|
|
aa &&
|
|
aa[0] &&
|
|
aa[0].evaluate(compressor);
|
|
|
|
first_arg =
|
|
first_arg instanceof AST_Dot ? first_arg.expression : first_arg;
|
|
|
|
if (
|
|
first_arg == null ||
|
|
(first_arg.thedef && first_arg.thedef.undeclared)
|
|
) {
|
|
return this.clone();
|
|
}
|
|
if (!is_pure_native_value(exp.name, key)) return this;
|
|
obj = global_objs[exp.name];
|
|
} else {
|
|
if (obj instanceof RegExp) {
|
|
if (key == 'source') {
|
|
return regexp_source_fix(obj.source);
|
|
} else if (key == 'flags' || regexp_flags.has(key)) {
|
|
return obj[key];
|
|
}
|
|
}
|
|
if (!obj || obj === exp || !HOP(obj, key)) return this;
|
|
|
|
if (typeof obj == 'function')
|
|
switch (key) {
|
|
case 'name':
|
|
return obj.node.name ? obj.node.name.name : '';
|
|
case 'length':
|
|
return obj.node.length_property();
|
|
default:
|
|
return this;
|
|
}
|
|
}
|
|
return obj[key];
|
|
}
|
|
return this;
|
|
});
|
|
|
|
def_eval(AST_Chain, function (compressor, depth) {
|
|
const evaluated = this.expression._eval(compressor, depth);
|
|
return (
|
|
evaluated === nullish ? undefined
|
|
: evaluated === this.expression ? this
|
|
: evaluated
|
|
);
|
|
});
|
|
|
|
def_eval(AST_Call, function (compressor, depth) {
|
|
var exp = this.expression;
|
|
|
|
const callee = exp._eval(compressor, depth);
|
|
if (callee === nullish || (this.optional && callee == null)) return nullish;
|
|
|
|
if (compressor.option('unsafe') && exp instanceof AST_PropAccess) {
|
|
var key = exp.property;
|
|
if (key instanceof AST_Node) {
|
|
key = key._eval(compressor, depth);
|
|
if (key === exp.property) return this;
|
|
}
|
|
var val;
|
|
var e = exp.expression;
|
|
if (is_undeclared_ref(e)) {
|
|
var first_arg =
|
|
e.name === 'hasOwnProperty' &&
|
|
key === 'call' &&
|
|
this.args[0] &&
|
|
this.args[0].evaluate(compressor);
|
|
|
|
first_arg =
|
|
first_arg instanceof AST_Dot ? first_arg.expression : first_arg;
|
|
|
|
if (
|
|
first_arg == null ||
|
|
(first_arg.thedef && first_arg.thedef.undeclared)
|
|
) {
|
|
return this.clone();
|
|
}
|
|
if (!is_pure_native_fn(e.name, key)) return this;
|
|
val = global_objs[e.name];
|
|
} else {
|
|
val = e._eval(compressor, depth + 1);
|
|
if (val === e || !val) return this;
|
|
if (!is_pure_native_method(val.constructor.name, key)) return this;
|
|
}
|
|
var args = [];
|
|
for (var i = 0, len = this.args.length; i < len; i++) {
|
|
var arg = this.args[i];
|
|
var value = arg._eval(compressor, depth);
|
|
if (arg === value) return this;
|
|
if (arg instanceof AST_Lambda) return this;
|
|
args.push(value);
|
|
}
|
|
try {
|
|
return val[key].apply(val, args);
|
|
} catch (ex) {
|
|
// We don't really care
|
|
}
|
|
}
|
|
return this;
|
|
});
|
|
|
|
// Also a subclass of AST_Call
|
|
def_eval(AST_New, return_this);
|