Help us understand the problem. What is going on with this article?

JavaScriptのリファクタリングツール「jscodeshift」の使い方

はじめに

JavaScriptのコードを一括で変換したり修正したい場合、正規表現などを使い置換しますか?
シンプルなケースであればそれでも問題ないですが、複雑な変換であればASTベースでコードを自在に変換できるjscodeshiftが便利です。

jscodeshiftを利用すると、以下のようなことができます。

例) functionで書かれた関数をアロー関数に一括で変換

target/arrow-function/index.js

const fn = function() {
  console.log("foo");
}.bind(this);

[1, 2, 3].map(function(v) {
  return v * v;
});

Promise.resolve()
  .then(function() {
    console.log("foo");
  })
  .then(function() {
    return 1;
  });

$ npx jscodeshift -t transforms/arrow-function.js target/arrow-function/index.js

transforms/arrow-function.jsは事前に準備

target/arrow-function/index.js

const fn = () => {
  console.log("foo");
};

[1, 2, 3].map(v => v * v);

Promise.resolve()
  .then(() => {
    console.log("foo");
  })
  .then(() => 1);

コマンドを実行しただけで、コードが一括で変換されました。

この記事ではjscodeshiftの紹介から、基本的な使い方までを紹介します。ASTの入門としても良いと思います。

コード変換の必要性

フロントエンド開発の中でコードを新しい書き方に変換したいケースに遭遇することは多々あると思います。

例えば

  • JavaScriptの新しい構文の登場で、シンプルな記述に
  • 不要になった記述を削除
  • 新しいライブラリのAPIを使う
  • ライブラリのブレイキングチェンジによるコードの修正

などなど、様々なケースがあり、これらを手動で行ったり、正規表現を使った検索や置換では限界があります。また、現状のコードが複数のパターンある場合はそれぞれの正規表現を作成して、別々に適用するのも手間です。

そんなときにjscodeshiftなどのASTベースのツールを使うことで、難しい変換処理をプログラマブルに簡単に行うことができます。

ASTについて

コードをプログラマブルに変換したい場合、ASTを使うことが多いと思います。

ASTはフロントエンド開発に触れる中で身近な存在になっています。例えばBabelPrettierESLintTypeScriptなどはASTをベースに作られています。

ASTとは、Abstract Syntax Treeの略で、日本語にすると抽象構文木です。

ソースコードを解析し、それを木構造で表現したものです。

例えばvar a = 0というコードは以下のような木構造で表現されます。

スクリーンショット 2019-11-28 17.11.34.png

そしてJSONであれば以下のように表現されます。

スクリーンショット 2019-11-28 17.13.10.png

つまりJavaScriptの世界においてはただのオブジェクト(JSON)なので、それをプログラマブルに扱い、変換することが可能になります。

どのようなASTが生成されるかは、AST Explorerを使うことで簡単に見れます。是非手元のコードで試してみてください。

Babel Plugin

JavaScriptコードを変換する代表例としてBabelのPluginを作るというアプローチもあります。
今日紹介するjscodeshiftとできることは基本的に同じですが、BabelのPluginはビルドプロセスでコンパイル時に毎回適用したいケースに向いています。例えばReactのJSXをJSに変換するのはBabelのPluginを使うことが多いです。

特定のタイミングで1回だけ実行して終わり。というユースケースではjscodeshiftを使う方が簡単だと思います。

それでは、ここからはjscodeshiftについて深ぼっていきます。

jscodeshiftとは

jscodeshiftとはFacebookが開発しているOSSで、複数のJavaScriptまたはTypeScriptのファイルに対して、ASTによる操作を簡単に行えるツールです。

ASTの操作などを行うrecastというライブラリをラップしたAPIを提供しています。またrecastが依存しているast-typesについても知っておく必要があります。

利用者は、変換処理が記述された自作またはOSSのtransform fileを用意して、jscodeshiftコマンドで変換対象のコードを指定し、リファクタリングを実行する流れになります。

例)

$ npx jscodeshift -t transform.js src

OSSとして様々なtransform fileが提供されています。

jscodeshiftを使ってみる

自分でtransform fileを作成する前に、OSSとして用意されているものを使ってみるところから始めましょう。今回はjs-codemodjs-codemod/transforms/no-vars.jsを使います。

これは、varconstまたはletに書き換えてくれるtransform fileです。
js-codemodはnpmにも公開されているので、それを使ってもいいのですが最新版がpublishされてないのでリポジトリをcloneします。

git clone https://github.com/cpojer/js-codemod.git

