mizdra's blog

ぽよぐらみんぐ

Babel をリファクタリングツールとして使う

この記事は はてなエンジニア Advent Calendar 2021 の 5 日目の記事です。

4 日目は id:anatofuz さんの 「入社してから書いていた分報の行数を眺めてみる」 でした。日報に書き込んだ行数を可視化するというアイデアが面白い! 僕も日報書いているので今度可視化してみようと思います。

anatofuz.hatenablog.com

本題

さて今回はタイトルにもある通り、Babel の話をします。Babel というのは JavaScript のトランスパイラです。 JavaScript のソースコードを入力として受け取り、適切な変換を施し、JavaScript のソースコードを出力する (トランスパイルする) ツールです。主に新しい構文で書かれた JavaScript を、古いブラウザなどでも動くよう、古い構文で書かれた JavaScript に変換するために使われています。

実はあまり知られていないのですが、 Babel は旧構文への変換以外にも、ソースコードを機械的に書き換えるためのリファクタリングツールとしても利用できます。通常 Babel を利用する際は、src/配下にある JavaScript ファイルを、Babel で変換してdist/に出力しますが、Babel をリファクタリングツールとして利用する場合は、src/配下にある JavaScript ファイルを、Babel で変換してsrc/に出力することになります。

f:id:mizdra:20211205173833p:plain
旧構文への変換ツールとしての利用した時と、リファクタリングツールとしての利用した時の違い

JavaScript におけるリファクタリングツールとなると、codemods や JSCodeshift が有名で、Babel の名前は聞き慣れないと思います。しかし、公式ドキュメントにも Babel がリファクタリングツールとして利用できることが明記されており、きちんとユースケースの一部としてカバーされていることが分かります。

Here are the main things Babel can do for you:

  • Transform syntax
  • Polyfill features that are missing in your target environment (through a third-party polyfill such as core-js)
  • Source code transformations (codemods)
  • And more! (check out these videos for inspiration)

https://babeljs.io/docs/en/

リファクタリングツールとして利用する方法

Babel をリファクタリングツールとして利用する方法については、Babel のメンテナーである Nicolò Ribaudo 氏が発表されているので、その資料を参考にするのが良いです。

発表では React の Class Component を Function Component を書き換える例が紹介されています。合わせてリファクタリングのデモに使ったコード郡も以下のリポジトリで公開されており、ここから簡単に Babel を使ったリファクタリングを試せます。

github.com

デモを動かしてみる

まずはデモを動かしてみましょう。README に起動方法が書いてあるので、その手順通りにやれば良いです。

$ # リポジトリを clone
$ git clone https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2020
$ cd conf-holyjs-moscow-2020

$ # ディレクトリ構成を確認
$ ls -1F
Babel_ A refactoring tool.pdf
README.md
codemod/ ... Babel を使ったリファクタリングツールのコード
todomvc/ ... Class Component を含むリファクタリング対象のアプリケーションのコード

$ # codemod/ 配下に package.json があるので、`npm ci` で依存パッケージをインストールしておく
$ cd codemod
$ npm ci

$ # プロジェクトルートに戻る
$ cd ..

