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.
		
		
		
		
		
			
		
			
				
					470 lines
				
				15 KiB
			
		
		
			
		
	
	
					470 lines
				
				15 KiB
			| 
											3 years ago
										 | /*********************************************************************** | ||
|  | 
 | ||
|  |   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 | ||
|  |             && this.expression instanceof AST_Constant | ||
|  |             && unaryPrefix.has(this.operator); | ||
|  |     } | ||
|  | }); | ||
|  | 
 | ||
|  | 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(); | ||
|  | }); | ||
|  | 
 | ||
|  | def_eval(AST_BigInt, 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; | ||
|  |     // Function would be evaluated to an array and so typeof would
 | ||
|  |     // incorrectly return 'object'. Hence making is a special case.
 | ||
|  |     if (compressor.option("typeofs") | ||
|  |         && this.operator == "typeof" | ||
|  |         && (e instanceof AST_Lambda | ||
|  |             || e instanceof AST_SymbolRef | ||
|  |             && e.fixed_value() instanceof AST_Lambda)) { | ||
|  |         return typeof function () { }; | ||
|  |     } | ||
|  |     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; | ||
|  |     var result; | ||
|  | 
 | ||
|  |     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; | ||
|  |     } | ||
|  | 
 | ||
|  |     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 = Math.pow(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 (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; | ||
|  |     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); |