Code Monkey home page Code Monkey logo

fn-to-method-codeshift's Introduction

fn-to-method-codeshift

Initial code

calc.js exports single function that adds two numbers.

// calc.js
module.exports = function add(a, b) { return a + b }
// index.js
const add = require('./calc')
console.log('2 + 3 =', add(2, 3))
// node index.js
// 2 + 3 = 5

Transform setup

Let us initialize the transform that does not change the source code yet.

// transform.js
const j = require('jscodeshift')

function transform (file, api, options) {
  console.log('transforming', file.path)

  const parsed = j(file.source)
  const transformed = parsed

  const outputOptions = {
    quote: 'single'
  }
  return transformed.toSource(outputOptions)
}
module.exports = transform

We can add the script command to the package.json and the dependency on jscodeshift

{
  "scripts": {
    "test": "jscodeshift index.js -t transform.js --dry --print"
  },
  "devDependencies": {
    "jscodeshift": "0.3.30"
  }
}

We are running the transform in "dry" mode that will NOT overwrite the source file index.js. It will also print the output source code for now by --print option.

Change calc API

Ket us change the API exported by the calc.js file. Instead of directly exporting a single function, let us export an object with add property. This allows us to add sub, mul and any other function in the future.

// calc.js
module.exports = {
  add: function add(a, b) { return a + b }
}

Our module calc.js changed its external "public" API, thus this is a major change according to semantic versioning. Every existing client will crash when trying to use the new version.

console.log('2 + 3 =', add(2, 3))
                       ^
TypeError: add is not a function

Let us create a code transform that will change any client from using the exported function to use the exported "add" property.

// existing client
const add = require('./calc')
// transformed client
const add = rewuire('./calc').add

Input abstract syntax tree

We can print the abstract syntax tree of an example client index.js to see the initial code. Let us remove all code from the index.js leaving only the const add = require('./calc') line for simplicity. Let us also print the parsed object inside the transform.js

const parsed = j(file.source)
console.log(parsed)

Calling npm test produces the following

transforming index.js
Collection {
  _parent: undefined,
  __paths:
   [ NodePath {
       value: [Object],
       parentPath: null,
       name: null,
       __childCache: null } ],
  _types: [ 'File', 'Node', 'Printable' ] }

Ok, just printing the top level "NodePath" object is not good enough. We really want to traverse all nodes in the tree and only print the require('./calc') calls. Luckily, the parsed object implements "Collections" methods, just like an Array. We can print all "CallExpression" nodes for example. Each "NodePath" object has "value" property with actual parsed values (without links to the parent and children nodes, these are inside "NodePath" itself)

const parsed = j(file.source)
  parsed.find(j.CallExpression)
    .forEach(function (path) {
      console.log(path.value)
    })

The above code finds a single node

transforming index.js
Node {
  type: 'CallExpression',
  start: 12,
  end: 29,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 12 },
     end: Position { line: 1, column: 29 },
     lines: Lines {},
     indent: 0 },
  callee:
   Node {
     type: 'Identifier',
     start: 12,
     end: 19,
     loc: SourceLocation { start: [Object], end: [Object], lines: Lines {}, indent: 0 },
     name: 'require',
     typeAnnotation: null },
  arguments:
   [ Node {
       type: 'Literal',
       start: 20,
       end: 28,
       loc: [Object],
       value: './calc',
       rawValue: './calc',
       raw: '\'./calc\'',
       regex: null } ],
  trailingComments: null }

We are only interested in the call expressions require('./calc') thus we can filter our node collection. Let us filter "NodePath" objects

const isRequire = n =>
  n && n.callee && n.callee.name === 'require'
const isUnary = args =>
  Array.isArray(args) && args.length === 1
const isCalc = arg => arg.value === './calc'
const isRequireCalc = n =>
  isRequire(n) && isUnary(n.arguments) && isCalc(n.arguments[0])
function transform (file, api, options) {
  const parsed = j(file.source)
  parsed.find(j.CallExpression)
    .filter(path => isRequireCalc(path.value))
    .forEach(function (path) {
      console.log(path.value)
    })
}

This should produce the same list, but if we had other function calls in our program, only the require('./calc') would be processed.

Desired output

I found that the easiest way to transform one abstract syntax into another one is to take the desired output and call the transform function on it, printing all the nodes. In our case, let us just create a file desired.js with simple require('./calc').add line.

Then I would see the created AST online at http://astexplorer.net/.

File index.js with just require('./calc') source code has the following tree.

{
  "type": "Program",
  "start": 0,
  "end": 18,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 17,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 17,
        "callee": {
          "type": "Identifier",
          "start": 0,
          "end": 7,
          "name": "require"
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 8,
            "end": 16,
            "value": "./calc",
            "raw": "'./calc'"
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

Same file with require('./calc').add produces slightly more complex tree

{
  "type": "Program",
  "start": 0,
  "end": 22,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 21,
      "expression": {
        "type": "MemberExpression",
        "start": 0,
        "end": 21,
        "object": {
          "type": "CallExpression",
          "start": 0,
          "end": 17,
          "callee": {
            "type": "Identifier",
            "start": 0,
            "end": 7,
            "name": "require"
          },
          "arguments": [
            {
              "type": "Literal",
              "start": 8,
              "end": 16,
              "value": "./calc",
              "raw": "'./calc'"
            }
          ]
        },
        "property": {
          "type": "Identifier",
          "start": 18,
          "end": 21,
          "name": "add"
        },
        "computed": false
      }
    }
  ],
  "sourceType": "module"
}

Thus we need to transform every require('./calc') "CallExpression" into a "MemberExpression" with additional property "add".

Transformation

We are going to replace each filtered call expression with a member expression. We can tell the Collections api to replace the current syntax tree node with new value using replaceWith method.

parsed.find(j.CallExpression)
  .filter(path => isRequireCalc(path.value))
  .replaceWith(function (path) {
    // return new AST node
  })

jscodeshift includes helpful builder functions that match 1 to 1 the AST names, just in lowercase. Here is our transformation

.replaceWith(function (path) {
  return j.memberExpression(
    path.value,
    j.identifier('add')
  )
})

Notice the trick - we are reusing the existing "CallExpression" in path.value so we do not have to construct require('./calc') node again. We just use it as the first argument in j.memberExpression which is the "object".

The transformation prints the code result require('./calc').add which matches what we need. Let us remove "--dry" parameter and save the output file. The diff shows the change.

-const add = require('./calc')
+const add = require('./calc').add
 console.log('2 + 3 =', add(2, 3))

The transformed index.js now works with our new API.

node index.js
2 + 3 = 5

fn-to-method-codeshift's People

Stargazers

 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.