$ # リファクタリングツールを起動し、アプリケーションコードをリファクタリング
$ node codemod/run.js todomvc/js/*.{ts,tsx}

リファクタリングツールの実行が終わると、以下のように Class Component が Function Component に書き変わっているはずです。

$ git diff
diff --git a/todomvc/js/app.tsx b/todomvc/js/app.tsx
index 6fc2842..ded2b9c 100644
--- a/todomvc/js/app.tsx
+++ b/todomvc/js/app.tsx
@@ -7,167 +7,172 @@
 /// <reference path="./interfaces.d.ts"/>

 declare var Router;
-import React from "react";
+import React, { useState, useEffect } from "react";
 import ReactDOM from "react-dom";
 import { TodoModel } from "./todoModel";
 import { TodoFooter } from "./footer";
 import { TodoItem } from "./todoItem";
 import { ALL_TODOS, ACTIVE_TODOS, COMPLETED_TODOS, ENTER_KEY } from "./constants";

-class TodoApp extends React.Component<IAppProps, IAppState> {
+const TodoApp = props => {
+  const [nowShowing, setNowShowing] = useState(ALL_TODOS);
+  const [editing, setEditing] = useState(null);
...

リファクタリングツールの仕組み

codemod/配下のコードを見てみると分かるのですが、リファクタリングツールの実装はcodemod/codemod.jscodemod/run.jsのたった 2 つの JS ファイルから構成されています。codemod/codemod.jsは実行したい変換処理を表す Babel Plugin です。ここに Class Component を Function Component に変換する処理が書かれています。一方codemod/run.jsはコマンドライン引数で渡されたファイルを1ファイルずつ AST に parse し (ステップ1)、前述のBabel Plugin を使って AST を変換して (ステップ2)、AST を文字列化して元のファイルに上書きする (ステップ3) コードになっています。

// https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2020/blob/7c2b597f284f0fbafe3f823aba0e6bc2f0e46154/codemod/run.js#L20 より一部引用
function transform(source, filename, codemod) {
  // ステップ1
  const ast = recast.parse(source, {
    parser: {
      parse(source) {
        return parser.parse(source, {
          filename,
          tokens: true,
          sourceType: "module",
          // typescript や jsx が扱えるように
          plugins: ["typescript", "classProperties", "jsx"],
        });
      },
    },
  });

  // ステップ2
  babel.transformFromAstSync(ast, source, {
    code: false,
    cloneInputAst: false,
    configFile: false,
    // 注: ここで plugin を差し込んでいる
    plugins: [codemod],
  });

  // ステップ3
  return recast.print(ast).code;
}

任意のリファクタリングができるように処理を差し替える

何となく気づいていると思いますが、codemod/codemod.jsの実装を変更すれば、任意のリファクタリングを実行できるようになります。先程も触れましたが、codemod/codemod.jsの実態はただの Babel Plugin なので、Babel Plugin のルールに沿ってコードを書いていけば良いです。

// @ts-check
// ts-check を使って書くと、型検査が効いてお得
module.exports = function plugin(babel) {
  /** @type {import('@babel/core').types} */
  const t = babel.types;
  /** @type {import('@babel/core').template} */
  const template = babel.template;

  return {
    visitor: {
      StringLiteral(path) {
        // シングルクオートで囲んでいるstring literal をダブルクオートで囲むように
        path.node.extra.raw = path.node.extra.raw.replaceAll("'", '"');
      },
    },
  };
};

使い所

正直なところ、単にリファクタリングしたいだけであれば JSCodeshift を使えば事足りるので、Babel をリファクタリングツールとして使う場面はそうないと思います。JSCodeshift はリファクタリングに特化したツール故に、リファクタリングに特化した utility が用意されていて、公式 example やも豊富で、使い方を解説した資料も沢山あります。一方で、Babel を使った手法では、Babel Plugin 向けに提供されている API をそのままリファクタリングに利用できたり、Babel Plugin のノウハウをそのまま利用できるという利点があります。これは Nicolò Ribaudo 氏の発表スライドでも触れられています。

f:id:mizdra:20211205164558p:plain
Babel: A refactoring tool 63 ページより引用

特に Babel Plugin のノウハウをそのまま活かせる、というのは大きなメリットだと考えています。ちょっと Babel Plugin を書いたことのある人なら、簡単に扱えますし、codemod.js を実装するときも既存の Babel Plugin のコードを参考にしたり、そのまま流用できます。

実際に id:mizdra は社内でとあるリファクタリングを行う際に、既存の Babel Plugin の一部コードをそのまま流用してリファクタリングをする、ということをやっていました。Babel Plugin 向けに書かれたコードを一切変更せずそのまま使えるので、(JSCodeshift 向けの codemod を新規に書くよりは) 簡単にリファクタリングを実施できました。

まとめ

今回は Babel を使ったリファクタリング手法について紹介しました。JSCodeshift という良い代替ツールがあるので、出番はそうないと思いますが、覚えておくとどこかで役に立つかもしれません。

明日は id:yutailang0119 さんです!

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.