代码重构利器 —— jscodeshift

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

注意红色框出来的 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 条评论
登录 后参与评论

相关文章

来自专栏生信宝典

Python学习教程 (六)

作业(三) 使 “作业(二)” 中的程序都能接受命令行参数 import sys sys.argv import optparse 用到的知识点 2.备注...

2139
来自专栏技术之路

Caliburn.Micro学习笔记(三)----事件聚合IEventAggregator和 Ihandle<T>

今天 说一下Caliburn.Micro的IEventAggregator和IHandle<T>分成两篇去讲这一篇写一个简单的例子 看一它的的实现和源码 下一篇...

1909
来自专栏人工智能LeadAI

资源安全

在所有异常分之里记得释放资源,存在较为严重的安全隐患。此处,引入ScopedExit的封装,使用C++特有的RAII机制,在析构函数中完成资源的安全释放;即使程...

752
来自专栏乐沙弥的世界

MHA 手动故障转移

        MHA提供了3种方式用于实现故障转移,分别自动故障转移,需要启用MHA监控;在无监控的情况下的手动故障转移以及基于在线手动切换。三种方式可以应对...

812
来自专栏鬼谷君

python 解析json loads dumps

1122
来自专栏ASP.NET MVC5 后台权限管理系统

ASP.NET MVC5+EF6+EasyUI 后台管理系统(29)-T4模版

本节不再适合本系统,在58,59节已经重构。请超过本节 这讲适合所有的MVC程序 很荣幸,我们的系统有了体验的地址了。演示地址 之前我们发布了一个简单的代码生...

2427
来自专栏跟着阿笨一起玩NET

C# 4.0命名参数和可选参数

Named And Optional Arguments - 命名参数和可选参数

421
来自专栏Seebug漏洞平台

Exim Off-by-one(CVE-2018-6789)漏洞复现分析

前段时间meh又挖了一个Exim的RCE漏洞[1],而且这次RCE的漏洞的约束更少了,就算开启了PIE仍然能被利用。虽然去年我研究过Exim,但是时间过去这么久...

4857
来自专栏乐沙弥的世界

使用 resource_limit 及 profile 限制用户连接

      数据库性能是一个永恒的话题,那就是如何使用更少的资源以达到更高效的性能。Oracle系统参数RESOURCE_LIMIT是一个用于控制用户对于数据库...

911
来自专栏乐沙弥的世界

MHA 自动故障转移步骤及过程剖析

    MHA是众多使用MySQL数据库企业高可用的不二选择,它简单易用,功能强大,实现了基于MySQL replication架构的自动主从故障转移,本文主要...

853

扫码关注云+社区