JavaScript疲れに効く! codemodとJSCodeshiftでリファクタリングが捗る

2017/07/21

Chris Laughlin

73

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

開発者なら誰しもJavaScriptをはじめとするコードのメンテナンスが面倒だと感じています。日々変更される標準や構文、サードパーティのパッケージ変更に付いていくのも大変です。

近年、JavaScriptを取り巻く状況は一変しています。JavaScript言語のコアは進化し続けて、基本中の基本である変数宣言の方法さえ変更されているのです。ES6では、letconst、アロー関数などの変更がコアに導入され、開発者とアプリケーションにメリットをもたらしました。

開発者は長く使えるコードを書き、維持する負担が増しています。この記事では、大規模なコードのリファクタリング作業をcodemodJSCodeshiftツールで自動化する方法を紹介します。言語の新機能を利用したいときに、コードを簡単にアップデートできます。

Codemod

Codemodは、Facebookが開発した大規模コードベース向けのリファクタリング支援ツールです。IDEを使ってクラスや変数名をリファクタリングすると、1度に1ファイルしか処理できない制約があります。検索や置換は正規表現で対応しますが、変更が必要なインプリメンテーションが複数ある場合など、対処できないシナリオも多くあります。

CodemodはPythonツールで、検索したい表現、置換後の表現をはじめ、多数のパラメーターを指定できます。

codemod -m -d /code/myAwesomeSite/pages --extensions php,html \
    '<font *color="?(.*?)"?>(.*?)</font>' \
    '<span style="color: \1;">\2</span>'

<font>タグの代わりにspanにインラインでカラースタイル指定しています。最初の2つのパラメーターで、複数の行に渡るマッチングを実行することと(-m)、処理を始めるディレクトリ(-d /code/myAwesomeSite/pages)を指定します。処理するファイルの拡張子も制限できます(–extensions php,html)。さらに、検索表現と置換表現を渡します。置換表現を渡さないと、実行したときに入力するためのプロンプトを表示します。便利なツールですが、正規表現を使う検索や置換ツールと違いはありません。

JSCodeshift

JSCodeshiftも、Facebookが開発したリファクタリングツールキットです。codemodから一歩進んだツールで、複数のファイルに対してcodemodを走らせます。Nodeモジュールとして使用し、APIはきれいで使いやすく、裏ではRecastが動いています。RecastはAST-to-AST(Abstract Syntax Tree:抽象構文木)変換ツールです。

Recast

RecastはNodeモジュールで、JavaScriptのコードをパースして書き換えます。文字列フォーマットでコードを整形し、AST構造に従ったオブジェクトを生成します。関数宣言などのパターンに関して、コードを検査できます。

var recast = require("recast");

var code = [
    "function add(a, b) {",
    "  return a + b",
    "}"
].join("\n");

