├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json └── src └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ovidiu Cherecheș 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-trace-execution 2 | Trace execution context of JS functions 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-trace-execution", 3 | "version": "0.2.0", 4 | "description": "Trace execution context of JS functions", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/skidding/babel-plugin-trace-execution.git" 9 | }, 10 | "main": "lib/index.js", 11 | "scripts": { 12 | "lint": "xo", 13 | "test": "npm run lint", 14 | "build": "babel src -d lib", 15 | "prepublish": "npm run test && npm run build" 16 | }, 17 | "devDependencies": { 18 | "babel-cli": "^6.18.0", 19 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 20 | "babel-preset-es2015": "^6.18.0", 21 | "xo": "^0.17.1" 22 | }, 23 | "xo": { 24 | "space": true, 25 | "esnext": true, 26 | "rules": { 27 | "comma-dangle": 0, 28 | "object-curly-spacing": 0 29 | } 30 | }, 31 | "dependencies": { 32 | "babel-template": "^6.16.0", 33 | "lodash.clonedeep": "^4.5.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import template from 'babel-template'; 2 | 3 | const buildTemplate = template(` 4 | const cloneDeep = require('lodash.clonedeep'); 5 | 6 | let __steps; 7 | let __parentStepId; 8 | let __lastStepId; 9 | 10 | function __enterCall() { 11 | // Mark steps from sub call as decendends of last step from curr call 12 | __parentStepId = __lastStepId; 13 | } 14 | 15 | function __leaveCall() { 16 | if (__parentStepId) { 17 | // Revert step refs in parent callee 18 | __lastStepId = __parentStepId; 19 | __parentStepId = __steps[__parentStepId].parentStepId; 20 | } 21 | } 22 | 23 | function __trace({ 24 | step, 25 | enterCall, 26 | leaveCall, 27 | }) { 28 | if (enterCall) { 29 | __enterCall(); 30 | } 31 | 32 | __steps.push({ 33 | parentStepId: __parentStepId, 34 | ...step, 35 | }); 36 | 37 | // Track last step id to reference back to entering a subcall 38 | __lastStepId = __steps.length - 1; 39 | 40 | if (leaveCall) { 41 | __leaveCall(); 42 | } 43 | } 44 | 45 | ALGORITHM_BODY 46 | 47 | function __traceWrapper(...args) { 48 | __steps = []; 49 | __parentStepId = undefined; 50 | __lastStepId = undefined; 51 | 52 | const returnValue = ALGORITHM_NAME(...args); 53 | return { 54 | steps: __steps, 55 | returnValue: returnValue, 56 | }; 57 | }; 58 | 59 | __traceWrapper.code = ALGORITHM_CODE; 60 | 61 | module.exports = __traceWrapper; 62 | `, { 63 | plugins: ['objectRestSpread'] 64 | }); 65 | 66 | const MUTATOR_METHODS = ['push', 'shift']; 67 | 68 | export default function ({ types: t }) { 69 | function isNewlyCreatedPath(path) { 70 | return !path.node.loc; 71 | } 72 | 73 | function getOffsettedRange(start, end, offset) { 74 | return { 75 | start: start - offset, 76 | end: end - offset, 77 | }; 78 | } 79 | 80 | function getBindingsForScope(scope, allBindings) { 81 | const bindings = []; 82 | let currScope = scope; 83 | 84 | while (currScope) { 85 | const scopeBindings = allBindings[currScope.uid]; 86 | if (scopeBindings) { 87 | bindings.unshift(...scopeBindings); 88 | } 89 | currScope = currScope.parent; 90 | } 91 | 92 | return bindings; 93 | } 94 | 95 | function createTraceCall({ 96 | start, 97 | end, 98 | bindings, 99 | compared, 100 | beforeChildCall, 101 | afterChildCall, 102 | returnValue, 103 | enterCall, 104 | leaveCall, 105 | }) { 106 | const stepProps = [ 107 | t.objectProperty( 108 | t.identifier('highlight'), 109 | t.objectExpression([ 110 | t.objectProperty(t.identifier('start'), t.numericLiteral(start)), 111 | t.objectProperty(t.identifier('end'), t.numericLiteral(end)), 112 | ]), 113 | ), 114 | t.objectProperty( 115 | t.identifier('bindings'), 116 | t.objectExpression(bindings.map(name => 117 | t.objectProperty(t.identifier(name), t.callExpression( 118 | t.identifier('cloneDeep'), 119 | [t.identifier(name)], 120 | )) 121 | )) 122 | ), 123 | ]; 124 | if (compared !== undefined) { 125 | stepProps.push(t.objectProperty( 126 | t.identifier('compared'), 127 | t.arrayExpression(compared.map(s => t.stringLiteral(s))) 128 | )); 129 | } 130 | if (beforeChildCall !== undefined) { 131 | stepProps.push(t.objectProperty(t.identifier('beforeChildCall'), t.booleanLiteral(true))); 132 | } 133 | if (afterChildCall !== undefined) { 134 | stepProps.push(t.objectProperty(t.identifier('afterChildCall'), t.booleanLiteral(true))); 135 | } 136 | if (returnValue !== undefined) { 137 | stepProps.push(t.objectProperty(t.identifier('returnValue'), returnValue)); 138 | } 139 | 140 | const args = [ 141 | t.objectProperty(t.identifier('step'), t.objectExpression(stepProps)), 142 | ]; 143 | if (enterCall) { 144 | args.push(t.objectProperty(t.identifier('enterCall'), t.booleanLiteral(true))); 145 | } 146 | if (leaveCall) { 147 | args.push(t.objectProperty(t.identifier('leaveCall'), t.booleanLiteral(true))); 148 | } 149 | 150 | return t.expressionStatement(t.callExpression(t.identifier('__trace'), [ 151 | t.objectExpression(args) 152 | ])); 153 | } 154 | 155 | const innerVisitor = { 156 | /** 157 | * Prepend trace call(enter) BEFORE anything in the main function's body. 158 | * Highlight contains function signature "fn(args)". 159 | */ 160 | BlockStatement(path) { 161 | const parentPath = path.parentPath; 162 | const parentNode = parentPath.node; 163 | if (!parentNode.id || parentNode.id.name !== this.fnName) { 164 | return; 165 | } 166 | 167 | const start = parentPath.get('id').node.start; 168 | const end = parentNode.params[parentNode.params.length - 1].end + 1; 169 | path.unshiftContainer('body', createTraceCall({ 170 | bindings: getBindingsForScope(path.scope, this.bindings), 171 | ...getOffsettedRange(start, end, this.fnOffset), 172 | enterCall: true, 173 | })); 174 | }, 175 | 176 | /** 177 | * Append trace call() AFTER var declaration. Highlight contains entire 178 | * declaration. 179 | * 180 | * Created vars are pushed to visitor state to be included in bindings of 181 | * this and future trace() calls 182 | */ 183 | VariableDeclaration(path) { 184 | // Ignore var declarations we create 185 | if (isNewlyCreatedPath(path)) { 186 | return; 187 | } 188 | 189 | const scopeId = path.scope.uid; 190 | let scopeBindings = this.bindings[scopeId]; 191 | if (!scopeBindings) { 192 | scopeBindings = this.bindings[scopeId] = []; 193 | } 194 | scopeBindings.push(...path.node.declarations.map(d => d.id.name)); 195 | 196 | const { start, end } = path.node; 197 | path.insertAfter(createTraceCall({ 198 | bindings: getBindingsForScope(path.scope, this.bindings), 199 | ...getOffsettedRange(start, end, this.fnOffset), 200 | })); 201 | }, 202 | 203 | /** 204 | * Append trace call() AFTER assignment expressions. Highlight contains 205 | * entire expression. Bindings include new values. 206 | */ 207 | AssignmentExpression(path) { 208 | const { start, end } = path.node; 209 | path.insertAfter(createTraceCall({ 210 | bindings: getBindingsForScope(path.scope, this.bindings), 211 | ...getOffsettedRange(start, end, this.fnOffset), 212 | })); 213 | }, 214 | 215 | /** 216 | * Replace test expressions with IIFE containing trace() call BEFORE 217 | * test value has been computed. Highlight contains test expression only. 218 | */ 219 | 'WhileStatement|IfStatement'(path) { 220 | const testPath = path.get('test'); 221 | const { start, end, left, right, type } = testPath.node; 222 | const { code } = path.hub.file; 223 | const compared = type === 'BinaryExpression' ? [ 224 | code.slice(left.start, left.end), 225 | code.slice(right.start, right.end), 226 | ] : [code.slice(start, end)]; 227 | testPath.replaceWith( 228 | t.callExpression( 229 | t.arrowFunctionExpression([], 230 | t.blockStatement([ 231 | createTraceCall({ 232 | bindings: getBindingsForScope(path.scope, this.bindings), 233 | ...getOffsettedRange(start, end, this.fnOffset), 234 | compared, 235 | }), 236 | t.returnStatement(testPath.node) 237 | ]) 238 | ), 239 | []) 240 | ); 241 | }, 242 | 243 | CallExpression(path) { 244 | // This prevents an infinite loop 245 | if (this.processedCallExpressions.indexOf(path.node) !== -1) { 246 | return; 247 | } 248 | 249 | this.processedCallExpressions.push(path.node); 250 | 251 | const { node } = path; 252 | const { start, end } = node; 253 | const bindings = getBindingsForScope(path.scope, this.bindings); 254 | const offsetRange = getOffsettedRange(start, end, this.fnOffset); 255 | 256 | /** 257 | * Replace recursive call expressions with IIFE containing trace() call 258 | * both BEFORE and AFTER return value has been computed. 259 | */ 260 | if (node.callee.name === this.fnName) { 261 | const traceCallBefore = createTraceCall({ 262 | bindings, 263 | ...offsetRange, 264 | beforeChildCall: true, 265 | }); 266 | const traceCallAfter = createTraceCall({ 267 | bindings, 268 | ...offsetRange, 269 | afterChildCall: true, 270 | }); 271 | const returnValId = path.scope.generateUidIdentifier('uid'); 272 | path.replaceWith( 273 | t.callExpression( 274 | t.arrowFunctionExpression([], 275 | t.blockStatement([ 276 | traceCallBefore, 277 | t.variableDeclaration('const', [ 278 | t.variableDeclarator(returnValId, node) 279 | ]), 280 | traceCallAfter, 281 | t.returnStatement(returnValId) 282 | ]) 283 | ), 284 | []) 285 | ); 286 | } 287 | 288 | /** 289 | * Replace mutative call expressions with IIFE containing trace() call 290 | * AFTER object has been mutated. 291 | */ 292 | if ( 293 | node.callee.type === 'MemberExpression' && 294 | bindings.indexOf(node.callee.object.name) !== -1 && 295 | MUTATOR_METHODS.indexOf(node.callee.property.name) !== -1 296 | ) { 297 | const returnValId = path.scope.generateUidIdentifier('uid'); 298 | path.replaceWith( 299 | t.callExpression( 300 | t.arrowFunctionExpression([], 301 | t.blockStatement([ 302 | t.variableDeclaration('const', [ 303 | t.variableDeclarator(returnValId, node) 304 | ]), 305 | createTraceCall({ 306 | bindings, 307 | ...offsetRange, 308 | }), 309 | t.returnStatement(returnValId) 310 | ]) 311 | ), 312 | []) 313 | ); 314 | } 315 | }, 316 | 317 | /** 318 | * Replace return calls with IIFE containing trace(leave) call AFTER return 319 | * value has been computed, before it is returned. Highlight contains 320 | * entire return statement. 321 | */ 322 | ReturnStatement(path) { 323 | // Ignore return statements we create 324 | if (isNewlyCreatedPath(path)) { 325 | return; 326 | } 327 | 328 | const returnValId = path.scope.generateUidIdentifier('uid'); 329 | const argumentPath = path.get('argument'); 330 | const { start, end } = path.node; 331 | 332 | argumentPath.replaceWith( 333 | t.callExpression( 334 | t.arrowFunctionExpression([], 335 | t.blockStatement([ 336 | t.variableDeclaration('const', [ 337 | t.variableDeclarator(returnValId, argumentPath.node) 338 | ]), 339 | createTraceCall({ 340 | bindings: getBindingsForScope(path.scope, this.bindings), 341 | ...getOffsettedRange(start, end, this.fnOffset), 342 | leaveCall: true, 343 | returnValue: returnValId, 344 | }), 345 | t.returnStatement(returnValId) 346 | ]) 347 | ), 348 | []) 349 | ); 350 | }, 351 | }; 352 | 353 | return { 354 | visitor: { 355 | ExportDefaultDeclaration(path) { 356 | const fnPath = path.get('declaration'); 357 | const fnName = fnPath.node.id.name; 358 | const { start, end } = fnPath.node; 359 | 360 | path.replaceWithMultiple( 361 | buildTemplate({ 362 | ALGORITHM_NAME: t.identifier(fnName), 363 | ALGORITHM_BODY: fnPath.node, 364 | ALGORITHM_CODE: t.stringLiteral( 365 | path.hub.file.code.slice(start, end) 366 | ), 367 | }) 368 | ); 369 | 370 | const bodyPath = fnPath.get('body'); 371 | const params = Object.keys(bodyPath.scope.getAllBindingsOfKind('param')); 372 | 373 | fnPath.traverse(innerVisitor, { 374 | fnName, 375 | bindings: { [fnPath.scope.uid]: params }, 376 | processedCallExpressions: [], 377 | fnOffset: start, 378 | }); 379 | } 380 | }, 381 | }; 382 | } 383 | --------------------------------------------------------------------------------