Merge branch 'propmangle'

This commit is contained in:
Mihai Bazon
2015-03-29 12:50:43 +03:00
9 changed files with 6102 additions and 11 deletions

77
bin/extract-props.js Executable file
View File

@@ -0,0 +1,77 @@
#! /usr/bin/env node
"use strict";
var U2 = require("../tools/node");
var fs = require("fs");
var yargs = require("yargs");
var ARGS = yargs
.describe("o", "Output file")
.argv;
var files = ARGS._.slice();
var output = {
vars: {},
props: {}
};
if (ARGS.o) try {
output = JSON.parse(fs.readFileSync(ARGS.o, "utf8"));
} catch(ex) {}
files.forEach(getProps);
if (ARGS.o) {
fs.writeFileSync(ARGS.o, JSON.stringify(output, null, 2), "utf8");
} else {
console.log("%s", JSON.stringify(output, null, 2));
}
function getProps(filename) {
var code = fs.readFileSync(filename, "utf8");
var ast = U2.parse(code);
ast.walk(new U2.TreeWalker(function(node){
if (node instanceof U2.AST_ObjectKeyVal) {
add(node.key);
}
else if (node instanceof U2.AST_ObjectProperty) {
add(node.key.name);
}
else if (node instanceof U2.AST_Dot) {
add(node.property);
}
else if (node instanceof U2.AST_Sub) {
addStrings(node.property);
}
}));
function addStrings(node) {
var out = {};
try {
(function walk(node){
node.walk(new U2.TreeWalker(function(node){
if (node instanceof U2.AST_Seq) {
walk(node.cdr);
return true;
}
if (node instanceof U2.AST_String) {
add(node.value);
return true;
}
if (node instanceof U2.AST_Conditional) {
walk(node.consequent);
walk(node.alternative);
return true;
}
throw out;
}));
})(node);
} catch(ex) {
if (ex !== out) throw ex;
}
}
function add(name) {
output.props[name] = true;
}
}

View File