それではvarで書かれたコードを手元に用意しましょう。

index.js

for (var i = 0; i < 10; i++) {

}

var letItBe;
var shouldBeLet = 0;
var shouldBeConst = 0;

function mutate() {
  shouldBeLet = 1;
}

var whileIterator = 10;
while (whileIterator > 0) {
  whileIterator--;
}

あとは、jscodeshiftを使って変換を適用します。

$ npx jscodeshift -t js-codemod/transforms/no-vars.js index.js

見事に変換されました。

for (let i = 0; i < 10; i++) {

}

let letItBe;
let shouldBeLet = 0;
const shouldBeConst = 0;

function mutate() {
  shouldBeLet = 1;
}

let whileIterator = 10;
while (whileIterator > 0) {
  whileIterator--;
}

自分で変換処理を書く

jscodeshiftを活用していく際は、transform fileをプロジェクトや要件に合わせて自分で作成していくことになると思います。本記事ではtransform fileをJavaScriptファイルとして作成しますが、TypeScriptでも書くことが可能です。

transform file

transform fileは以下のように最大で3つの引数を受け取り、ソースコードを文字列として返す関数をモジュールとして作成するだけです

module.exports = function(fileInfo, api, options) {
  // `fileInfo.source`をもとにASTでの変換操作を行う
  // ...
  // 変換操作した結果を返す
  return source;
};

fileInfo

Property Description
path ファイルのパス(string)
source ソースコード(string)

api

Property Description
jscodeshift jscodeshiftが提供するAPIの参照
j 同上
stats --dry実行中にデータを収集するヘルパー
report 渡された文字列を標準出力に出力する関数

options

全てのオプション情報が含まれます。ここで、プロジェクト固有の値も渡すことができます。

例)

$ jscodeshift -t transform.js src --foo=bar

options{foo: 'bar'}を含んだオブジェクトになります。

戻り値

基本的にはソースコードを文字列として返しますが、戻り値に応じて変換のステータスが決められます。

  • 入力と違う文字列を返す: OK
  • 何も返さない: skipped
  • 入力と同じ文字列を返す: unmodified
  • エラー発生: error

jscodeshiftはこれらの結果に従って変換のステータスを処理します。

foo.bar()をfoo.baz()に変換

今回は「foo.bar()をfoo.baz()に変換」というシンプルな例を題材に自分でtransform fileを作成してみます。

対象のNodeを見つける

まずは、foo.bar()を検出します。検出したいコードがどんなAST NodeなのかはAST Explorerで確認します。

jscodeshift関数にsourceを渡すとCollectionオブジェクトが返ってきます。Collectionが持つfindfiltermapforEachなどの汎用的なAPIを利用することができます。
特定のNodeを取得する場合はfind関数が便利です。また、第2引数で具体的な条件も指定することができます。

今回はCallExpressionが対象なので、以下のようになります。

module.exports = function(fileInfo, { jscodeshift }, options) {
  const j = jscodeshift;
  const root = jscodeshift(fileInfo.source);
  root
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" }
      }
    })
};

Nodeを置き換える

次は、foo.baz()への変換を行います。find関数で検出できたNodeに対して変換処理を記述します。

forEach関数のコールバックの第一引数のpathは、実際のASTをラップしてparentなどの追加情報を持つNodePathです。詳しい詳細はast-typesを参照してください。

変換については、今回replaceWith関数で別のNodeに置き換えます。

module.exports = function(fileInfo, { jscodeshift }, options) {
  const j = jscodeshift;
  // ...
    .forEach(path => {
      j(path).replaceWith(
        // ...
      );
    });
};

新しいNodeの作成

replaceWith関数に渡すNodeは、ASTノードを簡単に作成できるast-typesのBuilderメソッドを利用します。jscodeshift経由で利用しましょう。メソッドはNodeのTypeを小文字始まりにしたものです。

module.exports = function(fileInfo, { jscodeshift }, options) {
  const j = jscodeshift;
  // ...
        j.callExpression(
          j.memberExpression(j.identifier("foo"), j.identifier("baz")),
          path.value.arguments
        )
  // ...
};

成果物

最終的には以下のようなコードになりました。

transform.js

module.exports = function(fileInfo, { jscodeshift }, options) {
  const j = jscodeshift;
  const root = jscodeshift(fileInfo.source);

  root
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" }
      }
    })
    .forEach(path => {
      j(path).replaceWith(
        j.callExpression(
          j.memberExpression(j.identifier("foo"), j.identifier("baz")),
          path.value.arguments
        )
      );
    });
  return root.toSource();
};

