Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Harnessing the Power of Abstract Syntax Trees

Harnessing the Power of Abstract Syntax Trees

A look at building powerful tooling with abstract syntax trees in JavaScript

Avatar for Jamund Ferguson

Jamund Ferguson

September 21, 2015
Tweet

More Decks by Jamund Ferguson

Other Decks in Technology

Transcript

  1. { type: "Program", body: [ { type: "ExpressionStatement", expression: {

    type: "CallExpression", callee: { type: "MemberExpression", computed: false, object: { type: "Identifier", name: "console" }, property: { type: "Identifier", name: "log" } }, arguments: [ { type: "Literal", value: "UtahJS", raw: "\"UtahJS\"" } ] } } ] }
  2. PARSING // generate an AST from a string of code

    espree.parse("console.log('UtahJS')");
  3. PARSING // generate an AST from a string of code

    acorn.parse("console.log('UtahJS')");
  4. PARSING ES6 // generate an AST from a string of

    code acorn.parse("console.log('UtahJS')", { ecmaVersion: 6 });
  5. BABEL PLUGIN module.exports = function (Babel) { return new Babel.Plugin("plugin-example",

    { // visitor: { // "FunctionDeclaration": swapWithExpression // } }); };
  6. BABEL PLUGIN module.exports = function (Babel) { return new Babel.Plugin("plugin-example",

    { visitor: { "FunctionDeclaration": swapWithExpression } }); };
  7. GUTS function swapWithExpression(node, parent) { var id = node.id; //

    change the node type // node.type = "FunctionExpression"; // node.id = null; // return a variable declaration // return Babel.types.variableDeclaration("var", [ // Babel.types.variableDeclarator(id, node) // ]); }
  8. GUTS function swapWithExpression(node, parent) { var id = node.id; //

    change the node type node.type = "FunctionExpression"; node.id = null; // return a variable declaration // return Babel.types.variableDeclaration("var", [ // Babel.types.variableDeclarator(id, node) // ]); }
  9. GUTS function swapWithExpression(node, parent) { var id = node.id; //

    change the node type node.type = "FunctionExpression"; node.id = null; // return a variable declaration return Babel.types.variableDeclaration("var", [ Babel.types.variableDeclarator(id, node) ]); }
  10. BABEL PLUGIN RESULTS // function declaration function help() {} //

    transformed to a function expression var help = function() {}
  11. JSCODESHIFT EXAMPLE module.exports = function(fileInfo, api) { return api.jscodeshift(fileInfo.source) //

    .findVariableDeclarators('foo') // .renameTo('bar') // .toSource(); };
  12. CUSTOM LINT RULES // lib/rules/no-class.js module.exports = function(context) { return

    { "ClassDeclaration": function(node) { context.report(node, "Your code has no class"); } } }
  13. A BETTER DIFF // turn stdin into an array of

    lines var lines = fs.readFileSync('/dev/stdin').toString().split('\n');
  14. A BETTER DIFF // lines now looks something like this

    [':100644 100644 9a0b08f... 0000000... M tree1.js']
  15. A BETTER DIFF lines.map(function(line) { var parts = line.split(' ');

    // var file = parts.pop().split('\t'); // return [file[1], parts[2].slice(0, -3)]; });
  16. A BETTER DIFF lines.map(function(line) { var parts = line.split(' ');

    var file = parts.pop().split('\t'); // return [file[1], parts[2].slice(0, -3)]; });
  17. A BETTER DIFF lines.map(function(line) { var parts = line.split(' ');

    var file = parts.pop().split('\t'); return [file[1], parts[2].slice(0, -3)]; });
  18. A BETTER DIFF // the key parts of our git

    diff [ [ "tree1.js", "9a0b08f"] ]
  19. PARSING // generate an AST from a string of code

    espree.parse("console.log('UtahJS)");
  20. .map(function(files) { var after = fs.readFileSync(files[0]); // var before =

    child_process.execSync("git show" + files[1]); // return { // filename: files[0], // before: espree.parse(before, options), // after: espree.parse(after, options) // }; })
  21. .map(function(files) { var after = fs.readFileSync(files[0]); var before = child_process.execSync("git

    show" + files[1]); // return { // filename: files[0], // before: espree.parse(before, options), // after: espree.parse(after, options) // }; })
  22. .map(function(files) { var after = fs.readFileSync(files[0]); var before = child_process.execSync("git

    show" + files[1]); return { filename: files[0], before: espree.parse(before, options), after: espree.parse(after, options) }; })
  23. [{ filename: "trees1.js", before: { type: "Program", body: [Object] },

    after: { type: "Program", body: [Object] } }]
  24. var lines = fs.readFileSync('/dev/stdin').toString().split('\n'); var trees = lines.map(function(line) { var

    parts = line.split(' '); var file = parts.pop().split('\t'); return [path.resolve(file[1]), parts[2].slice(0, -3)]; }).filter(function(files) { return files[0].indexOf('.js') > -1; }).map(function(files) { var after = fs.readFileSync(files[0]); var before = child_process.execSync("git show " + files[1]); return { filename: files[0], before: espree.parse(before, options), after: espree.parse(after, options) }; });
  25. DIFFING THE TREES // let's see if something changed var

    different = deepEqual(treeBefore, treeAfter);
  26. function deepEqual(a, b) { if (a === b) { return

    true; } if (!a || !b) { return false; } if (Array.isArray(a)) { return a.every(function(item, i) { return deepEqual(item, b[i]); }); } if (typeof a === 'object') { return Object.keys(a).every(function(key) { return deepEqual(a[key], b[key]); }); } return false; }
  27. if (typeof a === 'object') { var equal = Object.keys(a).every(function(key)

    { return deepEqual(a[key], b[key]); }); return equal; } return false;
  28. if (typeof a === 'object') { // var equal =

    Object.keys(a).every(function(key) { // return deepEqual(a[key], b[key]); // }); if (!equal) { console.log('[' + a.type + '] => [' + b.type + ']'); } // return equal; } console.log('"' + a + '" => "' + b + '"'); // return false;
  29. git diff --raw | node compare.js "log" => "error" [Identifier]

    => [Identifier] [MemberExpressio] => [MemberExpression] [CallExpression] => [CallExpression] [ExpressionStatement] => [ExpressionStatement] [Program] => [Program]
  30. export function buildHouse(lot, color, size, bedrooms) { clearLot(lot); let foundation

    = buildFoundation(size); let walls = buildWalls(bedrooms); let paintedWalls = paintWalls(color, walls); let roof = buildRoof(foundation, walls); let house = foundation + paintedWalls + roof; // house is all done right-away return house; }
  31. function getPermits(callback) { setTimeout(callback, 1.0519e10); // 4 months because trees

    } export function buildHouse(lot, color, size, bedrooms, callback) { getPermits((permits) => { clearLot(permits, lot); let foundation = buildFoundation(size); let walls = buildWalls(bedrooms); let paintedWalls = paintWalls(color, walls); let roof = buildRoof(foundation, walls); let house = foundation + paintedWalls + roof; // house will be ready in about a year callback(house); }); }
  32. OUR GOAL git diff --raw | node compare.js house.js 1.

    The exported `buildHouse` function output went from a return to a callback. 2. The private `getPermits` function was added.
  33. AN ARRAY OF TREES [{ filename: "trees1.js", before: { type:

    "Program", body: [Object] }, after: { type: "Program", body: [Object] } }]
  34. VISITING OUR TREES esrecurse.visit(diff.before, { // export function a() {}

    ExportNamedDeclaration: function(node) { /* ... */ }, // function a() {} FunctionDeclaration: function(node) { /* ... */ } });
  35. INSPECTING FUNCTION DECLARATIONS function inspectFunction(node, visiblity) { return { name:

    node.id.name, // "buildHouse", // params: node.params.map(param => param.name), // ["lot", "color", ...] // visibility: visiblity || "private", // outputType: getOutputType(node) }; }
  36. INSPECTING FUNCTION NODES function inspectFunction(node, visiblity) { return { name:

    node.id.name, // "buildHouse" params: node.params.map(param => param.name), // ["lot", "color", ...] // visibility: visiblity || "private", // outputType: getOutputType(node) }; }
  37. INSPECTING FUNCTION DECLARATIONS function inspectFunction(node, visiblity) { return { name:

    node.id.name, // "buildHouse" params: node.params.map(param => param.name), // ["lot", "color", ...] visibility: visiblity || "private", // outputType: getOutputType(node) }; }
  38. INSPECTING FUNCTION DECLARATIONS function inspectFunction(node, visiblity) { return { name:

    node.id.name, // "buildHouse" params: node.params.map(param => param.name), // ["lot", "color", ...] visibility: visiblity || "private", outputType: getOutputType(node) }; }
  39. GETTING OUTPUT TYPE function getOutputType(node) { var params = node.params.map(param

    => param.name); // is there a callback param? var hasCallback = params[params.length - 1] === 'callback'; // is there a return in the immediate body? var hasReturn = node.body.some((node) => node.type === 'ReturnStatement'); // callback or return or '' return hasCallback ? 'callback' : (hasReturn ? 'return' : ''); }
  40. THE ESSENCE OF A FUNCTION { name: "getPermits", visibility: "private",

    params: [ "callback" ], outputType: "callback" }
  41. MORE COMPLEX REPRESENTATION [{ filename: "house.js", functions: { "getPermits": {

    after: { /* ... */ } }, "buildHouse": { before: { /* ... */ }, after: { /* ... */ } } }]
  42. REDUCING COMPLEXITY function getReadableOutput(functions) { return Object.keys(functions).reduce(function(ouput, name, i) {

    var whatHappened = getWhatHappened(functions[name]); // var visibility = functions[name].after.visibility; // var message = `The ${visibility} ${name} function ${whatHappened}.\n` // return `${output}${i + 1}. ${message}`; }, ""); }
  43. HUMAN READABLE FTW! function getWhatHappened(func) { if (!func.before) { return

    "was added" } if (!func.after) { return "was removed" } if (func.before.outputType !== func.after.outputType) { return "output went from a " + func.before.outputType + " to a " + func.after.outputType; } }
  44. `1. The exported `buildHouse` function output went from a return

    to a callback. 2. The private `getPermits` function was added.`
  45. UNROLLING OUR ARRAY .map(function(diff) { return diff.filename + "\n" +

    getReadableOutput(diff.functions); }).join("\n");
  46. A HAPPY ENDING git diff --raw | node compare.js house.js

    1. The exported `buildHouse` function output went from a return to a callback. 2. The private `getPermits` function was added.