suguru.dev

バンクーバーで働くエンジニアの備忘録

AST + Prettierで快適にコードを一括変換しよう

概要

この記事はQiita Node.js Advent Calendar 2019 5日の記事です。

「Node.jsのプロジェクトにTypeScriptを導入したい」

ちょうど去年の12月に面接で聞かれたことでした。

あなたならどう答えますか?

今回は面接のネタとしても使える、ASTとPrettierを利用してコードを一括変換する方法を紹介します。

ASTとは

AST(Abstract Syntax Tree)とは抽象構文木のことで、コードを抽象化しツリー構造にパースしたものです。 身近なところではTypeScript、Eslint、Prettierなどで使用されています。

コードの変換方法

コードを変換するアプローチは色々ありますが、ASTに出会う前はよく正規表現を利用してコードの変換をしていました。

正規表現による変換

例えば varconst に変換するくらいなら正規表現のほうが早いかもしれません。

例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',
  ...
}

varconstを比較したとき、大きな違いは kind の部分だけになります。

どうですか、AST簡単ですね!

しかし大きな問題があります。 パーサはパースしかしてくれずコードの再形成はしてくれないのです…困った。

Prettierによるコードの再形成

そこでPrettierの出番です。

PrettierJavaScript界隈では最も有名なコードの整形ツールの一つです。具体的にはコードを特定のパーサで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関数の中に任意のトラバーサを挟むことができます。 今回はvarconstに変換したいだけなので、コードは以下のように書けます。

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のトラバーサになります。 変換したいNodetypeを指定すると、その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ファイルに変換するツールです。

主な機能は、

などがあります。

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を使ってみて、面接でネタにしてくださいね!

リンク