通过 ast_walker 来操作 AST
by lifesinger
at 2011-09-29 13:34:46
original http://lifesinger.wordpress.com/2011/09/29/ast-walker/
通过 UglifyJS 可以得到 js 代码的 AST:
var fs = require('fs'); var jsp = require('uglify-js').parser; var code = fs.readFileSync('test.js', 'utf-8'); var ast = jsp.parse(code);
得到的 ast 是以数组形式表示的抽象语法树。
在 spm 项目里,要提取 js 模块里的依赖信息。最裸的做法是通过正则来实现:
function parseStatic(inputFile, charset) { var ast = getAst(inputFile, charset); var deps; var pattern = /,stat,call,name,define,(?:(?:[^,]+,){2})?array(,.*?,)(?:function|name),/; var match = ast.toString().match(pattern); if (match && match[1]) { deps = []; var t = match[1].match(/,string,[^,]+/g); if (t && t.length) { deps = t.map(function(s) { // s: ,string,xxx return s.slice(8); }); } } return deps; }
正则的实现方式在绝大部分情况下不会有问题,但代码的可读性和扩展性不好。
查看 UglifyJS 的源码,发现内部提供了 ast_walker 方法。我们可以简单封装成 Ast.js:
var uglifyjs = require('uglify-js'); var pro = uglifyjs.uglify; var Ast = exports; Ast.walk = function(ast, type, walker) { var w = pro.ast_walker(); var walkers = {}; walkers[type] = function() { walker(this); }; ast = w.with_walkers(walkers, function() { return w.walk(ast); }); return ast; };
这样,获取依赖信息的代码可以调整为:
function parseStatic(inputFile, charset) { var ast = getAst(inputFile, charset); var deps, times = 0; Ast.walk(ast, 'stat', function(stat) { if (stat.toString().indexOf('stat,call,name,define,') !== 0) { return stat; } if (++times > 1) { throw 'Found multiple "define" in one module file. It is NOT allowed.'; } // stat[1]: // [ 'call', // [ 'name', 'define' ], // [ [Object], [Object], [Object ] ] ] var argsAst = stat[1][2]; var depsAst; // argsAst: // [ [ 'string', 'a' ], // [ 'array', [ [Object], [Object] ] ], // [ 'function', null, [], [] ] ] argsAst.some(function(item) { // NOTICE: the deps MUST be literal, it can NOT be a reference. if (item[0] === 'array') { depsAst = item[1]; return true; } }); if (!depsAst) { return stat; } // depsAst: // [ [ 'string', 'b' ], [ 'string', 'c' ] ] deps = []; depsAst.forEach(function(item) { if (item[0] === 'string') { deps.push(item[1]); } }); return stat; }); return deps; }
详见:Dependences.js. 功能更可靠,可读性也更好了。
还可以通过 UglifyJS 隐藏的第三参数来让 AST 带上 type/value/start/end 等信息:
var ast = jsp.parse(code, false, true);
通过 ast_walker 来操作 AST, 可以完成很多意想不到的工作。比如获取某个闭包里的所有局部变量、删除某个特定函数、或者给所有 constructor 添加上调试信息等。老赵的 Jscex 也是基于这一原理,有兴趣的可以深入挖掘。