AST + Prettierで快適にコードを一括変換しよう
概要
この記事はQiita Node.js Advent Calendar 2019 5日の記事です。
「Node.jsのプロジェクトにTypeScriptを導入したい」
ちょうど去年の12月に面接で聞かれたことでした。
あなたならどう答えますか?
今回は面接のネタとしても使える、ASTとPrettierを利用してコードを一括変換する方法を紹介します。
ASTとは
AST(Abstract Syntax Tree)とは抽象構文木のことで、コードを抽象化しツリー構造にパースしたものです。 身近なところではTypeScript、Eslint、Prettierなどで使用されています。
コードの変換方法
コードを変換するアプローチは色々ありますが、ASTに出会う前はよく正規表現を利用してコードの変換をしていました。
正規表現による変換
例えば var
を const
に変換するくらいなら正規表現のほうが早いかもしれません。
例1
以下の例を見てみましょう。
// test.js var a = 'a'; var b = 'b'; var c = 'c';
これくらいは余裕ですね。
// 回答例 const str = fs.readFileSync(path.resolve(__dirname, './test.js'), 'utf8'); str.replace(/var/g, 'const')
例2
次の例はだいぶ意地悪なコードですが、まだいけそうな気がします。
var d = 'd'; var variable = 'var var var';
// 回答例 str.replace(/var\s(.+?=)/g, 'const $1')
例3
次の例はいかがでしょう。
/** Goの型宣言 var e =1; C#の型宣言 var e = 1; **/ var e = 1;
まだなんとかなりそうですが…
正規表現でコードを変換する場合、試行錯誤して例外を見つけ、最終的に呪文のような正規表現を書くことになるかと思います。
ASTによる変換
そこで登場するのがASTです。
今回はbabelのパーサを使いコードをASTに変換します。
以下のように実行することでASTを出力することができます。ちなみに util.inspect
を使うことでネスト構造のオブジェクトを展開することができます。
const parser = require('@babel/parser'); console.log(util.inspect(parser.parse(str), false, null));
以下は実行結果よりvar
宣言の部分とコメントの部分の抜粋です。
// var宣言部分 Node { type: 'VariableDeclaration', kind: 'var', ... } // コメント部分 trailingComments: [ { type: 'CommentBlock', value: '*\n Goの型宣言\n var e =1;\n C#の型宣言\n var e = 1;\n*', ... } ]
AST上では var
の宣言とコメントは全く別物として扱われます。また const
定義の部分は以下のような定義になります。
// const宣言部分 Node { type: 'VariableDeclaration', kind: 'const', ... }
var
とconst
を比較したとき、大きな違いは kind
の部分だけになります。
どうですか、AST簡単ですね!
しかし大きな問題があります。 パーサはパースしかしてくれずコードの再形成はしてくれないのです…困った。
Prettierによるコードの再形成
そこでPrettierの出番です。
PrettierはJavaScript界隈では最も有名なコードの整形ツールの一つです。具体的にはコードを特定のパーサでASTに変換し、そのASTを元にコードを整えつつ再形成します。JavaScriptの場合、先程登場した @babel/parser
が使用されています。
今回やりたいことはシンプルで、パース後のASTの var
の部分を const
に変換し、Prettierにコードの再形成をしてもらいたいのです。
そのために作ったツールがprettier-hook
です。このツールは上記の機能をサポートしています。
コードは以下のように書きます。
// hook.js const { Ast, hooks } = require('prettier-hook'); function parse(ast) { // ASTの変換処理 return ast; } hooks.babylon.addHook(parse);
余談ですが、babylon
とはbabel
がモノレポ化前の名残で、現在は@babel/parser
として開発されています。
このparse
関数の中に任意のトラバーサを挟むことができます。
今回はvar
をconst
に変換したいだけなので、コードは以下のように書けます。
function parse(ast) { new Ast() // 変換したいTypeを指定 .set('VariableDeclaration', (parent, key) => { const node = parent[key]; node.kind = 'const'; // varをconstに変換する }) .resolveAst(ast); return ast; }
このAst
のクラスがprettier-hook
のトラバーサになります。
変換したいNode
のtype
を指定すると、そのtype
が現れたときに関数が呼び出され、ASTの変換を可能にしています。
以下のように実行することで、ASTを使用して変換することができます。
npx prettier-hook --require hook.js test.js // or npx prettier-hook --require hook.js --write test.js
// 実行結果 const a = "a"; const b = "b"; const c = "c"; const d = "d"; const variable = "var var var"; /** Goの型宣言 var e =1; C#の型宣言 var e = 1; **/ const e = 1;
ツールの紹介
prettier-hookを利用して作成したツールを軽く紹介します。
JavaScrip2TypeScript
JavaScriptファイルをTypeScriptファイルに変換するツールです。
主な機能は、
- require/exportsをimport/exportに変換
- JSDocのマイグレーション
- クラスの型定義
などがあります。
JSDocの例
/** * @param {number} a * @param {boolean} [b] * @param {string} [c] **/ function test(a, b, c = 1) { return 1; }
/** * @param {number} a * @param {boolean} [b] * @param {string} [c] **/ function test(a: number, b?: boolean, c: string | number = 1) { return 1; }
Classの例
class Test { constructor() { this.num = 1; this.str = 'str'; } getNum() { return this.num; } getStr() { return this.str; } } Test.num = 1;
class Test { static num: number; num: number; str: string; constructor() { this.num = 1; this.str = "str"; } getNum() { return this.num; } getStr() { return this.str; } } Test.num = 1;
ツール作成も含め、1週間弱で400ファイル以上あるプロジェクトをTypeScriptに変換できました。
実際、ツールも完璧ではないのと、アンチパターンのコードが多かったので、変換後に手直しが必要でした。
まとめ
コードを変換する際にASTを利用するアプローチについて紹介しました。 正規表現でコードを変換するコードを書くより、ASTで書くことにより保守性も上がるケースも多いのでは無いかと思います。 ぜひコードを変換する際にはASTを使ってみて、面接でネタにしてくださいね!