こんにちは、クソアプリ Advent Calendar 2019の 19 日目は、@sandessOjisanがやっていきます。この記事では 俳句力を鍛えるための babel-plugin 開発 について記事を書きます。早速使いたい方はこちらからどうぞ。なお、季語はありませんが、無季俳句として俳句としてカウントされるようです。
背景
なんでこんなものを作ったかというと、それは自身の俳句を読む力を鍛えたかったからです。
俳句とプログラミング
みなさんの周りにも、ダジャレ・韻、そして 俳句 を言うのがやたら上手い人っていませんか。私の周りにはそういう人がたくさんいます。そして、そういう人に限ってプログラミング能力も非常に高いと感じています。もちろん、「俳句ができるからプログラミングもできる」なのか「プログラミングができるから俳句ができる」のどちらなのかはわかりませんし、そもそも因果関係があることも疑わしいです。しかし、現に私が憧れとしている人たちは、俳句を読める人たちです。日頃から彼らの俳句を聞いていると、「どうにかして私も俳句を読めるような人になりたい」と思うようになりました。
navigator.serviceWorker.register
ところで、navigator.serviceWorker.register
をみて、ピンときませんか?皆さんの中には「あっ、575 だ」って感じた人もいるかもしれません。しかしこれは「ナビゲーター サービスワーカー レジスター」であるため、6/8/5 です。残念でした。ただ、このようにソースコードの中には俳句のようなものが混じっていることがあり、もしかしたらソースコード内で本当に俳句を見つけるかもしれません。
**そう、実は私たちプログラマーは日頃のコーディングにおいて俳句を詠む機会に恵まれていたのです。**そこで、コーディング時に俳句を常に表示してくれるような仕組みがあれば、私たちの脳に俳句が記憶され、俳句を生成する力が刺激されるのではないかと仮説を立てました。そこで私はその仮説を検証すべく、ビルド時に 575 を検知し出力する babel-plugin を開発しました。
babel で 1 句読んでみる
このbabel-plugin-detect-haikuが 1 句を検出するためのプラグインです。このプラグインを導入することで、あなたはビルド時に 1 句を読めるようになります。では早速見てみましょう。
デモ
まず、npm からプラグインを入れてください。
$ yarn add -D babel-plugin-detect-haiku
そして、babel の設定ファイルに利用プラグインとして追記してください。
plugins: ["detect-haiku"]
plugin 指定時は babel-plugin-
をつける必要がないことに注意してください。
この設定とデモ用のサンプルファイルも用意しているので、すぐに試したい方はこちらをお使いください。
では、さっそく 1 句読んでみましょう。webpack 経由で babel でトランスパイルしてみましょう。(実務でも使えることをアピールするためにわざわざ webpack を使っていますが、本来は不要です。)
$ webpack
> document childNodes document
はい、575 が出力されましたね。
ふざけんな勤務時間は仕事しろ
私は素晴らしいプラグインを開発しました。これであなたは業務中に俳句を見つけることができ、自身の俳句ストックを貯めることができます。しかし、もしあなたが開発マネジメントをする立場でもあるならば、このように遊びを助長するような babel プラグインについて腹立たしいと感じるかもしれません。社員が俳句を読もうと色々な書き方を試したりして、仕事が進まない原因にもなる可能性もあります。そこで、誠に不本意ではあるのですが、我が国の生産性のことも考え、俳句を禁止する eslint plugin も作りました。
もし従業員が俳句を読んでもそれを検知し、CI や precommit で弾くことができます。
eslint で俳句を禁止する
それでは eslint-plugin が俳句を禁止するところをみていきましょう。
デモ
eslint-plugin-detect-haikuがその plugin です。早速導入しましょう。
$ yarn add -D eslint eslint-plugin-detect-haiku
次に設定ファイル(eslintrc など)を編集します。
module.exports = {
// 中略
plugins: ["detect-haiku"],
rules: {
"detect-haiku/forbid_haiku": 2
}
};
そして実行します。
$ eslint src/**.tsx
/Users/.../main.tsx
9:9 error 俳句を検知しました. => document childNodes document detect-haiku/forbid_haiku
このように俳句を見つけると、しっかりと警告が出ます。
仕組みの解説
それではこれらのプラグインがどう実装されているかを解説します。
AST プログラミング
babel も eslint もソースコードに対して何らかの処理をするツールです。これらのツールは、直接ソースコードに対して処理をするのではなく、処理をしやすいように ソースコードを AST(抽象構文木)という形式に直してから行います。
例えば、console.log("hello")
は次のような形式で表現されます。(※ parser には acorn を使用, 行数節約の観点で一部省略)
{
"type": "Program",
"body": [
{
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
},
},
"arguments": [
{
"type": "Literal",
"value": "hello",
}
]
}
}
],
}
babel はこの AST に対して ES5 に直す処理(Traverse)を行い、ESLint はルールに反しているものが無いかを探索します。
そして処理が終わると、今度は AST をソースコードに直します(Generate)。処理済みの AST からコードが生成されるので、元あったソースコードがなんらかの変換をされたことを指します。
探索と変換はどのようにして行われるか
babel も eslint も探索は各ノードを進んでいくことで行われます。JSON 表現された AST の各値を探索していっていると考えると良いでしょう。AST には type が割り振られており、例えば、Identifier
というものが振られています。babel や eslint の plugin は、「この type のときはこうこうしてください」と書くことで作れます。例えば Identifier の Node で何か文字を出力して欲しい場合はこのようにします。
module.exports = function() {
return {
name: "plugin-hike-detect",
visitor: {
Identifier(path) {
console.log("Identifier node なう");
}
}
};
};
つまりこういった Node 内での処理として、俳句に使えるかどうかの判定をすれば良さそうです。
関数名を文字数に変換
今回私が作ったプラグインは Identifier ノードの中で、その Node の識別子(関数名など)をカタカナに直し、文字数をカウントしています。
そしてその文字数を足し合わせていき、5, 7, 5 で綺麗に収まるかを判定しています。
const main = (node: TIdentifierType) => {
const { name } = node;
const kana = data[name];
if (!kana) {
// 単語辞書から該当する言葉を見つけられなかったとき
resetHikeObj();
} else {
// 1句目が5文字になるかチェック
if (countHikeWord(HIKE_OBJECT.first.kana) !== 5) {
// 1句目が5文字じゃないときは1句目を操作
if (countHikeWord(HIKE_OBJECT.first.kana) + countHikeWord(kana) === 5) {
// すでに入力された1句目といま入力されたものの合計が5になったかどうかをチェック
setWord(name, kana, "first");
return;
} else {
// 5文字以上になるまで次のNodeに進む.
}
}
// 中略(2, 3句目も同様の処理)
// 2句目が7文字, 3句目が5文字になるかチェック
// 575が達成された時の処理
return resultHike(HIKE_OBJECT);
}
};
上の例は行数節約のために省略しています。完成形はこちらをご覧ください。=> haiku-core
ちょっとした小技
eslint と babel の共通化
今回作った babel-plugin と eslint-plugin は、AST 操作の部分が同じであったため、haiku-core として共通モジュールとして切り出しました。babel と eslint は使う AST パーサーは異なるのですが、たまたまどちらにもある機能しか使っていなかったので、共通化することができました。
ライブラリを分割する
Node.jsの基本哲学として、小さなコア、小さなモジュールという考えがあります。Node.jsはコアとなる部分を小さくしておき、その周りにユーザーが独自で作ったモジュールを足してシステムを作っていくことを推奨しています。そしてそのモジュール一つ一つも小さく作り、その組み合わせで大きなシステムを作り上げていきます。(だからnode_modulesの中には依存モジュールが大量にあるものがあったりする)
そこで今回作った俳句プラグインも機能を小さく分割し、独立して提供できるようにしました。
haiku-core
haiku-coreは、babel-plugin と eslint-plugin を使って俳句を見つけるときの共通処理をまとめた関数です。
babel も eslint もプラグインを作るときは AST のノードに対して処理を書いていくため、書き方がほとんど同じで、俳句の検出ロジックを再利用できました。
導入
$ yarn add haiku-core
const hike = require("haiku-core");
const data = hike(node);
if (data) {
console.log(`俳句を検知しました. => ${data}`);
}
babel-plugin-detect-haiku
babel-plugin-detect-haikuは、ビルド時に俳句を出力する babel-plugin です。プラグインを install 後、babelrc に設定を書くだけで動作します。内部でhaiku-coreが利用されているので、とても軽量な実装です。
導入
babel とプラグインの導入
$ yarn add -D babel babel-plugin-detect-haiku
設定ファイル(babelrc など)を編集
module.exports = {
// 中略
plugins: ["detect-haiku"]
};
動作例や設定例は、 575-detect-plugin-example を参考ください。
eslint-plugin-detect-haiku
eslint-plugin-detect-haiku は俳句を禁止する eslint プラグインです。
内部でhaiku-coreが利用されているので、とても軽量な実装です。
導入
eslint とプラグインの導入
$ yarn add -D eslint eslint-plugin-detect-haiku
設定ファイル(eslintrc など)を編集
module.exports = {
// 中略
plugins: ["detect-haiku"],
rules: {
"detect-haiku/forbid_haiku": 2
}
};
575-detect-plugin-example
575-detect-plugin-exampleは作成した plugin が正常に動作するかを確かめるためのサンプルアプリケーションです。npm 経由で落としてきたプラグインが正常に動作するかを確かめたかったので、独立したアプリケーションとして開発しています。実務を想定し、ES6, React, TSX で開発されています。
babel-plugin の動作を確認
$ yarn build
> document childNodes document
eslint-plugin の動作を確認
$ yarn lint
> 9:9 error 俳句を検知しました. => document childNodes document detect-haiku/forbid_haiku
js-word-count-for-haiku
js-word-count-for-haikuは与えられたカタカナの文字数をカウントします。俳句用なのでャュョをカウントしないといった工夫がされています。この機能を別のライブラリとして切り出すことで、ャュョをカウントするようにしたいといった修正をこのレポジトリに閉じて行えるようになります。
導入
$ yarn add js-word-count-for-haiku
const countWord = require("js-word-count-for-haiku").default;
const c = countWord("コンソール");
console.log(c); // => 5
js-word-kana
js-word-kanaは JS のプログラミングで登場する言葉をカタカナに直したものの一覧です。後述する pubget を使って手で作成していきます。
導入
$ yarn add js-word-kana
const data: { [key: string]: string } = require("js-word-kana").default;
console.log(data);
{ import: 'インポート',
require: 'リクワイアー',
console: 'コンソール',
break: 'ブレイク',
// 中略
setState: 'セットステート',
ref: 'レフ',
useState: 'ユーズステート',
useEffect: 'ユーズエフェクト' }
pubget
pubgetは指定したライブラリの public 関数を取得するライブラリです。カタカナ辞書を作るときの補助ツールです。
導入
このツールを DL
$ git clone https://github.com/sadnessOjisan/pubget
解析対象のライブラリを DL
# it's ok -D option, because we do not use this library to develop application.
$ npm install -D react
解析
# node index.js ${targetLibraryName}
$ node index.js react
出力
{ react: '',
Children: '',
// 中略
useDebugValue: '',
useLayoutEffect: '',
}
haiku-lp
haiku-lpはこの記事で作成した全ライブラリを紹介するページです。ライブラリを分割したはいいものの、どういう関係にあるかという説明をどこかにまとめておきたかったので作りました。Gatsby で開発し、gatsby-starter-grayscaleというスターターをそれっぽく書き換えて作成しました。
https://haiku-plugin.netlify.com/
今後の課題
実はこれらのプラグインは予約語には反応しません。予約語を使うためには babel pluguin としてではなく、AST の parser そのものを使う必要があります。つまり AST を作り、出てきた単語を先頭から配列で保持するといったようなデータの保存方法が必要となります。このバージョンも independent-haiku-detectとして途中まで作っていたのですが、まだ未完成です。全単語を拾って配列で保持することはできますが、それをカタカナに直して、文字数を数え、俳句になりうるかどうかを判定するところまでは作っていません。ただ、色々なロジックを細かいライブラリに分けて作っていたので、この機能もすぐ作れるのではないかと思っています。
ちなみに各単語を取得するためには babel が内部で使っている parser をそれ単体で使うことで実現できます。babel は babelyonという parser を内部で使っており、オプションを指定することで全ノードの name を取得することができます。これによりソースコードで使われた単語を token として全て抽出することができます。どのように token が参照可能かは AST Explorerで作った例をご覧ください。toekns という配列で token を保持しています。
あとがき: plugin を作るということに対して
babel や eslint の自作プラグインの作り方は公式や blog などで多く紹介されていますが、実のところ作る機会がなくてこれまで作ったことはありませんでした。ただ、会社の独自コーディングルールがあってそのチェックをしたいだとか、自分の開発効率を上げるためのコーディング支援ツールを作りたいといった一定のニーズはあります。そのため AST を使ったプログラミングは習得していた方が良さそうと思っています。
しかし一方で、作る機会にはあまり恵まれないのではないでしょうか。なぜなら欲しいと思ったものは大抵のところ公式が用意してくれているので、本当に作るのは独自的な何かに限られてしまいます。そのためなんらかの課題が欲しいところです。この俳句を読むプラグイン自作は、チュートリアル以上の難易度はあるので、挑戦課題としても良いと思います。ぜひ皆さんも自作に挑戦してみてはいかがでしょう。