今から嫌がらせをします
こんにちは。どんぶラッコ(Twitter: @don_bu_rakko)です。
突然ですが、今から関数名を書き換える嫌がらせをします。
嫌がらせの手順
1. 関数が記述されているJSファイルを用意します
嫌がらせの対象となるJSファイルを作成します。
ここでは、関数 fuu()
, rin()
, ka()
, zan()
を作りました。風林火山。
2. rakko.js
を実行する
嫌がらせ開始です。
node rakko.js [対象のjsファイルリソース]
を実行します。
3.ファイルが生成されるので開きます
これで嫌がらせ完了です。
dist/
ディレクトリに生成されたJSファイルを開いてみてください。
4.その結果
関数が全部donBuRakko()
に変わってるーーーー!!!
関数名を書き換える嫌がらせプログラム
ということで、関数名を全て donBuRakko()
に変える嫌がらせのプログラムを作ってみました。
これの嫌なところは宣言時の関数名のみを変換するということ。
つまり、武田信玄に塩を送る関数があったとしても
isSendSalt is not defined
こうして敵に塩が送れなくなります。ただただ悪質な嫌がらせプログラムですね。間違っても先輩のPCで実行しないでください。絶対にです。
ところで、このプログラムはBabelのライブラリを使って作成されています。
ということでここからやっと今回のAdvent Calendarのテーマ、お勧めしたいテクニックの紹介に移ります。
Babel Toolingを使い倒す!
Babelとは
みなさん、Babel自体はご存知の方も多いと思います。新しいJavaScriptの構文を現在のWebブラウザでも使えるようにしてくれる、あれです。
例えばアロー関数で書かれたJavaScriptをBabelに通すと
こんな風にしてくれます。
今日の我々はBabelのおかげで、文法のブラウザ対応とかを気にすることなくJavaScriptを記述出来るわけです。超ありがたい。
Babel Tooling
実は、このBabel、公式ページのドキュメントを読んでいると、Toolingという項目でいくつかのツールが提供されているのがわかります。
今回はその中でも parser
, genertor
, traverse
を取り上げてみます。
Babel トランスパイルの仕組み
Babelがプログラムを組み替えることをトランスパイルなんて言ったりしますが、その仕組みはこうです。
- 形態素解析(Parse)
- 記述されているプログラムがどんな要素で構成されているのか、構文木(AST)の形式で解析します。
- 置き換え(Traverse)
- 置き換えのルールを作ることが出来ます。先ほどの
donBuRakko()
置き換え機能はtraverseの機能を使っています。
- 置き換えのルールを作ることが出来ます。先ほどの
- コード生成(Generate)
- 一度形態素解析でバラバラにしたコードを再度生成します。
そして、先ほどのparse
, traverse
, generate
のツールを使うことでそれぞれを実装することが出来ます。ToolingはBabelの機能を個別に切り出して提供してくれている機能、というわけです。
実装
では、先ほどの関数名変更嫌がらせプログラムを例に、実装の方法をみていきましょう。
ここからは細かい話になるので、最初は流し読みでいいと思います。興味を持ったら読んでください。
下記のリポジトリにも格納してあります。
https://github.com/cha1ra/dbk_babel_ex
まずは、npm init
して必要なライブラリをインストールします。
$ npm init
$ npm install --save @babel/parser @babel/traverse @babel/generator
次にrakko.js
を作ります。今回はsrc/
というディレクトリを作成し、その中に格納してあります。
// ライブラリの読み込み
const parse = require('@babel/parser').parse
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const fs = require('fs')
const path = require('path')
console.log('どんぶラッコに変換!')
// 引数・ファイルの読み込み
const arg = process.argv[2]
const ext = path.extname(arg)
const filePath = path.join(__dirname, arg)
const code = fs.readFileSync(filePath, { encoding: 'utf-8' })
// parse
const ast = parse(code)
// traverse
traverse(ast, {
FunctionDeclaration: (path) => {
path.node.id.name = 'donBuRakko'
}
})
// generator
const result = generate(ast).code
// ファイルを dist/ ディレクトリに書き出す
fs.writeFileSync(
path.join(
'../dist',
`${path.basename(arg, ext)}_dbk_${Date.now()}${ext}`
),
result
)
console.log('完了!')
冒頭、末尾の部分はファイルの読み書きやコマンドの引数を読み込んでいるパートなので、実際にBabelを使っている部分はわずか7行です。逆に言うと、たったそれだけの行数で書き換え処理が出来てしまうBabelさんすげえ。
parse
個別に簡単に解説します。まずはparse
の部分。
const parse = require('@babel/parser').parse
const code = fs.readFileSync(filePath, { encoding: 'utf-8' })
...
const ast = parse(code)
codeにはjavascriptの文章がぶち込まれています。これを構文木解析するわけです。
例えば先ほどの上杉謙信塩送りプログラムは、こんな風にバラバラにされます。
Node {
type: 'File',
start: 0,
end: 111,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 7, column: 1 } },
errors: [],
program:
Node {
type: 'Program',
start: 0,
end: 111,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'script',
interpreter: null,
body: [
Node {
type: 'FunctionDeclaration',
start: 0,
end: 55,
loc: SourceLocation { start: [Position], end: [Position] },
id:
Node {
type: 'Identifier',
start: 9,
end: 19,
loc: [SourceLocation],
name: 'isSendSalt' },
generator: false,
async: false,
params: [ [Node] ],
body:
Node {
type: 'BlockStatement',
start: 27,
end: 55,
loc: [SourceLocation],
body: [Array],
directives: [] } },
Node {
type: 'IfStatement',
start: 57,
end: 111,
loc: SourceLocation { start: [Position], end: [Position] },
test:
Node {
type: 'CallExpression',
start: 60,
end: 78,
loc: [SourceLocation],
callee: [Node],
arguments: [Array] },
consequent:
Node {
type: 'BlockStatement',
start: 79,
end: 111,
loc: [SourceLocation],
body: [Array],
directives: [] },
alternate: null
} ],
directives: [] },
comments: [] }
「うわっ」と思うかもしれませんが、ここで注目して欲しいのがtype
の項目です。ここを読むと、'Identifier'
, 'IfStatement'
などの文字が見て取れます。
そう、これはどのような要素でプログラムが構成されているか、タグ付けをしてくれているんです。
関数の宣言部分であれば、'FunctionDeclaration'
とタイプ分けされています。
一つひとつのNodeは要素ごとの情報をまとめた塊になっています。例えばloc
には開始/終了の行と列の情報が入っています。
様々な情報を内包している形で分解するから、また元に戻せるようになっているんですね。
traverse
続いてtraverse
です。
const traverse = require('@babel/traverse').default
...
traverse(ast, {
FunctionDeclaration: (path) => {
path.node.id.name = 'donBuRakko'
}
})
traverse([parseしたJS], {type名: (path) => {処理}})
という形で記述が出来ます。
ここでは、 type が FunctionDeclaration
, つまり関数宣言をしている箇所だけ、名前をdonBuRakko
に変更してください、という指示を出しています。
traverseを使うメリットは再帰的に変換をしてくれる点。いちいち再帰処理を書かなくてもよくなるのです。
generate
そして最後にgenerate
。これはめちゃめちゃ簡単です。
const generate = require('@babel/generator').default
...
const result = generate(ast).code
generate([parseしたJS]).code
でOK。これで元に戻るというわけです。
Babel Tooling の活用法
今のままでは、ただ単に嫌がらせをするだけのアプリです。
と言うことで、この機能を使うと何が良いのか、少し真面目なことを書いて終わりにしようと思います。
デバッグ用関数の一括削除
例えば、デバッグ用に debug()
と言う関数を作成していたとします。
const debug = (str) => {
console.log(`[Debug] ${str}`)
}
let name = '太郎'
debug(name)
name = '二郎'
debug(name)
本番環境に持って行くときは、もちろん必要ないわけです。
そんなときはpath.remove()
を使うことで取り除く処理をすることができます。
traverse(ast, {
ExpressionStatement: (path) => {
const exp = path.node.expression
if ('callee' in exp) {
exp.callee.name === funcName && path.remove()
}
}
})
先ほどのリポジトリにある src/rm_func.js
を実行してみてください。
# node rm_func.js [Filepath] [Function Name]
node rm_func.js ./resource/ex_2.js debug
debug()
が...
消えました!!
命名規則の追加
また、冒頭に紹介した嫌がらせアプリもきちんと応用ができます。
一番最初に説明したのは、関数を無条件にdonBuRakko()
に書き換えるものでしたが、この仕組みを応用すると、
- 戻り値が
boolean
だった場合、関数名はis
から始める
などのルールもチェックさせることができるようになります。
つまり
つまり、オリジナルのLinterが作れるのです!
これを知ったとき、私かなり感動しました。
ESLintなどでは面倒が見れない自分独自のルールを、簡単に構築・設定することができます。
Babelをうまく活用して行くことで、効率化が図れる。そんな可能性を感じてワクワクしています。
ということで、皆さんもぜひBabel Toolingを活用してみてください!!!
.
.
.
はぁ〜あ、腰が疲れたなあ。(チラッ
関連リンク
- 過去にLTした時の資料
- 今回のリポジトリ
明日の担当はikegam1さんです。予定ではWordpress関連のトピックとのこと。楽しみですね!