近年、JavaScriptを取り巻く状況は一変しています。JavaScript言語のコアは進化し続けて、基本中の基本である変数宣言の方法さえ変更されているのです。ES6では、letやconst、アロー関数などの変更がコアに導入され、開発者とアプリケーションにメリットをもたらしました。
開発者は長く使えるコードを書き、維持する負担が増しています。この記事では、大規模なコードのリファクタリング作業をcodemodとJSCodeshiftツールで自動化する方法を紹介します。言語の新機能を利用したいときに、コードを簡単にアップデートできます。
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なります。
body配列のVariableDeclarationを見ると、先ほど定義したconstがあるのが分かります。VariableDeclarationはすべてid属性を持ち、nameなどの重要な情報を含みます。codemodを用いて、使われているfooのnameをすべて変更したい場合、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は、findVariableDeclaratorsやrenameToなどの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’sとUpdateExpression’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 CoxとMichael Wanyoikeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Refactor Code in Your Lunch Break: Getting Started with Codemods)
[翻訳:関 宏也/編集:Livit]