@@ -67,6 +67,10 @@ You need to pass an argument to this option to specify the name that your module
.describe("bare-returns", "Allow return outside of functions. Useful when minifying CommonJS modules.") .describe("bare-returns", "Allow return outside of functions. Useful when minifying CommonJS modules.")
.describe("keep-fnames", "Do not mangle/drop function names. Useful for code relying on Function.prototype.name.") .describe("keep-fnames", "Do not mangle/drop function names. Useful for code relying on Function.prototype.name.")
.describe("quotes", "Quote style (0 - auto, 1 - single, 2 - double, 3 - original)") .describe("quotes", "Quote style (0 - auto, 1 - single, 2 - double, 3 - original)")
.describe("reserved-file", "File containing reserved names")
.describe("reserve-domprops", "Make (most?) DOM properties reserved for --mangle-props")
.describe("mangle-props", "Mangle property names")
.describe("name-cache", "File to hold mangled names mappings")
.alias("p", "prefix") .alias("p", "prefix")
.alias("o", "output") .alias("o", "output")
@@ -91,6 +95,8 @@ You need to pass an argument to this option to specify the name that your module
.string("comments") .string("comments")
.string("wrap") .string("wrap")
.string("p") .string("p")
.string("name-cache")
.array("reserved-file")
.boolean("expr") .boolean("expr")
.boolean("source-map-include-sources") .boolean("source-map-include-sources")
@@ -106,6 +112,8 @@ You need to pass an argument to this option to specify the name that your module
.boolean("noerr") .boolean("noerr")
.boolean("bare-returns") .boolean("bare-returns")
.boolean("keep-fnames") .boolean("keep-fnames")
.boolean("mangle-props")
.boolean("reserve-domprops")
.wrap(80) .wrap(80)
@@ -144,6 +152,15 @@ if (ARGS.acorn) {
var COMPRESS = getOptions("c", true); var COMPRESS = getOptions("c", true);
var MANGLE = getOptions("m", true); var MANGLE = getOptions("m", true);
var BEAUTIFY = getOptions("b", true); var BEAUTIFY = getOptions("b", true);
var RESERVED = null;
if (ARGS.reserved_file) ARGS.reserved_file.forEach(function(filename){
RESERVED = UglifyJS.readReservedFile(filename, RESERVED);
});
if (ARGS.reserve_domprops) {
RESERVED = UglifyJS.readDefaultReservedFile(RESERVED);
}
if (ARGS.d) { if (ARGS.d) {
if (COMPRESS) COMPRESS.global_defs = getOptions("d"); if (COMPRESS) COMPRESS.global_defs = getOptions("d");
@@ -153,6 +170,19 @@ if (ARGS.r) {
if (MANGLE) MANGLE.except = ARGS.r.replace(/^\s+|\s+$/g).split(/\s*,+\s*/); if (MANGLE) MANGLE.except = ARGS.r.replace(/^\s+|\s+$/g).split(/\s*,+\s*/);
} }
if (RESERVED && MANGLE) {
if (!MANGLE.except) MANGLE.except = RESERVED.vars;
else MANGLE.except = MANGLE.except.concat(RESERVED.vars);
}
function readNameCache(key) {
return UglifyJS.readNameCache(ARGS.name_cache, key);
}
function writeNameCache(key, cache) {
return UglifyJS.writeNameCache(ARGS.name_cache, key, cache);
}
if (ARGS.quotes === true) { if (ARGS.quotes === true) {
ARGS.quotes = 3; ARGS.quotes = 3;
} }
@@ -334,11 +364,22 @@ async.eachLimit(files, 1, function (file, cb) {
TOPLEVEL = TOPLEVEL.wrap_enclose(arg_parameter_list); TOPLEVEL = TOPLEVEL.wrap_enclose(arg_parameter_list);
} }
if (ARGS.mangle_props) (function(){
var reserved = RESERVED ? RESERVED.props : null;
var cache = readNameCache("props");
TOPLEVEL = UglifyJS.mangle_properties(TOPLEVEL, {
reserved: reserved,
cache: cache
});
writeNameCache("props", cache);
})();
var SCOPE_IS_NEEDED = COMPRESS || MANGLE || ARGS.lint; var SCOPE_IS_NEEDED = COMPRESS || MANGLE || ARGS.lint;
var TL_CACHE = readNameCache("vars");
if (SCOPE_IS_NEEDED) { if (SCOPE_IS_NEEDED) {
time_it("scope", function(){ time_it("scope", function(){
TOPLEVEL.figure_out_scope({ screw_ie8: ARGS.screw_ie8 }); TOPLEVEL.figure_out_scope({ screw_ie8: ARGS.screw_ie8, cache: TL_CACHE });
if (ARGS.lint) { if (ARGS.lint) {
TOPLEVEL.scope_warnings(); TOPLEVEL.scope_warnings();
} }
@@ -353,17 +394,20 @@ async.eachLimit(files, 1, function (file, cb) {
if (SCOPE_IS_NEEDED) { if (SCOPE_IS_NEEDED) {
time_it("scope", function(){ time_it("scope", function(){
TOPLEVEL.figure_out_scope({ screw_ie8: ARGS.screw_ie8 }); TOPLEVEL.figure_out_scope({ screw_ie8: ARGS.screw_ie8, cache: TL_CACHE });
if (MANGLE) { if (MANGLE && !TL_CACHE) {
TOPLEVEL.compute_char_frequency(MANGLE); TOPLEVEL.compute_char_frequency(MANGLE);
} }
}); });
} }
if (MANGLE) time_it("mangle", function(){ if (MANGLE) time_it("mangle", function(){
MANGLE.cache = TL_CACHE;
TOPLEVEL.mangle_names(MANGLE); TOPLEVEL.mangle_names(MANGLE);
}); });
writeNameCache("vars", TL_CACHE);
if (ARGS.source_map_include_sources) { if (ARGS.source_map_include_sources) {
for (var file in SOURCES_CONTENT) { for (var file in SOURCES_CONTENT) {
if (SOURCES_CONTENT.hasOwnProperty(file)) { if (SOURCES_CONTENT.hasOwnProperty(file)) {
@@ -418,9 +462,9 @@ function normalize(o) {
function getOptions(x, constants) { function getOptions(x, constants) {
x = ARGS[x]; x = ARGS[x];
if (!x) return null; if (x == null) return null;
var ret = {}; var ret = {};
if (x !== true) { if (x !== "") {
var ast; var ast;
try { try {
ast = UglifyJS.parse(x, { expression: true }); ast = UglifyJS.parse(x, { expression: true });

212
lib/propmangle.js Normal file
View File

@@ -0,0 +1,212 @@
/***********************************************************************
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";
function find_builtins() {
var a = [];
[ Object, Array, Function, Number,
String, Boolean, Error, Math,
Date, RegExp
].forEach(function(ctor){
Object.getOwnPropertyNames(ctor).map(add);
if (ctor.prototype) {
Object.getOwnPropertyNames(ctor.prototype).map(add);
}
});
function add(name) {
push_uniq(a, name);
}
return a;
}
function mangle_properties(ast, options) {
options = defaults(options, {
reserved : null,
cache : null
});
var reserved = options.reserved;
if (reserved == null)
reserved = find_builtins();
var cache = options.cache;
if (cache == null) {
cache = {
cname: -1,
props: new Dictionary()
};
}
var names_to_mangle = [];
// step 1: find candidates to mangle
ast.walk(new TreeWalker(function(node){
if (node instanceof AST_ObjectKeyVal) {
add(node.key);
}
else if (node instanceof AST_ObjectProperty) {
// setter or getter, since KeyVal is handled above
add(node.key.name);
}
else if (node instanceof AST_Dot) {
if (this.parent() instanceof AST_Assign) {
add(node.property);
}
}
else if (node instanceof AST_Sub) {
if (this.parent() instanceof AST_Assign) {
addStrings(node.property);
}
}
}));
// step 2: transform the tree, renaming properties
return ast.transform(new TreeTransformer(null, function(node){
if (node instanceof AST_ObjectKeyVal) {
if (should_mangle(node.key)) {
node.key = mangle(node.key);
}
}
else if (node instanceof AST_ObjectProperty) {
// setter or getter
if (should_mangle(node.key.name)) {
node.key.name = mangle(node.key.name);
}
}
else if (node instanceof AST_Dot) {
if (should_mangle(node.property)) {
node.property = mangle(node.property);
}
}
else if (node instanceof AST_Sub) {
node.property = mangleStrings(node.property);
}
// else if (node instanceof AST_String) {
// if (should_mangle(node.value)) {
// AST_Node.warn(
// "Found \"{prop}\" property candidate for mangling in an arbitrary string [{file}:{line},{col}]", {
// file : node.start.file,
// line : node.start.line,
// col : node.start.col,
// prop : node.value
// }
// );
// }
// }
}));
// only function declarations after this line
function can_mangle(name) {
if (reserved.indexOf(name) >= 0) return false;
if (/^[0-9.]+$/.test(name)) return false;
return true;
}
function should_mangle(name) {
return cache.props.has(name)
|| names_to_mangle.indexOf(name) >= 0;
}
function add(name) {
if (can_mangle(name))
push_uniq(names_to_mangle, name);
}
function mangle(name) {
var mangled = cache.props.get(name);
if (!mangled) {
do {
mangled = base54(++cache.cname);
} while (!can_mangle(mangled));
cache.props.set(name, mangled);
}
return mangled;
}
function addStrings(node) {
var out = {};
try {
(function walk(node){
node.walk(new TreeWalker(function(node){
if (node instanceof AST_Seq) {
walk(node.cdr);
return true;
}
if (node instanceof AST_String) {
add(node.value);
return true;
}
if (node instanceof AST_Conditional) {
walk(node.consequent);
walk(node.alternative);
return true;
}
throw out;
}));
})(node);
} catch(ex) {
if (ex !== out) throw ex;
}
}
function mangleStrings(node) {
return node.transform(new TreeTransformer(function(node){
if (node instanceof AST_Seq) {
node.cdr = mangleStrings(node.cdr);
}
else if (node instanceof AST_String) {
if (should_mangle(node.value)) {
node.value = mangle(node.value);
}
}
else if (node instanceof AST_Conditional) {
node.consequent = mangleStrings(node.consequent);
node.alternative = mangleStrings(node.alternative);
}
return node;
}));
}
}

View File

@@ -67,18 +67,26 @@ SymbolDef.prototype = {
|| this.orig[0] instanceof AST_SymbolDefun)); || this.orig[0] instanceof AST_SymbolDefun));
}, },
mangle: function(options) { mangle: function(options) {
if (!this.mangled_name && !this.unmangleable(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 s = this.scope; var s = this.scope;
if (!options.screw_ie8 && this.orig[0] instanceof AST_SymbolLambda) if (!options.screw_ie8 && this.orig[0] instanceof AST_SymbolLambda)
s = s.parent_scope; s = s.parent_scope;
this.mangled_name = s.next_mangled(options, this); this.mangled_name = s.next_mangled(options, this);
if (this.global && cache) {
cache.set(this.name, this.mangled_name);
}
} }
} }
}; };
AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){
options = defaults(options, { options = defaults(options, {
screw_ie8: false screw_ie8: false,
cache: null
}); });
// pass 1: setup scope chaining and handle definitions // pass 1: setup scope chaining and handle definitions
@@ -183,6 +191,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){
} }
}); });
self.walk(tw); self.walk(tw);
if (options.cache) {
this.cname = options.cache.cname;
}
}); });
AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){
@@ -344,6 +356,15 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){
// the AST_SymbolDeclaration that it points to). // the AST_SymbolDeclaration that it points to).
var lname = -1; var lname = -1;
var to_mangle = []; var to_mangle = [];
if (options.cache) {
this.globals.each(function(symbol){
if (options.except.indexOf(symbol.name) < 0) {
to_mangle.push(symbol);
}
});
}
var tw = new TreeWalker(function(node, descend){ var tw = new TreeWalker(function(node, descend){
if (node instanceof AST_LabeledStatement) { if (node instanceof AST_LabeledStatement) {
// lname is incremented when we get to the AST_Label // lname is incremented when we get to the AST_Label
@@ -378,6 +399,10 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){
}); });
this.walk(tw); this.walk(tw);
to_mangle.forEach(function(def){ def.mangle(options) }); to_mangle.forEach(function(def){ def.mangle(options) });
if (options.cache) {
options.cache.cname = this.cname;
}
}); });
AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options){ AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options){

View File

@@ -106,10 +106,12 @@ function defaults(args, defs, croak) {
}; };
function merge(obj, ext) { function merge(obj, ext) {
var count = 0;
for (var i in ext) if (ext.hasOwnProperty(i)) { for (var i in ext) if (ext.hasOwnProperty(i)) {
obj[i] = ext[i]; obj[i] = ext[i];
count++;
} }
return obj; return count;
}; };
function noop() {}; function noop() {};
@@ -298,5 +300,11 @@ Dictionary.prototype = {
for (var i in this._values) for (var i in this._values)
ret.push(f(this._values[i], i.substr(1))); ret.push(f(this._values[i], i.substr(1)));
return ret; return ret;
} },
toObject: function() { return this._values }
};
Dictionary.fromObject = function(obj) {
var dict = new Dictionary();
dict._size = merge(dict._values, obj);
return dict;
}; };

View File

@@ -17,7 +17,7 @@
"dependencies": { "dependencies": {
"async" : "~0.2.6", "async" : "~0.2.6",
"source-map" : "0.1.34", "source-map" : "0.1.34",
"yargs": "~1.3.3", "yargs": "~3.5.4",
"uglify-to-browserify": "~1.0.0" "uglify-to-browserify": "~1.0.0"
}, },
"devDependencies": { "devDependencies": {

5603
tools/domprops.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,8 @@ var FILES = exports.FILES = [
"../lib/output.js", "../lib/output.js",
"../lib/compress.js", "../lib/compress.js",
"../lib/sourcemap.js", "../lib/sourcemap.js",
"../lib/mozilla-ast.js" "../lib/mozilla-ast.js",
"../lib/propmangle.js"
].map(function(file){ ].map(function(file){
return fs.realpathSync(path.join(path.dirname(__filename), file)); return fs.realpathSync(path.join(path.dirname(__filename), file));
}); });
@@ -192,3 +193,63 @@ exports.describe_ast = function() {
doitem(UglifyJS.AST_Node); doitem(UglifyJS.AST_Node);
return out + ""; return out + "";
}; };
function readReservedFile(filename, reserved) {
if (!reserved) {
reserved = { vars: [], props: [] };
}
var data = fs.readFileSync(filename, "utf8");
data = JSON.parse(data);
if (data.vars) {
data.vars.forEach(function(name){
UglifyJS.push_uniq(reserved.vars, name);
});
}
if (data.props) {
data.props.forEach(function(name){
UglifyJS.push_uniq(reserved.props, name);
});
}
return reserved;
}
exports.readReservedFile = readReservedFile;
exports.readDefaultReservedFile = function(reserved) {
return readReservedFile(path.join(__dirname, "domprops.json"), reserved);
};
exports.readNameCache = function(filename, key) {
var cache = null;
if (filename) {
try {
var cache = fs.readFileSync(filename, "utf8");
cache = JSON.parse(cache)[key];
if (!cache) throw "init";
cache.props = UglifyJS.Dictionary.fromObject(cache.props);
} catch(ex) {
cache = {
cname: -1,
props: new UglifyJS.Dictionary()
};
}
}
return cache;
};
exports.writeNameCache = function(filename, key, cache) {
if (filename) {
var data;
try {
data = fs.readFileSync(filename, "utf8");
data = JSON.parse(data);
} catch(ex) {
data = {};
}
data[key] = {
cname: cache.cname,
props: cache.props.toObject()
};
fs.writeFileSync(filename, JSON.stringify(data, null, 2), "utf8");
}
};

61
tools/props.html Normal file
View File

@@ -0,0 +1,61 @@
<html>
<head>
</head>
<body>
<script>(function(){
var props = {};
function addObject(obj) {
if (obj == null) return;
try {
Object.getOwnPropertyNames(obj).forEach(add);
} catch(ex) {}
if (obj.prototype) {
Object.getOwnPropertyNames(obj.prototype).forEach(add);
}
if (typeof obj == "function") {
try {
Object.getOwnPropertyNames(new obj).forEach(add);
} catch(ex) {}
}
}
function add(name) {
props[name] = true;
}
Object.getOwnPropertyNames(window).forEach(function(thing){
addObject(window[thing]);
});
try {
addObject(new Event("click"));
addObject(new Event("contextmenu"));
addObject(new Event("mouseup"));
addObject(new Event("mousedown"));
addObject(new Event("keydown"));
addObject(new Event("keypress"));
addObject(new Event("keyup"));
} catch(ex) {}
var ta = document.createElement("textarea");
ta.style.width = "100%";
ta.style.height = "20em";
ta.style.boxSizing = "border-box";
<!-- ta.value = Object.keys(props).sort(cmp).map(function(name){ -->
<!-- return JSON.stringify(name); -->
<!-- }).join(",\n"); -->
ta.value = JSON.stringify({
vars: [],
props: Object.keys(props).sort(cmp)
}, null, 2);
document.body.appendChild(ta);
function cmp(a, b) {
a = a.toLowerCase();
b = b.toLowerCase();
return a < b ? -1 : a > b ? 1 : 0;
}
})();</script>
</body>
</html>