UglifyJS/lib/scope.js
2018-06-13 12:50:54 +02:00

623 lines
20 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.
***********************************************************************/
"use strict";
var utils = require("./utils");
var defaults = utils.defaults;
var return_false = utils.return_false;
var return_this = utils.return_this;
var push_uniq = utils.push_uniq;
var makePredicate = utils.makePredicate;
var Dictionary = utils.Dictionary;
var merge = utils.merge;
var AST = require("./ast");
var TreeWalker = AST.TreeWalker;
var parse = require("./parse");
var is_identifier = parse.is_identifier;
var parse = parse.parse;
function SymbolDef(scope, orig, init) {
this.name = orig.name;
this.orig = [ orig ];
this.init = init;
this.eliminated = 0;
this.scope = scope;
this.references = [];
this.replaced = 0;
this.global = false;
this.mangled_name = null;
this.undeclared = false;
this.id = SymbolDef.next_id++;
}
SymbolDef.next_id = 1;
SymbolDef.prototype = {
unmangleable: function(options) {
if (!options) options = {};
return this.global && !options.toplevel
|| this.undeclared
|| !options.eval && this.scope.pinned()
|| options.keep_fnames
&& (this.orig[0] instanceof AST.SymbolLambda
|| this.orig[0] instanceof AST.SymbolDefun);
},
mangle: function(options) {
var cache = options.cache && options.cache.props;
if (this.global && cache && cache.has(this.name)) {
this.mangled_name = cache.get(this.name);
} else if (!this.mangled_name && !this.unmangleable(options)) {
var def;
if (def = this.redefined()) {
this.mangled_name = def.mangled_name || def.name;
} else {
this.mangled_name = next_mangled_name(this.scope, options, this);
}
if (this.global && cache) {
cache.set(this.name, this.mangled_name);
}
}
},
redefined: function() {
return this.defun && this.defun.variables.get(this.name);
}
};
AST.Toplevel.DEFMETHOD("figure_out_scope", function(options) {
options = defaults(options, {
cache: null,
ie8: false,
});
// pass 1: setup scope chaining and handle definitions
var self = this;
var scope = self.parent_scope = null;
var defun = null;
var tw = new TreeWalker(function(node, descend) {
if (node instanceof AST.Catch) {
var save_scope = scope;
scope = new AST.Scope(node);
scope.init_scope_vars(save_scope);
descend();
scope = save_scope;
return true;
}
if (node instanceof AST.Scope) {
node.init_scope_vars(scope);
var save_scope = scope;
var save_defun = defun;
defun = scope = node;
descend();
scope = save_scope;
defun = save_defun;
return true; // don't descend again in TreeWalker
}
if (node instanceof AST.With) {
for (var s = scope; s; s = s.parent_scope)
s.uses_with = true;
return;
}
if (node instanceof AST.Symbol) {
node.scope = scope;
}
if (node instanceof AST.Label) {
node.thedef = node;
node.references = [];
}
if (node instanceof AST.SymbolLambda) {
defun.def_function(node, node.name == "arguments" ? undefined : defun);
}
else if (node instanceof AST.SymbolDefun) {
// Careful here, the scope where this should be defined is
// the parent scope. The reason is that we enter a new
// scope when we encounter the AST.Defun node (which is
// instanceof AST.Scope) but we get to the symbol a bit
// later.
(node.scope = defun.parent_scope).def_function(node, defun);
}
else if (node instanceof AST.SymbolVar) {
defun.def_variable(node, node.TYPE == "SymbolVar" ? null : undefined);
if (defun !== scope) {
node.mark_enclosed(options);
var def = scope.find_variable(node);
if (node.thedef !== def) {
node.thedef = def;
}
node.reference(options);
}
}
else if (node instanceof AST.SymbolCatch) {
scope.def_variable(node).defun = defun;
}
});
self.walk(tw);
// pass 2: find back references and eval
self.globals = new Dictionary();
var tw = new TreeWalker(function(node, descend) {
if (node instanceof AST.LoopControl && node.label) {
node.label.thedef.references.push(node);
return true;
}
if (node instanceof AST.SymbolRef) {
var name = node.name;
if (name == "eval" && tw.parent() instanceof AST.Call) {
for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) {
s.uses_eval = true;
}
}
var sym = node.scope.find_variable(name);
if (!sym) {
sym = self.def_global(node);
} else if (sym.scope instanceof AST.Lambda && name == "arguments") {
sym.scope.uses_arguments = true;
}
node.thedef = sym;
node.reference(options);
return true;
}
// ensure mangling works if catch reuses a scope variable
var def;
if (node instanceof AST.SymbolCatch && (def = node.definition().redefined())) {
var s = node.scope;
while (s) {
push_uniq(s.enclosed, def);
if (s === def.scope) break;
s = s.parent_scope;
}
}
});
self.walk(tw);
// pass 3: fix up any scoping issue with IE8
if (options.ie8) {
self.walk(new TreeWalker(function(node, descend) {
if (node instanceof AST.SymbolCatch) {
var name = node.name;
var refs = node.thedef.references;
var scope = node.thedef.defun;
var def = scope.find_variable(name) || self.globals.get(name) || scope.def_variable(node);
refs.forEach(function(ref) {
ref.thedef = def;
ref.reference(options);
});
node.thedef = def;
node.reference(options);
return true;
}
}));
}
});
AST.Toplevel.DEFMETHOD("def_global", function(node) {
var globals = this.globals, name = node.name;
if (globals.has(name)) {
return globals.get(name);
} else {
var g = new SymbolDef(this, node);
g.undeclared = true;
g.global = true;
globals.set(name, g);
return g;
}
});
AST.Scope.DEFMETHOD("init_scope_vars", function(parent_scope) {
this.variables = new Dictionary(); // map name to AST.SymbolVar (variables defined in this scope; includes functions)
this.functions = new Dictionary(); // map name to AST.SymbolDefun (functions defined in this scope)
this.uses_with = false; // will be set to true if this or some nested scope uses the `with` statement
this.uses_eval = false; // will be set to true if this or nested scope uses the global `eval`
this.parent_scope = parent_scope; // the parent scope
this.enclosed = []; // a list of variables from this or outer scope(s) that are referenced from this or inner scopes
this.cname = -1; // the current index for mangling functions/variables
});
AST.Lambda.DEFMETHOD("init_scope_vars", function() {
AST.Scope.prototype.init_scope_vars.apply(this, arguments);
this.uses_arguments = false;
this.def_variable(new AST.SymbolFunarg({
name: "arguments",
start: this.start,
end: this.end
}));
});
AST.Symbol.DEFMETHOD("mark_enclosed", function(options) {
var def = this.definition();
var s = this.scope;
while (s) {
push_uniq(s.enclosed, def);
if (options.keep_fnames) {
s.functions.each(function(d) {
push_uniq(def.scope.enclosed, d);
});
}
if (s === def.scope) break;
s = s.parent_scope;
}
});
AST.Symbol.DEFMETHOD("reference", function(options) {
this.definition().references.push(this);
this.mark_enclosed(options);
});
AST.Scope.DEFMETHOD("find_variable", function(name) {
if (name instanceof AST.Symbol) name = name.name;
return this.variables.get(name)
|| (this.parent_scope && this.parent_scope.find_variable(name));
});
AST.Scope.DEFMETHOD("def_function", function(symbol, init) {
var def = this.def_variable(symbol, init);
if (!def.init || def.init instanceof AST.Defun) def.init = init;
this.functions.set(symbol.name, def);
return def;
});
AST.Scope.DEFMETHOD("def_variable", function(symbol, init) {
var def = this.variables.get(symbol.name);
if (def) {
def.orig.push(symbol);
if (def.init && (def.scope !== symbol.scope || def.init instanceof AST.Function)) {
def.init = init;
}
} else {
def = new SymbolDef(this, symbol, init);
this.variables.set(symbol.name, def);
def.global = !this.parent_scope;
}
return symbol.thedef = def;
});
function names_in_use(scope, options) {
var names = scope.names_in_use;
if (!names) {
scope.names_in_use = names = Object.create(scope.mangled_names || null);
scope.cname_holes = [];
scope.enclosed.forEach(function(def) {
if (def.unmangleable(options)) names[def.name] = true;
});
}
return names;
}
function next_mangled_name(scope, options, def) {
var in_use = names_in_use(scope, options);
var holes = scope.cname_holes;
var names = Object.create(null);
// #179, #326
// in Safari strict mode, something like (function x(x){...}) is a syntax error;
// a function expression's argument cannot shadow the function expression's name
if (scope instanceof AST.Function && scope.name && def.orig[0] instanceof AST.SymbolFunarg) {
var tricky_def = scope.name.definition();
// the function's mangled_name is null when keep_fnames is true
names[tricky_def.mangled_name || tricky_def.name] = true;
}
var scopes = [ scope ];
def.references.forEach(function(sym) {
var scope = sym.scope;
do {
if (scopes.indexOf(scope) < 0) {
for (var name in names_in_use(scope, options)) {
names[name] = true;
}
scopes.push(scope);
} else break;
} while (scope = scope.parent_scope);
});
var name;
for (var i = 0, len = holes.length; i < len; i++) {
name = base54(holes[i]);
if (names[name]) continue;
holes.splice(i, 1);
scope.names_in_use[name] = true;
return name;
}
while (true) {
name = base54(++scope.cname);
if (in_use[name] || !is_identifier(name) || options.reserved.has[name]) continue;
if (!names[name]) break;
holes.push(scope.cname);
}
scope.names_in_use[name] = true;
if (options.ie8 && def.orig[0] instanceof AST.SymbolLambda) {
names_in_use(scope.parent_scope, options)[name] = true;
}
return name;
}
AST.Symbol.DEFMETHOD("unmangleable", function(options) {
var def = this.definition();
return !def || def.unmangleable(options);
});
// labels are always mangleable
AST.Label.DEFMETHOD("unmangleable", return_false);
AST.Symbol.DEFMETHOD("unreferenced", function() {
return !this.definition().references.length && !this.scope.pinned();
});
AST.Symbol.DEFMETHOD("definition", function() {
return this.thedef;
});
AST.Symbol.DEFMETHOD("global", function() {
return this.definition().global;
});
function _default_mangler_options(options) {
options = defaults(options, {
eval : false,
ie8 : false,
keep_fnames : false,
reserved : [],
toplevel : false,
});
if (!Array.isArray(options.reserved)) options.reserved = [];
// Never mangle arguments
push_uniq(options.reserved, "arguments");
options.reserved.has = makePredicate(options.reserved);
return options;
}
AST.Toplevel.DEFMETHOD("mangle_names", function(options) {
options = _default_mangler_options(options);
// We only need to mangle declaration nodes. Special logic wired
// into the code generator will display the mangled name if it's
// present (and for AST.SymbolRef-s it'll use the mangled name of
// the AST.SymbolDeclaration that it points to).
var lname = -1;
if (options.cache && options.cache.props) {
var mangled_names = this.mangled_names = Object.create(null);
options.cache.props.each(function(mangled_name) {
mangled_names[mangled_name] = true;
});
}
var redefined = [];
var tw = new TreeWalker(function(node, descend) {
if (node instanceof AST.LabeledStatement) {
// lname is incremented when we get to the AST.Label
var save_nesting = lname;
descend();
lname = save_nesting;
return true; // don't descend again in TreeWalker
}
if (node instanceof AST.Scope) {
descend();
if (options.cache && node instanceof AST.Toplevel) {
node.globals.each(mangle);
}
node.variables.each(mangle);
return true;
}
if (node instanceof AST.Label) {
var name;
do name = base54(++lname); while (!is_identifier(name));
node.mangled_name = name;
return true;
}
if (!options.ie8 && node instanceof AST.Catch) {
var def = node.argname.definition();
var redef = def.redefined();
if (redef) {
redefined.push(def);
reference(node.argname);
def.references.forEach(reference);
}
descend();
if (!redef) mangle(def);
return true;
}
function reference(sym) {
sym.thedef = redef;
sym.reference(options);
sym.thedef = def;
}
});
this.walk(tw);
redefined.forEach(mangle);
function mangle(def) {
if (options.reserved.has[def.name]) return;
def.mangle(options);
}
});
AST.Toplevel.DEFMETHOD("find_colliding_names", function(options) {
var cache = options.cache && options.cache.props;
var avoid = Object.create(null);
options.reserved.forEach(to_avoid);
this.globals.each(add_def);
this.walk(new TreeWalker(function(node) {
if (node instanceof AST.Scope) node.variables.each(add_def);
if (node instanceof AST.SymbolCatch) add_def(node.definition());
}));
return avoid;
function to_avoid(name) {
avoid[name] = true;
}
function add_def(def) {
var name = def.name;
if (def.global && cache && cache.has(name)) name = cache.get(name);
else if (!def.unmangleable(options)) return;
to_avoid(name);
}
});
AST.Toplevel.DEFMETHOD("expand_names", function(options) {
base54.reset();
base54.sort();
options = _default_mangler_options(options);
var avoid = this.find_colliding_names(options);
var cname = 0;
this.globals.each(rename);
this.walk(new TreeWalker(function(node) {
if (node instanceof AST.Scope) node.variables.each(rename);
if (node instanceof AST.SymbolCatch) rename(node.definition());
}));
function next_name() {
var name;
do {
name = base54(cname++);
} while (avoid[name] || !is_identifier(name));
return name;
}
function rename(def) {
if (def.global && options.cache) return;
if (def.unmangleable(options)) return;
if (options.reserved.has[def.name]) return;
var d = def.redefined();
def.name = d ? d.name : next_name();
def.orig.forEach(function(sym) {
sym.name = def.name;
});
def.references.forEach(function(sym) {
sym.name = def.name;
});
}
});
AST.Node.DEFMETHOD("tail_node", return_this);
AST.Sequence.DEFMETHOD("tail_node", function() {
return this.expressions[this.expressions.length - 1];
});
AST.Toplevel.DEFMETHOD("compute_char_frequency", function(options) {
options = _default_mangler_options(options);
base54.reset();
try {
AST.Node.prototype.print = function(stream, force_parens) {
this._print(stream, force_parens);
if (this instanceof AST.Symbol && !this.unmangleable(options)) {
base54.consider(this.name, -1);
} else if (options.properties) {
if (this instanceof AST.Dot) {
base54.consider(this.property, -1);
} else if (this instanceof AST.Sub) {
skip_string(this.property);
}
}
};
base54.consider(this.print_to_string(), 1);
} finally {
AST.Node.prototype.print = AST.Node.prototype._print;
}
base54.sort();
function skip_string(node) {
if (node instanceof AST.String) {
base54.consider(node.value, -1);
} else if (node instanceof AST.Conditional) {
skip_string(node.consequent);
skip_string(node.alternative);
} else if (node instanceof AST.Sequence) {
skip_string(node.tail_node());
}
}
});
var base54 = (function() {
var freq = Object.create(null);
function init(chars) {
var array = [];
for (var i = 0, len = chars.length; i < len; i++) {
var ch = chars[i];
array.push(ch);
freq[ch] = -1e-2 * i;
}
return array;
}
var digits = init("0123456789");
var leading = init("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_");
var chars, frequency;
function reset() {
frequency = Object.create(freq);
}
base54.consider = function(str, delta) {
for (var i = str.length; --i >= 0;) {
frequency[str[i]] += delta;
}
};
function compare(a, b) {
return frequency[b] - frequency[a];
}
base54.sort = function() {
chars = leading.sort(compare).concat(digits.sort(compare));
};
base54.reset = reset;
reset();
function base54(num) {
var ret = "", base = 54;
num++;
do {
num--;
ret += chars[num % base];
num = Math.floor(num / base);
base = 64;
} while (num > 0);
return ret;
}
return base54;
})();
merge(exports, {
SymbolDef : SymbolDef,
names_in_use : names_in_use,
next_mangled_name : next_mangled_name,
base54 : base54,
});