なぜこのツールが必要になったのか?
レガシーJS を prettier、eslint、tsc を使ってリファクタリングしていたときのことです。テストコードなし、E2Eテストは一部だけです。
prettier をかけると、インデントの他にもかなりの量の diff ができました。細かく見ていくのは骨が折れそう。prettierはフォーマッターだから動きは変えないはず。えいやっでマージしようとも思いましたが、これまでの数々のやってもうたが頭をよぎり、踏みとどまりました。
そこでリファクタ前後の js の AST(抽象構文木)のハッシュ値を比較することにしました。
AST(抽象構文木)とは
ソースコードの見た目が違っても、ASTが同じなら、同じプログラムです。不具合も含めて、稼働中のプログラムと同じだから、diffが大きくても安心してリリースしてできるよ、という技術的な根拠です。

ASTから除外するもの
prompt.md は用意しませんでしたが、ブラウザ版Gemini に初期バージョンを実装してもらい、標準入力バージョンに改造。
実際に使いながら、Geminiに相談して、ASTをJSON化する前にキーをソートしたり、ASTから削除する項目を調整しました。
ミニファイのハッシュ値はボツ
ミニファイしてハッシュ値を比較する案もありましたが、引用符の種類(' か " か)やセミコロンの有無にハッシュ値が左右されてしまいそうです。
JSのAST(抽象構文木)を試してみると、引用符の種類(' か " か)やセミコロンの有無が違っても、ASTは同じでした。
他にもミニファイの「変数名の圧縮」「糖衣構文を使った最適化」は、決定論的かどうかがわかりませんでした。ここをつきつめて調べるよりも、ASTを使うことにしました。
パイプでつなげて sha1sum に送る?
最後まで迷ったのが、js内でsha1計算するのか、パイプでつなげて sha1sum に送るか、です。
1. js内でsha1計算する
cat xxx.js | npx ast-logic-hasherCode language: Bash (bash)
メリット:
sha1sumを打つ手間がなく、使い勝手がいい
2. パイプでつなげて sha1sum に送る。このツールでは AST JSON を吐き出すだけにします。
cat xxx.js | npx ast-logic-hasher | sha1sumCode language: Bash (bash)
メリット:
「Unixの哲学(一つのことを、うまくやる)」に沿っている。
AST JSON が欲しいという要望を拾える
今回のツールで AST JSON を構築するとき、ASTオブジェクトのキーをソートしたり、コメントを無視するためにいくつかのキーを削除しています。仮に AST JSON が欲しいという要望があったとしても、その要望に合っていないかもしれません。
Geminiは、CLIオプションで切り替える方法を提案してくれましたが、今そこまでやる必要はないと判断し、js内でsha1計算することにしました。
工夫したこと
diff の原因ごとに、PRを分けました。
- インデント、空行
- 関数やブロック開始の波カッコの位置
- 行末セミコロンの有無
- 文字列の引用符をシングルクォートからダブルクォートへ揃える
- js内のオブジェクトリテラルのキーのクォートが削除される
修正前後で ASTが同じなら、動作確認なしで。ASTが違うときは、同じ種類の原因だけに揃えてから、かんたんなページ表示できることを確認しました。
tscを使いながら、JSDocのキャストを追加するときは、JSDoc追加の前後でASTが一致することを確認して、純粋に型注釈だけを足したことを確認しました。なぜなら、レガシーJSを見ているとうっかりリファクタしていることがあります。意図しない修正が紛れ込まないように注意しました。
学習の成果
プロンプトをガチガチに固めなくても、ブラウザ版Geminiと対話しながら、自分の欲しい『ちょうどいいツール』を爆速で作ることができたこと。
また「ミニファイ vs AST」「ツール内でsha1算出する vs sha1sumを使う」についても自分の考えを案1、案2と伝えて、Geminiの意見を聞き、自分が決める。その結果、いい塩梅のツールになったと思います。
