杨文坚

2016-08-15 11:47

代码重构利器 —— jscodeshift

本文作者:IMWeb 杨文坚 原文出处:IMWeb社区 未经同意,禁止转载

Standard Component 项目需要一个基于 AST 的 Javascript Transformer 编写工具,用于从一种类型的组件 transform 到 Standard Component。本来,想用著名的 esprima,来编写相应工具。但后来发现,Facebook 已经开发了 jscodeshift,重造一个轮子明显是多余的。

所以,jscodeshift 是什么鬼?

jscodeshift 是一个 Javscript Codemod 工具,官方对 Codemod 的解释是:

Codemod is a tool/library to assist you with large-scale codebase refactors that can be partially automated but still require human oversight and occasional intervention.

jscodeshift 也是基于 esprima 的,相比 esprima 及 estools 工具集,其通过 path 可以很容易的在 AST 上遍历 node。

OK,前戏不多言,直接上例子,项目主要依赖下面这些库:

简单重构,比如生命周期,初始化完成finished,改名成为了ready

先写好测试用用例:

import test from 'ava'
import jscodeshift from 'jscodeshift'
import testCodemod from '../test.plugin'
import transformer from '../transformer/old-component/test'

const { testChanged, testUnchanged } = testCodemod(jscodeshift, test, transformer)

testChanged(`import Base from 'base';

export default Base.extend({
  finished: () => {
    console.log('ready')
  }
});`, `import Base from 'base';

export default Base.extend({
  ready: () => {
    console.log('ready')
  }
});`)

testUnchanged(`import Base from 'base';

export default Base.extend({
  other: () => {
    console.log('other')
  }
});`)

然后我们将需要修改的代码粘贴进 AST explorer

screenshot

注意红色框出来的 node,好的开始写 codemod。

function transformer(file, api) {
  const j = api.jscodeshift

  // TODO 等下要写的过滤函数

  // 把 Identifier 节点的 name 从 finished 改成 ready 就行了 
  const replaceFishined = p => {
     Object.assign(p.node, { name: 'ready' })
     return p.node
  }

  return (
    // 读取文件
    j(file.source)
      // 找到 Identifier 节点,且其名字为 finished
      .find(j.Identifier, { name: 'finished' })
      // TODO 要验证一下是不是 Base.extend 里面的
      .replaceWith(replaceFishined)
      .toSource()
  )
}

module.exports = transformer

怎么过滤呢?主要就是通过 path 找到他的父节点,然后逐一判断类型和名字是不是符合预期的。

  • 先确定是不是 Propoerty:
  const isProperty = p => {
    return (
      p.parent.node.type === 'Property' &&
      p.parent.node.key.type === 'Identifier' &&
      p.parent.node.key.name === 'finished'
    )
  }
  • 找到 CallExpression:
  const isArgument = p => {
    if (p.parent.parent.parent.node.type === 'CallExpression') {
      const call = p.parent.parent.parent.node
      return checkCallee(call.callee)
    }
    return false
  }
  • 然后看看 CallExpression是不是 Base.extend:
  const checkCallee = node => {
    const types = (
      node.type === 'MemberExpression' &&
      node.object.type === 'Identifier' &&
      node.property.type === 'Identifier'
    )

    const identifiers = (
      node.object.name === 'Base' &&
      node.property.name === 'extend'
    )

    return types && identifiers
  }

大功告成:

function transformer(file, api) {
  const j = api.jscodeshift

  const isProperty = p => {
    return (
      p.parent.node.type === 'Property' &&
      p.parent.node.key.type === 'Identifier' &&
      p.parent.node.key.name === 'finished'
    )
  }

  const checkCallee = node => {
    const types = (
      node.type === 'MemberExpression' &&
      node.object.type === 'Identifier' &&
      node.property.type === 'Identifier'
    )

    const identifiers = (
      node.object.name === 'Base' &&
      node.property.name === 'extend'
    )

    return types && identifiers
  }

  const isArgument = p => {
    if (p.parent.parent.parent.node.type === 'CallExpression') {
      const call = p.parent.parent.parent.node
      return checkCallee(call.callee)
    }
    return false
  }

  const replaceFishined = p => {
    Object.assign(p.node, { name: 'ready' })
    return p.node
  }

  return (
    j(file.source)
      .find(j.Identifier, { name: 'finished' })
      .filter(isProperty)
      .filter(isArgument)
      .replaceWith(replaceFishined)
      .toSource()
  )
}

module.exports = transformer

测试通过,搞定!

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。