Vim & VSCodeのフォーマッタを自作する（分析SQLを例に）

## 概要
[こちら](https://qiita.com/kai_data/items/6c119c43ad3626226dfc)の「分析SQLスタイルガイド」を拝見し、**SQLのフォーマッタには需要がある**と感じて早速作り始めてみた。とりあえず動くようになったので、これまでの一部始終を記事にする。具体的には、以下の内容を扱う。

1. [Prettier](https://prettier.io/)という既存のフォーマッタをコマンドやエディタから利用する方法
2. Prettierのプラグインとして任意の言語のフォーマッタを実装する方法（分析SQLを題材にする）

普段から分析SQLを書くがフォーマッタは使ったことがないという人は、まずこの後のPrettierの紹介を読んでほしい。きっと使いたくなるはず。

▼ちょっと画質悪いけど、1月23日現在の開発進捗。とりあえず動く。
![before_after.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540947/0e524785-e7ab-82c9-2887-d1f768d74a77.gif)

## Prettierとは
### Prettier
デフォルトではJavaScript・HTML・CSSなど[^default]に対応したコードフォーマッタ。これを使うと、コマンド一発で以下のようにコードをフォーマットできる。不自然に1行に並べられたコードがきれいに改行・インデントされ、セミコロンが挿入されていることがわかる。

[^default]: デフォルトで対応している言語は[こちら](https://prettier.io/docs/en/index.html)

```jsx:App.js
// フォーマット前
const http = require("http");const server = http.createServer((req, res) => {res.end("hello world")});server.listen(3000)


// フォーマット後
const http = require("http");
const server = http.createServer((req, res) => {
  res.end("hello world");
});
server.listen(3000);
```

これを導入すると、例えば以下のようなメリットがある。

- 最小限の労力でチーム内の**コーディングスタイルを統一**できる
- **コードディングスタイルではなくコードの内容**に意識と時間を集中できる

分析SQLの文脈だと、例えばチーム内に予約語大文字派と小文字派が共存していても、共有前にPrettierを使うだけでスタイルを揃えることができる。これは時間の節約にも貢献するだろう[^eg]。

[^eg]: 冒頭で引用した「分析SQLスタイルガイド」では、コーディングが楽だから予約語小文字を推奨していた。私もこれには賛成だが、予約語大文字派の同僚にクエリをチェックしてもらう際に申し訳なくて、最後に大文字に変換していた。**この手間がゼロになる！**

### Prettierプラグイン
Prettierはデフォルトで対応していない言語にも、プラグインで対応できる。SQLもこのパターンで、[prettier-plugin-pg](https://github.com/benjie/prettier-plugin-pg)というプラグインがあるが、実用段階ではないらしい。それなら**最低限機能するプラグインを自分で作ってしまおう**というのがこの記事のスタンス。ちなみに利用可能なプラグインは[ここ](https://prettier.io/docs/en/plugins.html#official-plugins)で紹介されている。

### 基本的な使い方
#### インストール
以下のコマンドでインストールできる。プラグインを利用する場合は一緒にインストールするとよい。`-g`オプションでグローバルにインストールすることもできるが、あまり推奨されていないようだ。

```sh
npm install --save-dev --save-exact prettier
```
#### コマンドラインからの利用
prettierをインストールしたプロジェクト内で以下のコマンドを実行する。対象は`App.js`のようなファイル名でもよいし、`.`でまとめて指定してもよい。私はまだ試していないが、[ドキュメント](https://prettier.io/docs/en/precommit.html)によるとcommit前に自動で実行することもできるらしい。

```sh
npx prettier --write App.js
```

#### Vim (Neovim) からの利用
[vim-prettier](https://github.com/prettier/vim-prettier)が便利。vim-plugを利用しているなら、以下を*.vimrc* (*init.vim*)に追記して`:PlugInstall`を実行。

```vimscript:init.vim
call plug#begin('~/.vim/plugged')
" ... other plugins
Plug 'prettier/vim-prettier', { 'do': 'yarn install' }
" ... other plugins
call plug#end()
```

`:Prettier`というのがフォーマットを行うコマンドで、デフォルトだと`<leader>p`に割り当てられる。`:PrettierAsync`コマンドの方が快適なので、基本こちらを利用するとよさそう。
ちなみにvim-prettierのインストール時にPrettierもインストールされるので、Prettierプラグインが必要ないなら前段の`npm install ...`を省略できる[^vim-prettier]。

[^vim-prettier]: `npm install ...`していれば、vim-prettierに同梱されたPrettierではなく、各プロジェクトのPrettierが優先して利用される。

#### VSCodeからの利用
[prettier-vscode](https://github.com/prettier/prettier-vscode)を使う。まずはExtensionsから`Prettier - Code formatter`を検索してインストールする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540947/e2bb60ea-a8e3-b8ae-47f4-b7c4567457ff.png)

Prettierを利用することを明示するため、必要に応じて*setting.json*に以下を記入しておく。

```setting.json
{
    "editor.defaultFormatter": "esbenp.prettier-vscode"
}
```

ここまでで準備は完了。`CMD + Shift + P`でコマンドパレットを開き、`Format Document`を選択すればフォーマットを実行できる。
ちなみにvim-prettierと同様、prettier-vscodeについても以下のことがいえる。
> ちなみにvim-prettierのインストール時にprettierもインストールされるので、Prettierプラグインが必要ないなら前段の`npm install ...`を省略できる[^vim-prettier]。


## Prettierプラグインの実装（分析SQLを例に）
ここからPrettierプラグインの実装方法を紹介する。Prettierの[チュートリアル](https://prettier.io/docs/en/plugins.html#tutorials)を参考にしているが、さらに簡単な内容に限って記載する。なお、この記事のコードは全て[GitHub](https://github.com/dr666m1/qiita_formatter)にあげている。

### 準備
npmパッケージとして実装していく。まずは以下のコマンドでプロジェクトの初期化と依存パッケージのインストールを行う。

```sh
npm init -y
npm install --save prettier node-sql-parser
```

そして*index.js*という名称で以下のファイルを作成する。これはチュートリアルで紹介された[コード](https://github.com/fvictorio/prettier-plugin-toml/blob/master/src/index.js)を参考にしたもので、テンプレだと思ってほしい。printSQL関数のみ後ほど編集する。

```js:index.js
const { Parser } = require("node-sql-parser");
const parser = new Parser();
const {
  doc: {
    builders: { concat, hardline, group, indent, softline, join, line },
  },
  util,
} = require("prettier");

const languages = [
  {
    extensions: [".sql"],
    name: "sql",
    parsers: ["sql-parse"],
  },
];

const parsers = {
  "sql-parse": {
    parse: (text) => parser.astify(text),
    astFormat: "sql-ast",
  },
};

function printSQL(path, options, print) {
  const node = path.getValue();
  if (Array.isArray(node)) {
    return concat(path.map(print));
  }
  switch (node.type) {
    default:
      return "";
  }
}

const printers = {
  "sql-ast": {
    print: printSQL,
  },
};

module.exports = {
  languages,
  parsers,
  printers,
};
```

### parserの実装
Prettierでフォーマットを行う際、**テキストは一時的に[AST](https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8)（AbstractSyntaxTree、抽象構文木）に変換される。**この役割を担うのがparserである。今回は自前で実装せずに、[node-sql-parser](https://github.com/taozhi8833998/node-sql-parser)を利用する。以下に簡単な使い方と、出力されるASTを示す。

```jsx
const { Parser } = require("node-sql-parser");
const parser = new Parser();
const ast = parser.astify("select c1,c2,c3 from data")
console.log(ast)
/*
{
  with: null,
  type: 'select',
  options: null,
  distinct: null,
  columns: [
    { expr: [Object], as: null },
    { expr: [Object], as: null },
    { expr: [Object], as: null }
  ],
  from: [ { db: null, table: 'data', as: null } ],
  where: null,
  groupby: null,
  having: null,
  orderby: null,
  limit: null,
  for_update: null
}
*/
```

### printerの実装
**ASTをフォーマット済みのコードに変換するのがprinterの役割。**まずは*index.js*内のprintSQL関数を再掲する。ASTに対してこの関数を再帰的に適用していくことになる。今はまだ何も出力しない状態だが、処理の流れをコメントで解説しておく。

```js:index.js
// printSQLのみ再掲
function printSQL(path, options, print) {
  const node = path.getValue(); // pathからASTを抽出
  if (Array.isArray(node)) { // nodeが配列の場合、各要素にprintSQL関数を再帰的に適用
    return concat(path.map(print)); // path.map(print)が再帰的な呼び出しを表す。戻り値が配列だからconcatで結合
  }
  switch (node.type) { // node.typeに応じてフォーマット処理をして返す
    default:
      return "";
  }
}
```

単純なselect文のフォーマットに対応するなら以下のようになる。詳細はコメントで解説しているので見てほしい。難しいのは`path.call(print, "xxx")` `path.map(print, "xxx")`だと思うが、printSQL関数を再帰的に適用する定型文のようなもの。

```js:index.js
// printSQL修正版
function printSQL(path, options, print) {
  const node = path.getValue();
  if (Array.isArray(node)) {
    return concat(path.map(print));
  }
  if (node.expr) { // nodeが {expr: {type: "xxx"}} のような形式に対応
    return path.call(print, "expr"); // nodeからexprプロパティのみ抽出してprintSQLを適用。path.mapと違って戻り値は文字列（配列ではない）
  }
  switch (node.type) {
    case "select": // nodeが {type: "select"} の場合
      return concat([
        "SELECT",
        indent( // indent()の内側はインデントされる
          concat([
            hardline, // "SELECT"直後の改行
            join(concat([hardline, ","]), path.map(print, "columns")), // nodeからcolumnsプロパティを抽出しprintSQLを適用。各要素は改行とカンマで結合
          ])
        ),
        hardline, // 改行
        "FROM",
        indent(concat(node.from.map((x) => concat([hardline, x.table])))),
      ]);
    case "column_ref": // nodeが {type: "column_ref"} の場合
      return node.column;
    default:
      return "";
  }
}
```

まだSELCT句とFROM句のみの単純なSQLしか処理できないが、これでいったん動く。ここまで理解できたら、あとはASTの構造をよく見て処理を充実させるだけ！

### 動作確認
実装の解説はここまでにして、最後に動作確認を行う。プロジェクトのルートディレクトリ（*package.json*があるところ）で以下のコードを実行すると、きれいに改行・インデントされ、予約語を大文字に変換したSQLが表示されるはず。

```check.js
const prettier = require("prettier");
const format = (code) => {
  const res = prettier.format(code, {
    parser: "sql-parse",
    plugins: ["."],
  });
  return res;
};
console.log(format("select c1,c2,c3 from data"));
/*
SELECT
  c1
  ,c2
  ,c3
FROM
  data
*/
```

### 公開
`npm publish`でアップロードしたら、既存のPrettierプラグインと同じように利用できる。Prettierプラグインとして認識されるには、パッケージの名称が`@prettier/plugin-` `prettier-plugin-` `@<scope>/prettier-plugin-`のいずれかで始まる必要があるので、*package.json*を適切に編集しておくこと。


## 最後に
この記事ではSELECT句とFROM句のみのSQLに対応したフォーマッタを作成した。もちろんこれでは業務に使えないので、**より実用的なSQLフォーマッタを現在作成中**（主にBigQuery用を想定、GitHubは[こちら](https://github.com/dr666m1/prettier-plugin-bq)）。進捗としてはある程度複雑なSQLにも対応しつつあるが、node-sql-parserがBigQuery独自の文法には弱かったり、コメントアウトを消してしまったりと実は課題も多い状況にある。parserから再検討するので時間はかかりそうだが、せっかくなので最後まで頑張ってみる（**LGTMとかで応援してくれたらもっと頑張る**）