16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

今年イチ!お勧めしたいテクニック by ゆめみ feat.やめ太郎Advent Calendar 2019

Day 18

"関数名を書き換える"嫌がらせプログラムを作ってみた - Babel Toolingの活用法

Last updated at Posted at 2019-12-17

今から嫌がらせをします

こんにちは。どんぶラッコ(Twitter: @don_bu_rakko)です。
突然ですが、今から関数名を書き換える嫌がらせをします

嫌がらせの手順

1. 関数が記述されているJSファイルを用意します

嫌がらせの対象となるJSファイルを作成します。
ここでは、関数 fuu(), rin(), ka(), zan() を作りました。風林火山。

image.png

2. rakko.jsを実行する

嫌がらせ開始です。
node rakko.js [対象のjsファイルリソース]を実行します。

image.png

3.ファイルが生成されるので開きます

これで嫌がらせ完了です。
dist/ ディレクトリに生成されたJSファイルを開いてみてください。

image.png

4.その結果

image.png

image.png

関数が全部donBuRakko()に変わってるーーーー!!!

関数名を書き換える嫌がらせプログラム

ということで、関数名を全て donBuRakko() に変える嫌がらせのプログラムを作ってみました。
これの嫌なところは宣言時の関数名のみを変換するということ。

つまり、武田信玄に塩を送る関数があったとしても

image.png

でん!
image.png

image.png

isSendSalt is not defined

こうして敵に塩が送れなくなります。ただただ悪質な嫌がらせプログラムですね。間違っても先輩のPCで実行しないでください。絶対にです。

ところで、このプログラムはBabelのライブラリを使って作成されています。
ということでここからやっと今回のAdvent Calendarのテーマ、お勧めしたいテクニックの紹介に移ります。

Babel Toolingを使い倒す!

Babelとは

みなさん、Babel自体はご存知の方も多いと思います。新しいJavaScriptの構文を現在のWebブラウザでも使えるようにしてくれる、あれです。

例えばアロー関数で書かれたJavaScriptをBabelに通すと

image.png

こんな風にしてくれます。
今日の我々はBabelのおかげで、文法のブラウザ対応とかを気にすることなくJavaScriptを記述出来るわけです。超ありがたい。

Babel Tooling

実は、このBabel、公式ページのドキュメントを読んでいると、Toolingという項目でいくつかのツールが提供されているのがわかります。

image.png

今回はその中でも parser, genertor, traverse を取り上げてみます。

image.png

Babel トランスパイルの仕組み

Babelがプログラムを組み替えることをトランスパイルなんて言ったりしますが、その仕組みはこうです。

image.png

  • 形態素解析(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/というディレクトリを作成し、その中に格納してあります。

/src/rakko.js

// ライブラリの読み込み
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の文章がぶち込まれています。これを構文木解析するわけです。
例えば先ほどの上杉謙信塩送りプログラムは、こんな風にバラバラにされます。

ast
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

image.png

debug()が...

image.png

消えました!!

命名規則の追加

また、冒頭に紹介した嫌がらせアプリもきちんと応用ができます。
一番最初に説明したのは、関数を無条件にdonBuRakko()に書き換えるものでしたが、この仕組みを応用すると、

  • 戻り値がbooleanだった場合、関数名はisから始める

などのルールもチェックさせることができるようになります。

つまり

つまり、オリジナルのLinterが作れるのです!

これを知ったとき、私かなり感動しました。

ESLintなどでは面倒が見れない自分独自のルールを、簡単に構築・設定することができます。
Babelをうまく活用して行くことで、効率化が図れる。そんな可能性を感じてワクワクしています。

ということで、皆さんもぜひBabel Toolingを活用してみてください!!!

.
.
.

はぁ〜あ、腰が疲れたなあ。(チラッ

関連リンク


明日の担当はikegam1さんです。予定ではWordpress関連のトピックとのこと。楽しみですね!

16
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?