今回の例では、正規表現で書いたほうが簡単でしたが、自分でtransform fileを作成してプログラマブルに変換する流れが分かったと思うので、次は少し複雑な変換を行います。

Object.assign()をスプレッド構文で置き換える

最後に、Object.assignで行っていたオブジェクトのコピーやマージをスプレッド構文で書き換えます。

js-codemodrm-object-assignを参考にして実装していきます。

Object.assign({}, a);

Object.assign({ a: 1 }, b, { c: 3 });

上記のコードがスプレッド構文を使った下記のコードになるイメージです。

({
  ...a,
});

({
  a: 1,
  ...b,
  c: 3,
});

対象のNodeを見つける

まずは変換対象のNodeをfind関数で取得しましょう。

AST Explorerで以下のコードを見て、どんなASTか確認すると簡単ですね。

Object.assign({}, a);

スクリーンショット 2019-11-30 19.21.53.png

module.exports = function(fileInfo, { jscodeshift }, options) {
  const j = jscodeshift;
  const root = jscodeshift(fileInfo.source);

  root
    .find(j.CallExpression, {
      callee: { object: { name: "Object" }, property: { name: "assign" } },
      arguments: [{ type: "ObjectExpression" }]
    })
};

Object.assign({}, ...b);など、SpreadElementを引数に渡しているコードだとNodeが持つ情報が違うのでサンプルコードではfilter処理をしていますが今回は割愛します

スプレッド構文で置き換える

あとは、取得したCallExpressionをスプレッド構文に置き換えれば完成です。

取得できた各Nodeに対して特定の変換処理を実装していきます。今回はrmObjectAssignCall関数を実装していきます。

module.exports = function(fileInfo, { jscodeshift }, options) {
  // ...
  const rmObjectAssignCall = path =>
    j(path).replaceWith();

  root
    .find(j.CallExpression, {
      callee: { object: { name: "Object" }, property: { name: "assign" } },
      arguments: [{ type: "ObjectExpression" }]
    })
    .forEach(rmObjectAssignCall);

  return root.toSource();
};

スプレッド構文のコードをAST Explorerで見ると、どんなNodeに置き換えればいいか分かりますね。

({
  a: 1,
  ...b
});

スクリーンショット 2019-12-01 10.59.55.png

ObjectExpressionを作成していきます。
AST Nodeを作成するBuilderに何を渡すといいのかはドキュメントがないので、ast-typesのコードを見て確認しましょう。

module.exports = function(fileInfo, { jscodeshift }, options) {
  // ... 
  const rmObjectAssignCall = path => {
    const properties = path.value.arguments.reduce(
      (allProperties, { ...argument }) => {
        if (argument.type === "ObjectExpression") {
          const { properties } = argument;
          return [...allProperties, ...properties];
        }

        return [...allProperties, { ...j.spreadProperty(argument) }];
      },
      []
    );
    return j(path).replaceWith(j.objectExpression(properties));
  };
  // ...
};

成果物

最終的には以下のようなコードになりました。

transform.js

module.exports = function(fileInfo, { jscodeshift }, options) {
  const j = jscodeshift;
  const root = jscodeshift(fileInfo.source);

  const rmObjectAssignCall = path => {
    const properties = path.value.arguments.reduce(
      (allProperties, {  ...argument }) => {
        if (argument.type === "ObjectExpression") {
          const { properties } = argument;
          return [...allProperties, ...properties];
        }

        return [...allProperties, { ...j.spreadProperty(argument) }];
      },
      []
    );
    return j(path).replaceWith(j.objectExpression(properties));
  };

  root
    .find(j.CallExpression, {
      callee: { object: { name: "Object" }, property: { name: "assign" } },
      arguments: [{ type: "ObjectExpression" }]
    })
    .forEach(rmObjectAssignCall);

  return root.toSource();
};

まとめ

  • JavaScriptのコードをリファクタリングしたいときに便利なASTベースのツールjscodeshiftについて紹介しました
  • OSSとして公開されているtransform fileを利用したり、自作して活用していきます
  • 自作する時は、AST Explorerを使っていきます
  • ドキュメントがあまり用意されていないので、TypeScriptの型定義を都度参照していくのが良さそうです

テスト

今回はjscodeshiftのテストについては触れられませんでした。
同僚の@pirosikickがテストについて書いてくれたので、是非こちらも合わせて読んでください。

jscodeshiftのテストを書く - Qiita

参考リンク

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away