├── example ├── wrap.js └── microwave.js ├── test ├── parent.js ├── microwave.js ├── err.js ├── label.js └── wrap.js ├── package.json ├── README.markdown └── index.js /example/wrap.js: -------------------------------------------------------------------------------- 1 | var burrito = require('burrito'); 2 | 3 | var src = burrito('f() && g(h())\nfoo()', function (node) { 4 | if (node.name === 'call') node.wrap('qqq(%s)'); 5 | }); 6 | 7 | console.log(src); 8 | -------------------------------------------------------------------------------- /example/microwave.js: -------------------------------------------------------------------------------- 1 | var burrito = require('burrito'); 2 | 3 | var res = burrito.microwave('Math.sin(2)', function (node) { 4 | if (node.name === 'num') node.wrap('Math.PI / %s'); 5 | }); 6 | 7 | console.log(res); // sin(pi / 2) == 1 8 | -------------------------------------------------------------------------------- /test/parent.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var burrito = require('burrito'); 3 | 4 | exports.checkParent = function () { 5 | var src = 'Math.tan(0) + Math.sin(0)'; 6 | 7 | var res = burrito.microwave(src, function (node) { 8 | if (node.name === 'binary') { 9 | node.wrap('%a - %b'); 10 | } 11 | else if (node.name === 'num') { 12 | assert.equal(node.parent().value[0][0], 'dot'); 13 | 14 | var fn = node.parent().value[0][2]; 15 | if (fn === 'sin') { 16 | node.wrap('Math.PI / 2'); 17 | } 18 | else if (fn === 'tan') { 19 | node.wrap('Math.PI / 4'); 20 | } 21 | else assert.fail('Unknown fn'); 22 | } 23 | }); 24 | 25 | assert.equal(res, Math.tan(Math.PI / 4) - Math.sin(Math.PI / 2)); // ~ 0 26 | }; 27 | -------------------------------------------------------------------------------- /test/microwave.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var burrito = require('burrito'); 3 | 4 | exports.microwave = function () { 5 | var times = 0; 6 | var context = { 7 | f : function (x) { return x + 1 }, 8 | g : function (x) { return x + 2 }, 9 | h : function (x) { return x + 3 }, 10 | z : function (x) { 11 | times ++; 12 | return x * 10; 13 | }, 14 | }; 15 | 16 | var res = burrito.microwave('f(g(h(5)))', context, function (node) { 17 | if (node.name === 'call') { 18 | node.wrap(function (s) { 19 | return 'z(' + s + ')'; 20 | }); 21 | } 22 | }); 23 | 24 | assert.equal(res, (((((5 + 3) * 10) + 2) * 10) + 1) * 10); 25 | assert.equal(times, 3); 26 | }; 27 | 28 | exports.emptyContext = function () { 29 | var res = burrito.microwave('Math.sin(2)', function (node) { 30 | if (node.name === 'num') node.wrap('Math.PI / %s'); 31 | }); 32 | assert.equal(res, 1); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "burrito", 3 | "description" : "Wrap up expressions with a trace function while walking the AST with rice and beans on the side", 4 | "version" : "0.2.6", 5 | "repository" : { 6 | "type" : "git", 7 | "url" : "git://github.com/substack/node-burrito.git" 8 | }, 9 | "main" : "./index.js", 10 | "keywords" : [ 11 | "trace", 12 | "ast", 13 | "walk", 14 | "syntax", 15 | "source", 16 | "tree", 17 | "uglify" 18 | ], 19 | "directories" : { 20 | "lib" : ".", 21 | "example" : "example", 22 | "test" : "test" 23 | }, 24 | "scripts" : { 25 | "test" : "expresso" 26 | }, 27 | "dependencies" : { 28 | "traverse" : ">=0.4.2 <0.5", 29 | "uglify-js" : "1.0.4" 30 | }, 31 | "devDependencies" : { 32 | "expresso" : "=0.7.x" 33 | }, 34 | "engines" : { 35 | "node" : ">=0.4.0" 36 | }, 37 | "license" : "BSD", 38 | "author" : { 39 | "name" : "James Halliday", 40 | "email" : "mail@substack.net", 41 | "url" : "http://substack.net" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/err.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var burrito = require('../'); 3 | 4 | exports.wrapError = function () { 5 | try { 6 | var src = burrito('f() && g()', function (node) { 7 | if (node.name === 'binary') node.wrap('h(%a, %b') 8 | }); 9 | assert.fail('should have blown up'); 10 | } 11 | catch (err) { 12 | assert.ok(err.message.match(/unexpected/i)); 13 | assert.ok(err instanceof SyntaxError); 14 | assert.ok(!err.stack.match(/uglify-js/)); 15 | assert.equal(err.line, 0); 16 | assert.equal(err.col, 10); 17 | assert.equal(err.pos, 10); 18 | } 19 | }; 20 | 21 | exports.nonString = function () { 22 | assert.throws(function () { 23 | burrito.parse(new Buffer('[]')); 24 | }); 25 | 26 | assert.throws(function () { 27 | burrito.parse(new String('[]')); 28 | }); 29 | 30 | assert.throws(function () { 31 | burrito.parse(); 32 | }); 33 | }; 34 | 35 | exports.syntaxError = function () { 36 | try { 37 | var src = burrito('f() && g())', function (node) { 38 | if (node.name === 'binary') node.wrap('h(%a, %b)') 39 | }); 40 | assert.fail('should have blown up'); 41 | } 42 | catch (err) { 43 | assert.ok(err.message.match(/unexpected/i)); 44 | assert.ok(err instanceof SyntaxError); 45 | assert.ok(!err.stack.match(/uglify-js/)); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /test/label.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var burrito = require('burrito'); 3 | 4 | exports.callLabel = function () { 5 | var times = 0; 6 | var src = burrito('foo(10)', function (node) { 7 | if (node.name === 'call') { 8 | assert.equal(node.label(), 'foo'); 9 | times ++; 10 | } 11 | }); 12 | 13 | assert.equal(times, 1); 14 | }; 15 | 16 | exports.varLabel = function () { 17 | var times = 0; 18 | var src = burrito('var x = 2', function (node) { 19 | if (node.name === 'var') { 20 | assert.deepEqual(node.label(), [ 'x' ]); 21 | times ++; 22 | } 23 | }); 24 | 25 | assert.equal(times, 1); 26 | }; 27 | 28 | exports.varsLabel = function () { 29 | var times = 0; 30 | var src = burrito('var x = 2, y = 3', function (node) { 31 | if (node.name === 'var') { 32 | assert.deepEqual(node.label(), [ 'x', 'y' ]); 33 | times ++; 34 | } 35 | }); 36 | 37 | assert.equal(times, 1); 38 | }; 39 | 40 | exports.defunLabel = function () { 41 | var times = 0; 42 | var src = burrito('function moo () {}', function (node) { 43 | if (node.name === 'defun') { 44 | assert.deepEqual(node.label(), 'moo'); 45 | times ++; 46 | } 47 | }); 48 | 49 | assert.equal(times, 1); 50 | }; 51 | 52 | exports.functionLabel = function () { 53 | var times = 0; 54 | var src = burrito('(function zzz () {})()', function (node) { 55 | if (node.name === 'function') { 56 | assert.deepEqual(node.label(), 'zzz'); 57 | times ++; 58 | } 59 | }); 60 | 61 | assert.equal(times, 1); 62 | }; 63 | 64 | exports.anonFunctionLabel = function () { 65 | var times = 0; 66 | var src = burrito('(function () {})()', function (node) { 67 | if (node.name === 'function') { 68 | assert.ok(node.label() === null); 69 | times ++; 70 | } 71 | }); 72 | 73 | assert.equal(times, 1); 74 | }; 75 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | burrito 2 | ======= 3 | 4 | Burrito makes it easy to do crazy stuff with the javascript AST. 5 | 6 | This is super useful if you want to roll your own stack traces or build a code 7 | coverage tool. 8 | 9 | ![node.wrap("burrito")](http://substack.net/images/burrito.png) 10 | 11 | examples 12 | ======== 13 | 14 | microwave 15 | --------- 16 | 17 | examples/microwave.js 18 | 19 | ````javascript 20 | var burrito = require('burrito'); 21 | 22 | var res = burrito.microwave('Math.sin(2)', function (node) { 23 | if (node.name === 'num') node.wrap('Math.PI / %s'); 24 | }); 25 | 26 | console.log(res); // sin(pi / 2) == 1 27 | ```` 28 | 29 | output: 30 | 31 | 1 32 | 33 | wrap 34 | ---- 35 | 36 | examples/wrap.js 37 | 38 | ````javascript 39 | var burrito = require('burrito'); 40 | 41 | var src = burrito('f() && g(h())\nfoo()', function (node) { 42 | if (node.name === 'call') node.wrap('qqq(%s)'); 43 | }); 44 | 45 | console.log(src); 46 | ```` 47 | 48 | output: 49 | 50 | qqq(f()) && qqq(g(qqq(h()))); 51 | 52 | qqq(foo()); 53 | 54 | methods 55 | ======= 56 | 57 | var burrito = require('burrito'); 58 | 59 | burrito(code, cb) 60 | ----------------- 61 | 62 | Given some source `code` and a function `trace`, walk the ast by expression. 63 | 64 | The `cb` gets called with a node object described below. 65 | 66 | burrito.microwave(code, context={}, cb) 67 | --------------------------------------- 68 | 69 | Like `burrito()` except the result is run using 70 | `vm.runInNewContext(res, context)`. 71 | 72 | node object 73 | =========== 74 | 75 | node.name 76 | --------- 77 | 78 | Name is a string that contains the type of the expression as named by uglify. 79 | 80 | node.wrap(s) 81 | ------------ 82 | 83 | Wrap the current expression in `s`. 84 | 85 | If `s` is a string, `"%s"` will be replaced with the stringified current 86 | expression. 87 | 88 | If `s` is a function, it is called with the stringified current expression and 89 | should return a new stringified expression. 90 | 91 | If the `node.name === "binary"`, you get the subterms "%a" and "%b" to play with 92 | too. These subterms are applied if `s` is a function too: `s(expr, a, b)`. 93 | 94 | Protip: to insert multiple statements you can use javascript's lesser-known block 95 | syntax that it gets from C: 96 | 97 | ````javascript 98 | if (node.name === 'stat') node.wrap('{ foo(); %s }') 99 | ```` 100 | 101 | node.node 102 | --------- 103 | 104 | raw ast data generated by uglify 105 | 106 | node.value 107 | ---------- 108 | 109 | `node.node.slice(1)` to skip the annotations 110 | 111 | node.start 112 | ---------- 113 | 114 | The start location of the expression, like this: 115 | 116 | ````javascript 117 | { type: 'name', 118 | value: 'b', 119 | line: 0, 120 | col: 3, 121 | pos: 3, 122 | nlb: false, 123 | comments_before: [] } 124 | ```` 125 | 126 | node.end 127 | -------- 128 | 129 | The end location of the expression, formatted the same as `node.start`. 130 | 131 | node.state 132 | ---------- 133 | 134 | The state of the traversal using traverse. 135 | 136 | node.source() 137 | ------------- 138 | 139 | Returns a stringified version of the expression. 140 | 141 | node.parent() 142 | ------------- 143 | 144 | Returns the parent `node` or `null` if the node is the root element. 145 | 146 | node.label() 147 | ------------ 148 | 149 | Return the label of the present node or `null` if there is no label. 150 | 151 | Labels are returned for "call", "var", "defun", and "function" nodes. 152 | 153 | Returns an array for "var" nodes since `var` statements can 154 | contain multiple labels in assignment. 155 | 156 | installation 157 | ============ 158 | 159 | With [npm](http://npmjs.org) you can just: 160 | 161 | npm install burrito 162 | 163 | kudos 164 | ===== 165 | 166 | Heavily inspired by (and previously mostly lifted outright from) isaacs's nifty 167 | tmp/instrument.js thingy from uglify-js. 168 | -------------------------------------------------------------------------------- /test/wrap.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var burrito = require('burrito'); 3 | var vm = require('vm'); 4 | 5 | exports.preserveTernaryParentheses = function () { 6 | var originalSource = '"anything" + (x ? y : z) + "anything"'; 7 | var burritoSource = burrito(originalSource, function (node) { 8 | // do nothing. we just want to check that ternary parens are persisted 9 | }); 10 | 11 | var ctxt = { 12 | x : false, 13 | y : 'y_'+~~(Math.random()*10), 14 | z : 'z_'+~~(Math.random()*10) 15 | }; 16 | 17 | var expectedOutput = vm.runInNewContext(originalSource, ctxt); 18 | var burritoOutput = vm.runInNewContext(burritoSource, ctxt); 19 | 20 | assert.equal(burritoOutput, expectedOutput); 21 | 22 | ctxt.x = true; 23 | 24 | expectedOutput = vm.runInNewContext(originalSource, ctxt); 25 | burritoOutput = vm.runInNewContext(burritoSource, ctxt); 26 | 27 | assert.equal(burritoOutput, expectedOutput); 28 | }; 29 | 30 | exports.wrapCalls = function () { 31 | var src = burrito('f() && g(h())\nfoo()', function (node) { 32 | if (node.name === 'call') node.wrap('qqq(%s)'); 33 | if (node.name === 'binary') node.wrap('bbb(%s)'); 34 | assert.ok(node.state); 35 | assert.equal(this, node.state); 36 | }); 37 | 38 | var tg = setTimeout(function () { 39 | assert.fail('g() never called'); 40 | }, 5000); 41 | 42 | var times = { bbb : 0, qqq : 0 }; 43 | 44 | var res = []; 45 | vm.runInNewContext(src, { 46 | bbb : function (x) { 47 | times.bbb ++; 48 | res.push(x); 49 | return x; 50 | }, 51 | qqq : function (x) { 52 | times.qqq ++; 53 | res.push(x); 54 | return x; 55 | }, 56 | f : function () { return true }, 57 | g : function (h) { 58 | clearTimeout(tg); 59 | assert.equal(h, 7); 60 | return h !== 7 61 | }, 62 | h : function () { return 7 }, 63 | foo : function () { return 'foo!' }, 64 | }); 65 | 66 | assert.deepEqual(res, [ 67 | true, // f() 68 | 7, // h() 69 | false, // g(h()) 70 | false, // f() && g(h()) 71 | 'foo!', // foo() 72 | ]); 73 | assert.equal(times.bbb, 1); 74 | assert.equal(times.qqq, 4); 75 | }; 76 | 77 | exports.wrapFn = function () { 78 | var src = burrito('f(g(h(5)))', function (node) { 79 | if (node.name === 'call') { 80 | node.wrap(function (s) { 81 | return 'z(' + s + ')'; 82 | }); 83 | } 84 | }); 85 | 86 | var times = 0; 87 | assert.equal( 88 | vm.runInNewContext(src, { 89 | f : function (x) { return x + 1 }, 90 | g : function (x) { return x + 2 }, 91 | h : function (x) { return x + 3 }, 92 | z : function (x) { 93 | times ++; 94 | return x * 10; 95 | }, 96 | }), 97 | (((((5 + 3) * 10) + 2) * 10) + 1) * 10 98 | ); 99 | assert.equal(times, 3); 100 | }; 101 | 102 | exports.binaryString = function () { 103 | var src = 'z(x + y)'; 104 | var context = { 105 | x : 3, 106 | y : 4, 107 | z : function (n) { return n * 10 }, 108 | }; 109 | 110 | var res = burrito.microwave(src, context, function (node) { 111 | if (node.name === 'binary') { 112 | node.wrap('%a*2 - %b*2'); 113 | } 114 | }); 115 | 116 | assert.equal(res, 10 * (3*2 - 4*2)); 117 | }; 118 | 119 | exports.binaryFn = function () { 120 | var src = 'z(x + y)'; 121 | var context = { 122 | x : 3, 123 | y : 4, 124 | z : function (n) { return n * 10 }, 125 | }; 126 | 127 | var res = burrito.microwave(src, context, function (node) { 128 | if (node.name === 'binary') { 129 | node.wrap(function (expr, a, b) { 130 | return '(' + a + ')*2 - ' + '(' + b + ')*2'; 131 | }); 132 | } 133 | }); 134 | 135 | assert.equal(res, 10 * (3*2 - 4*2)); 136 | }; 137 | 138 | exports.intersperse = function () { 139 | var src = '(' + (function () { 140 | f(); 141 | g(); 142 | }).toString() + ')()'; 143 | 144 | var times = { f : 0, g : 0, zzz : 0 }; 145 | 146 | var context = { 147 | f : function () { times.f ++ }, 148 | g : function () { times.g ++ }, 149 | zzz : function () { times.zzz ++ }, 150 | }; 151 | 152 | burrito.microwave(src, context, function (node) { 153 | if (node.name === 'stat') node.wrap('{ zzz(); %s }'); 154 | }); 155 | 156 | assert.deepEqual(times, { f : 1, g : 1, zzz : 3 }); 157 | }; 158 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var uglify = require('uglify-js'); 2 | var parser = uglify.parser; 3 | var parse = function (expr) { 4 | if (typeof expr !== 'string') throw 'expression should be a string'; 5 | 6 | try { 7 | var ast = parser.parse.apply(null, arguments); 8 | } 9 | catch (err) { 10 | if (err.message === undefined 11 | || err.line === undefined 12 | || err.col === undefined 13 | || err.pos === undefined 14 | ) { throw err } 15 | 16 | var e = new SyntaxError( 17 | err.message 18 | + '\n at line ' + err.line + ':' + err.col + ' in expression:\n\n' 19 | + ' ' + expr.split(/\r?\n/)[err.line] 20 | ); 21 | 22 | e.original = err; 23 | e.line = err.line; 24 | e.col = err.col; 25 | e.pos = err.pos; 26 | throw e; 27 | } 28 | return ast; 29 | }; 30 | 31 | var deparse = function (ast, b) { 32 | return uglify.uglify.gen_code(ast, { beautify : b }); 33 | }; 34 | 35 | var traverse = require('traverse'); 36 | var vm = require('vm'); 37 | 38 | var burrito = module.exports = function (code, cb) { 39 | var ast = parse(code.toString(), false, true); 40 | 41 | var ast_ = traverse(ast).map(function mapper () { 42 | wrapNode(this, cb); 43 | }); 44 | 45 | return deparse(parse(deparse(ast_)), true); 46 | }; 47 | 48 | var wrapNode = burrito.wrapNode = function (state, cb) { 49 | var node = state.node; 50 | 51 | var ann = Array.isArray(node) && node[0] 52 | && typeof node[0] === 'object' && node[0].name 53 | ? node[0] 54 | : null 55 | ; 56 | 57 | if (!ann) return undefined; 58 | 59 | var self = { 60 | name : ann.name, 61 | node : node, 62 | start : node[0].start, 63 | end : node[0].end, 64 | value : node.slice(1), 65 | state : state 66 | }; 67 | 68 | self.wrap = function (s) { 69 | var subsrc = deparse( 70 | traverse(node).map(function (x) { 71 | if (!this.isRoot) wrapNode(this, cb) 72 | }) 73 | ); 74 | 75 | if (self.name === 'binary') { 76 | var a = deparse(traverse(node[2]).map(function (x) { 77 | if (!this.isRoot) wrapNode(this, cb) 78 | })); 79 | var b = deparse(traverse(node[3]).map(function (x) { 80 | if (!this.isRoot) wrapNode(this, cb) 81 | })); 82 | } 83 | 84 | var src = ''; 85 | 86 | if (typeof s === 'function') { 87 | if (self.name === 'binary') { 88 | src = s(subsrc, a, b); 89 | } 90 | else { 91 | src = s(subsrc); 92 | } 93 | } 94 | else { 95 | src = s.toString() 96 | .replace(/%s/g, function () { 97 | return subsrc 98 | }) 99 | ; 100 | 101 | if (self.name === 'binary') { 102 | src = src 103 | .replace(/%a/g, function () { return a }) 104 | .replace(/%b/g, function () { return b }) 105 | ; 106 | } 107 | } 108 | 109 | var expr = parse(src); 110 | state.update(expr, true); 111 | }; 112 | 113 | var cache = {}; 114 | 115 | self.parent = state.isRoot ? null : function () { 116 | if (!cache.parent) { 117 | var s = state; 118 | var x; 119 | do { 120 | s = s.parent; 121 | if (s) x = wrapNode(s); 122 | } while (s && !x); 123 | 124 | cache.parent = x; 125 | } 126 | 127 | return cache.parent; 128 | }; 129 | 130 | self.source = function () { 131 | if (!cache.source) cache.source = deparse(node); 132 | return cache.source; 133 | }; 134 | 135 | self.label = function () { 136 | return burrito.label(self); 137 | }; 138 | 139 | if (cb) cb.call(state, self); 140 | 141 | if (self.node[0].name === 'conditional') { 142 | self.wrap('[%s][0]'); 143 | } 144 | 145 | return self; 146 | } 147 | 148 | burrito.microwave = function (code, context, cb) { 149 | if (!cb) { cb = context; context = {} }; 150 | if (!context) context = {}; 151 | 152 | var src = burrito(code, cb); 153 | return vm.runInNewContext(src, context); 154 | }; 155 | 156 | burrito.generateName = function (len) { 157 | var name = ''; 158 | var lower = '$'.charCodeAt(0); 159 | var upper = 'z'.charCodeAt(0); 160 | 161 | while (name.length < len) { 162 | var c = String.fromCharCode(Math.floor( 163 | Math.random() * (upper - lower + 1) + lower 164 | )); 165 | if ((name + c).match(/^[A-Za-z_$][A-Za-z0-9_$]*$/)) name += c; 166 | } 167 | 168 | return name; 169 | }; 170 | 171 | burrito.parse = parse; 172 | burrito.deparse = deparse; 173 | 174 | burrito.label = function (node) { 175 | if (node.name === 'call') { 176 | if (typeof node.value[0] === 'string') { 177 | return node.value[0]; 178 | } 179 | else if (node.value[0] && typeof node.value[0][1] === 'string') { 180 | return node.value[0][1]; 181 | } 182 | else { 183 | return null; 184 | } 185 | } 186 | else if (node.name === 'var') { 187 | return node.value[0].map(function (x) { return x[0] }); 188 | } 189 | else if (node.name === 'defun') { 190 | return node.value[0]; 191 | } 192 | else if (node.name === 'function') { 193 | return node.value[0]; 194 | } 195 | else { 196 | return null; 197 | } 198 | }; 199 | --------------------------------------------------------------------------------