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

ASTs in JavaScript

ASTs in JavaScript

A presentation I held at JavaZone in 2017 about ASTs and tooling in JavaScript

Kim Joar Bekkelund

September 13, 2017
Tweet

More Decks by Kim Joar Bekkelund

Other Decks in Programming

Transcript

  1. … but what are abstract syntax trees? … how do

    they work in JavaScript? … how can we use them effectively?
  2. [ { type: 'Keyword', value: 'const' }, { type: 'Identifier',

    value: 'conference' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: 'JavaZone' }, { type: 'Punctuator', value: ';' } ] const conference = "JavaZone";
  3. Syntax analysis (parser) const conference = "JavaZone"; [ { type:

    'Keyword', value: 'const' }, { type: 'Identifier', value: 'conference' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: 'JavaZone' }, { type: 'Punctuator', value: ';' } ]
  4. … but what can we do with it? Okey, so

    now we know what an AST is
  5. Use-cases Static analysis Syntax highlighting Code completion Minification Compile-time optimisations

    Source maps Code coverage Transpilers Code formatter One-time code transformations Build-time code transformations
  6. module.exports = { }; Creating our own ESLint plugin that

    disallows default exports export default myFunction
  7. module.exports = { meta: {}, create: function(context) { return {

    }; } }; ESLint helpers Creating our own ESLint plugin that disallows default exports
  8. Node type module.exports = { meta: {}, create: function(context) {

    return { ExportDefaultDeclaration: function(node) { } }; } }; (we’ll talk about how we find these later) ESLint calls this function to “visit” the node while traversing down the AST Creating our own ESLint plugin that disallows default exports
  9. module.exports = { meta: {}, create: function(context) { return {

    ExportDefaultDeclaration: function(node) { context.report(node, 'Default exports are not allowed.'); } }; } }; Publishes a warning or error Creating our own ESLint plugin that disallows default exports
  10. create: function(context) { const dependencies = new Set(); return {

    ImportDeclaration: function(node) { dependencies.add(node.source.value); }, 'Program:exit': function() { failIfTooManyDeps(dependencies, context); } } } ESLint example: fail if too many dependencies
  11. WHY!?!? “NOTE: Only 'stage 4' (finalized) ECMAScript features are being

    implemented by Acorn.” “[Babylon has] support for experimental language proposals” "Unfortunately, ESLint relies on more than just the AST to do its job. It relies on […] tokens and comment attachment features to get a complete picture of the source code." “Experimental support for JSX”
  12. Some of the changes we’ve done over time: AMD CommonJS

    ES6 modules var const / let Change test lib (Mocha to Jest) and assertion lib (e.g. to.be() to toBe()) Remove unused code, e.g. unused variables and functions Object.assign Object spread Introduce support for I18N
  13. { type: 'dashboard', id: indexId, ...additionalOptions } Object.assign( { type:

    'dashboard', id: indexId }, additionalOptions ); Object.assign to Object spread
  14. Object.assign( myVar, { type: 'dashboard', id: indexId } ); Object.assign

    to Object spread Don’t change as it’s mutating myVar
  15. // most parsers: visitor pattern const ast = parse(file.source); traverse(ast,

    { Identifier: function(node) { // do something with node } }); const ast = j(file.source); ast .find(j.Identifier) .forEach(function(path) { // do something with path }); // jscodeshift: fluent interface
  16. const ast = j(file.source); ast .find(j.Identifier) .forEach(function(path) { // do

    something with path }); Collection: Similar to jQuery's $(...) APIs to find, filter, map and remove AST nodes.
  17. Wraps an AST node and adds helpers to simplify working

    with it. Path: Collection: Similar to jQuery's $(...) APIs to find, filter, map and remove AST nodes. const ast = j(file.source); ast .find(j.Identifier) .forEach(function(path) { // do something with path });
  18. Object.assign({}, obj); AST { "type": "CallExpression", "callee": { "type": "MemberExpression",

    "object": { "type": "Identifier", "name": "Object" }, "property": { "type": "Identifier", "name": "assign" } }, "arguments": [ { "type": "ObjectExpression", "properties": [] }, { "type": "Identifier", "name": "obj" } ] }
  19. .find( ); Object.assign({}, obj); AST { "type": "CallExpression", "callee": {

    "type": "MemberExpression", "object": { "type": "Identifier", "name": "Object" }, "property": { "type": "Identifier", "name": "assign" } }, "arguments": [ { "type": "ObjectExpression", "properties": [] }, { "type": "Identifier", "name": "obj" } ] }
  20. .find(j.CallExpression ); Object.assign({}, obj); AST { "type": "CallExpression", "callee": {

    "type": "MemberExpression", "object": { "type": "Identifier", "name": "Object" }, "property": { "type": "Identifier", "name": "assign" } }, "arguments": [ { "type": "ObjectExpression", "properties": [] }, { "type": "Identifier", "name": "obj" } ] }
  21. Object.assign({}, obj); .find(j.CallExpression, { callee: { type: "MemberExpression", object: {

    name: "Object" }, property: { name: "assign" } } }); AST { "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type": "Identifier", "name": "Object" }, "property": { "type": "Identifier", "name": "assign" } }, "arguments": [ { "type": "ObjectExpression", "properties": [] }, { "type": "Identifier", "name": "obj" } ] }
  22. Object.assign({}, obj); test(); Object.assign( {}, query, hashedQuery ); Object.keys({ hello:

    'world' }); console.log('hello world'); export default function transformer(file, api) { const j = api.jscodeshift; const ast = j(file.source); return ast.toSource(); } Input Transform
  23. Object.assign({}, obj); test(); Object.assign( {}, query, hashedQuery ); Object.keys({ hello:

    'world' }); console.log('hello world'); export default function transformer(file, api) { const j = api.jscodeshift; const ast = j(file.source); ast .find(j.CallExpression, { callee: { type: "MemberExpression", object: { name: "Object" }, property: { name: "assign" } } }) return ast.toSource(); } Input Transform
  24. Object.assign({}, obj); test(); Object.assign( {}, query, hashedQuery ); Object.keys({ hello:

    'world' }); console.log('hello world'); export default function transformer(file, api) { const j = api.jscodeshift; const ast = j(file.source); ast .find(j.CallExpression, { callee: { type: "MemberExpression", object: { name: "Object" }, property: { name: "assign" } } }) .forEach(path => transformToObject(path)); return ast.toSource(); } Input Transform
  25. { "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type":

    "Identifier", "name": "Object" }, "property": { "type": "Identifier", "name": "assign" } }, "arguments": [ { "type": "ObjectExpression", "properties": [] }, { "type": "Identifier", "name": "obj" } ] } function transformToObject(path) { } Wraps the AST
  26. { "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type":

    "Identifier", "name": "Object" }, "property": { "type": "Identifier", "name": "assign" } }, "arguments": [ { "type": "ObjectExpression", "properties": [] }, { "type": "Identifier", "name": "obj" } ] } function transformToObject(path) { const assignCall = path.value; }
  27. { "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type":

    "Identifier", "name": "Object" }, "property": { "type": "Identifier", "name": "assign" } }, "arguments": [ { "type": "ObjectExpression", "properties": [] }, { "type": "Identifier", "name": "obj" } ] } function transformToObject(path) { const assignCall = path.value; const args = assignCall.arguments; }
  28. function transformToObject(path) { const assignCall = path.value; const args =

    assignCall.arguments; const newProps = createObjectProperties(args); }
  29. function transformToObject(path) { const assignCall = path.value; const args =

    assignCall.arguments; const newProps = createObjectProperties(args); path.replace( j.objectExpression(newProps) ); } Builders to generate AST nodes Replace Object.assign call with an object expression j.identifier("name") j.memberExpression(j.identifier("foo"), j.identifier("bar")) j.objectExpression([ j.property("init", j.literal("key"), j.literal("value")) ]) j.callExpression( j.identifier("foo"), [j.identifier("bar")] ) ...
  30. Then I copy the transform into a local file and

    run it on the full codebase (and fix all the edge-cases)
  31. if (isStopped) { return <div className='is-warning'> <FormattedMessage id='cluster-status.stopped' defaultMessage='Stopped' />

    </div> } if (isStopped) { return <div className='is-warning'> Stopped </div> } Introducing I18N using React-intl and jscodeshift
  32. <div> <FormattedMessage id='hello-name' defaultMessage='Hello { name }' values={{ name: <strong>{

    name }</strong> }} /> </div> <div> Hello <strong>{ name }<strong> </div> Introducing I18N using React-intl and jscodeshift
  33. Took us a couple of days on a 30k+ line

    codebase to solve 80% of the problems
  34. Interesting use-cases: Libraries can include transforms that fix breaking changes,

    e.g. React deprecating PropTypes Transforms for switching test libraries, e.g. moving from Mocha to Jest Pre-built codemods for moving to ES6+ features, e.g. arrow functions and const/let