Large-scale refactoring with codemods

Edd Yerburgh

@eddyerburgh

Refactoring...

import { flatten } from 'lodash'

const flattenedArray = flatten(arr)
arr.flatten()
arr.flat()
import { flatten } from 'lodash' // =>

flatten(arr) // => arr.flat()

Manually

πŸ™…β€β™‚οΈ

codemod -m -d src --extensions js,jsx \
    'flatten\((.*)\)' \
    '\1.flat()'
flatten([
  a,
  ...flatten([
    getC(),
    flatten([d, e])
  ]),
  f
])

Using regex

πŸ™…β€β™‚οΈ

import { 
  flatten,
  mergeDeep 
} from 'lodash'

JSCodeshift

  • CLI tool
  • Modify JS by editing AST

AST

Abstract Syntax Tree

Tree representation of the syntactic structure of source code

function sum(a, b) {
  return a + b
}

sum(1,2)

Program

Function

Declaration

Expression

Statement

Block

Statement

Return

Statement

{
  type: 'Program',
  body: [
    {
      type: 'FunctionDeclaration',
      body: [
        {
          type: 'BlockStatement',
          body: [{ type: 'ReturnStatement' }]
        }
      ]
    },
    {
      type: 'ExpressionStatement'
    }
  ]
}
{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "sum"
  },
  params: [
    {
      type: "Identifier",
      name: "a"
    }
    // ..
  ],
  async: false,
  generator: false,
  body: [
    // ..
  ]
}
{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "sum"
  },
  params: [
    {
      type: "Identifier",
      name: "arg1"
    }
    // ..
  ],
  async: true,
  generator: false,
  body: [
    // ..
  ]
}

github.com/estree/estree

Libraries that use ASTs

Source code

AST

Β New AST

Source code

Parse

Transform

Print

JSCodeshift transform

const update = path => {
  const arr = path.value.arguments[0]
  path.replace(
    j.callExpression(
      j.memberExpression(
        arr,
        j.identifier("flat")
      ),
      []
    )
  )
}
root
  .find(j.CallExpression, {
    callee: { name: 'flatten' }
  })
  .forEach(update)

The transform

// __tests__/flatten.spec.js

const testUtils = require('jscodeshift/dist/testUtils')
const defineTest = testUtils.defineTest

defineTest(__dirname, 'flatten')

Testing

// __testFixtures__/flatten.input.js

import { flatten, someUtil } from "@bbc/utils";

flatten([arr, flatten(createArr())])
// __testFixtures__/flatten.output.js

import { someUtil } from "lodash";

[arr, createArr().flat()].flat()

Uses

  • Internal refactoring
  • Improve migration path
  • Learning ASTs!

Thankyou

Resources

github.com/facebook/jscodeshift

astexplorer.net/

github.com/estree/estree

github.com/benjamn/ast-types

Β 

Refactoring with codemods

By Edd Yerburgh

Refactoring with codemods

Perform large-sale refactoring with codemods

  • 1,957