var ast = recast.parse(code);
console.log(ast);
//output
{
    "program": {
        "type": "Program",
        "body": [
            {
                "type": "FunctionDeclaration",
                "id": {
                    "type": "Identifier",
                    "name": "add",
                    "loc": {
                        "start": {
                            "line": 1,
                            "column": 9
                        },
                        "end": {
                            "line": 1,
                            "column": 12
                        },
                        "lines": {},
                        "indent": 0
                    }
                },
        ...........    

2つの数字を足し算する関数のコードを通しています。オブジェクトを整形しログをとると、ASTを見られます。FunctionDeclarationと関数の名前などが分かります。JavaScriptオブジェクトなので、適当に修正できます。print関数を走らせれば、コードが更新されて返ってきます。

AST(抽象構文木)

Recastはコード文字列からASTを生成します。ASTはソースコードの抽象的な構文のツリー表現です。ツリーの各ノードはソースコードではコンストラクトを表現し、ノードはコンストラクトの重要な情報を提供します。ASTExplorerはブラウザーベースのツールで、コードツリーの整形と理解を助けます。


ASTExplorer
を使って、簡単なコードのASTを見てみます。fooというconstを宣言し、文字列「bar」を代入します。

const foo = 'bar';

下のASTなります。

Screenshot of the resulting AST

body配列のVariableDeclarationを見ると、先ほど定義したconstがあるのが分かります。VariableDeclarationはすべてid属性を持ち、nameなどの重要な情報を含みます。codemodを用いて、使われているfoonameをすべて変更したい場合、name属性に注目して、すべての事例に対して繰り返し処理を実行して、名前を変更します。

インストールと使い方

上のツールとテクニックで、JSCodeshiftの機能を使いこなせます。JSCodeshiftはnodeモジュールなので、プロジェクトあるいはグローバルレベルでインストールできます。

npm install -g jscodeshift

インストールしたら、JSCodeshiftにパラメータを何個か渡すことで、用意されているcodemodが使えます。基本的な構文は、jscodeshiftを呼ぶときに変換したいファイル(複数個でもよい)のパスを指定します。必須のパラメータは変換ルールファイルの位置を示すパラメータ(-t)です。これは、ローカルファイルでもよいし、codemodファイルのURLでも構いません。デフォルトでは、カレントディレクトリのtransform.jsファイルを探します。

ほかの便利なパラメータは、ドライラン(-d)です。ドライランは、変換して、ファイルの更新はしません。Verbose(-v)は、変換プロセスに関する情報をすべてログに出力します。変換はcodemodで実行します。Codemodは関数をエクスポートする単純なJavaScriptモジュールで、次のパラメータをとります。

  • fileInfo
  • api
  • options

FileInfoは、処理しているファイルの情報(パスとソースも含む)をすべて保持しています。APIは、findVariableDeclaratorsrenameToなどのJSCodeshiftのヘルパー関数へのアクセスを可能にするオブジェクトです。最後のパラメータはoptionsで、CLIからcodemodにオプションを渡します。たとえば、サーバーで実行すると、すべてのファイルにコードバージョンを加えたい場合、CLIからjscodeshift -t myTransforms fileA fileB --codeVersion=1.2でオプションを渡せます。オプションには{codeVersion: '1.2'}が含まれます。

エクスポートする関数の内部で、変換されたコードを文字変数として返します。たとえば、const foo = 'bar'というコード文字列のconst fooをconst barに置き換えるならcodemodは以下の通りです。

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

  return j(file.source)
    .find(j.Identifier)
    .forEach(path => {
      j(path).replaceWith(
        j.identifier('bar')
      );
    })
    .toSource();
}

関数をいくつもつなげて最後にtoSource()を呼ぶと、変換したコード文字列を作成します。

コードを返すときのルールがあります。入力と違う文字列を返すのは変換が成功した場合で、入力と同じ文字列を返すのは変換が失敗した場合です。なにも返さない場合は、変換が不要だと意味します。JSCodeshiftはこれらの結果に従って変換のステータスを処理します。

既存のcodemod

よく使うリファクタリングは、すでにcodemodに用意されています。

そのうちの1つjs-codemod no-varsは、コード内で使われているvarをすべて、letかconstに変換します。あとでvarに値が代入されるならlet、値の変更がなければconstにします。

js-codemod template-literalsは、文字列結合のインスタンスをテンプレートリテラルで置換します。

const sayHello = 'Hi my name is ' + name;
//after transform
const sayHello = `Hi my name is ${name}`;

Codemodの書き方

上のvarを置換するcodemodを例に、コードを分解して、複雑なcodemodの動作を解説します。

const updatedAnything = root.find(j.VariableDeclaration).filter(
            dec => dec.value.kind === 'var'
        ).filter(declaration => {
            return declaration.value.declarations.every(declarator => {
                return !isTruelyVar(declaration, declarator);
            });
        }).forEach(declaration => {
            const forLoopWithoutInit = isForLoopDeclarationWithoutInit(declaration);
            if (
                declaration.value.declarations.some(declarator => {
                    return (!declarator.init && !forLoopWithoutInit) || isMutated(declaration, declarator);
                })
            ) {
                declaration.value.kind = 'let';
            } else {
                declaration.value.kind = 'const';
            }
        }).size() !== 0;
    return updatedAnything ? root.toSource() : null;

上のコードはvarsを置換するcodemodの中心部分です。var、letおよびconstを含む変数宣言にフィルターをかけます。フィルターはvar宣言のみ返します。結果を二番目のフィルターに渡して、カスタム関数isTruelyVarを呼びます。ここで、varがクロージャ内にあるか、2回宣言されているか、ホイストされる関数宣言かなどvarの使い方を判定し、変換しても安全かを判断します。isTruelyVarを通るvarはすべて、forEachloopで処理します。

ループの中で、varはループ内かチェックします。

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

varがループ内にあるか検知するために、親タイプをチェックします。

const isForLoopDeclarationWithoutInit = declaration => {
        const parentType = declaration.parentPath.value.type;
        return parentType === 'ForOfStatement' || parentType === 'ForInStatement';
    };

varがループ内にあり、変化しないなら、constに変換します。varノードに対してAssignmentExpression’sUpdateExpression’sにフィルターをかけて変化のチェックします。AssignmentExpressionで、いつ、どこでvarに値が割り振られたか分かります。

var foo = 'bar';

UpdateExpressionで、いつ、どこでvarが更新されたか分かります。

var foo = 'bar';
foo = 'Foo Bar'; //Updated(更新しました)

varがループ内にあり、変化するなら、インスタンスが生成されたあとに再割り当てできるletを使います。Codemodの最終行で、更新されたものがあるかどうか、たとえばvarのうち、変更されたものがあるかをチェックします。変更があれば新しいソースファイルを返して、そうでなければnullを返します。nullを返すのは、処理の必要がないとJSCodeshiftに示しているのです。
Codemodのソースはここにあります。

Facebookの開発陣は、React構文の更新やReact APIの変更を処理するために、codemodをいくつも追加しています。その中のreact-codemod sort-compはReactライフサイクルメソッドをソートして、ESlint sort-comp ruleに適合します。

よく使われるReact codemodにReact-PropTypes-to-prop-typesがあります。コアReactチームは、React.PropTypesを別のノードモジュールに移しました。codemodはこの変更に対応しています。React v16以降、コンポーネントでpropTypesを使い続ける場合は、prop-typesをインストールする必要があります。codemodのユースケースの絶好の例です。propTypesを使う方法が変わり、以下すべて有効になりました。

Reactをインポートして、デフォルトインポートからPropTypesにアクセスします。

import React from 'react';

class HelloWorld extends React.Component {

    static propTypes = {
        name: React.PropTypes.string,
    }
    .....

PropTypesのnameを指定して、Reactをインポートします。

import React, { PropTypes, Component } from 'react';

class HelloWorld extends Component {

    static propTypes = {
        name: PropTypes.string,
    }
    .....

PropTypesのnameを指定して、Reactをインポートします。PropTypesはステートレスコンポーネントで宣言します。

import React, { PropTypes } from 'react';

const HelloWorld = ({name}) => {
    .....
}

HelloWorld.propTypes = {
    name: PropTypes.string
};

3通りの方法があるので、正規表現で検索・置換するのは大変です。以下を走らせれば、簡単にPropTypesパターンを更新できます。

jscodeshift src/ -t transforms/proptypes.js

react-codemodsリポジトリからPropTypes codemodを取ってきて、プロジェクトのtransformsディレクトリに加えます。codemodはimport PropTypes from 'prop-types';を各ファイル加え、React.PropTypesをすべてPropTypesで置き換えます。

最後に

Facebookはコードメンテナンスでパイオニアの役割を果たしています。日々変化するAPIやコード規範に開発者が順応できているのは、Facebookのおかげです。JavaScript疲れは大きな問題になっていますが、紹介したツールを使えば、既存のコードの更新が楽になり、JavaScript疲れが少しでも癒せるはずです。

データベースに依存するサーバー側の開発では、データベースのサポートを維持し、データベースを最新バージョンにアップデートするのを保証する必要があります。日常的に移行スクリプトを作成することで、大きなバージョンアップにも、JavaScriptライブラリーを維持するために、codemodを移行スクリプトとして提供できます。Codemodならかなり大きな変更にも対応できると思います。

npmではインストールスクリプトも走らせられるので、いままでの移行プロセスとの整合性も良いと思います。インストール時やアップグレード時にcodemodを走らせると、アップグレードが速くなり、カスタマーの信頼も得やすくなるはずです。Codemodをリリースに含めることは、デモやガイドを更新するときに、単にカスタマーの利益になるだけでなく、メンテナンスのオーバーヘッドを低減できます。

codemodとJSCodeshiftの強力な機能を紹介しました。複雑なコードでも短時間で更新できます。Codemodから始めて、ASTExplorerやJSCodeshiftが使えると、目的にあったcodemodを作れます。既存のcodemodを最大限に利用して、時間が節約してください。

本記事はGraham CoxMichael Wanyoikeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Refactor Code in Your Lunch Break: Getting Started with Codemods

[翻訳:関 宏也/編集:Livit

Copyright © 2017, Chris Laughlin All Rights Reserved.

Chris Laughlin

Chris Laughlin

北アイルランドのベルファスト在住のアプリケーション開発者です。フロントエンド、特にJavaScriptの開発に注力しています。2010年からソフトウェアの開発に携わり、いまも毎日学び、知識をシェアしています。

Loading...