JavaScriptからグローバル変数を抽出し、レガシーなコードに立ち向かう

こんにちは、Misoca開発チームのmzpです。ゴールデン・ウィークは北海道で過していました。

最近、JavaScript関連の技術がどんどんでてきてますね。

それはそれとして、数年前から続いているコードベースだと、グローバル変数を利用していたりjQueryを直接利用していたりといった箇所がいくつか残っています。 Misocaでも2〜3年前に書かれたJavaScriptが不用意にグローバル変数を利用していて、メンテナンスが難しい状態になっていました。

少し前にそういったJavaScriptをからグローバル変数を除去し、メンテナンス性を向上させたので、今回はそのときの話を紹介したいと思います。

f:id:mzp:20150514124156p:plain

手法の選定

グローバル変数を抽出するには主に2通りの方法が考えられます。

  • 実際にJavaScriptを実行しその前後でwindowオブジェクトに増えたプロパティを調べる。 minify等でコードが変形されている場合でも検出できるが、十分なテストケースがないと検出漏れが発生してしてしまう。
  • JavaScriptのコードを解析し、そこからグローバル変数を抽出する。テストケースなしでも検出できるが、minify等でコードが変形されている場合、検出漏れ、誤検出が発生する可能性がある。

今回は、画面上の要素と強く結びついていて単体で実行が困難なJavaScriptが多数あることや、対象のコードがminifyされていないことなどから、後者の手法を採用しました。

なお前者の手法は動的解析、後者の手法は静的解析と呼ばれています。

グローバル変数の抽出

Sapidのビルド

今回は解析器として Sapidを利用します。

Spaidはソフトウェア解析器を作るためのプラットファームであり、様々な言語のソースコードを解析し、シンタックスツリーやデターフローなどを得ることができます。 私が把握している範囲では、以下の言語に対応しています。

JavaScriptの解析

以下のコマンドで、JavaScriptを解析できます。

java -cp /usr/local/Sapid/class/sapid.jar org.sapid.model.MkJSXModel misoca.js

すると、 ./SDB/JSX-model/misoca.js.xml として以下の内容のファイルが生成されます。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE File SYSTEM "JSX-model.dtd">
<File id="0" modelName="JSX-model" modelMajorVersion="0" modelMinorVersion="8"><comment sort="Single" pos="1 1">// Generated by CoffeeScript 1.9.0</comment><nl line="1" coffset="35" offset="35">
</nl><Stmt id="1" sort="Expr"><Expr id="133" sort="FunCall"><Expr id="131" sort="Dot"><Expr id="2" sort="Paren"><op pos="2 1">(</op><Expr id="3" sort="FunDec"><FunDec id="4" sort="Anonymous"><kw pos="2 2">function</kw><op pos="2 10">(</op><op pos="2 11">)</op><sp pos="2 12"> </sp><Stmt id="5" sort="Block"><op pos="2 13">{</op><nl line="2" coffset="14" offset="14">
</nl><sp pos="3 1">  </sp><Stmt id="6" sort="Expr"><Expr id="9" sort="FunCall"><Expr id="7" sort="VarRef" read="true"><ident id="8" refid="8" nameId="8" pos="3 3">$</ident></Expr><op pos="3 4">(</op><Expr id="10" sort="Argument"><Expr id="11" sort="FunDec"><FunDec id="12" sort="Anonymous"><kw pos="3 5">function</kw><op pos="3 13">(</op><op pos="3 14">)</op><sp pos="3 15"> </sp><Stmt id="13" sort="Block"><op pos="3 16">{</op><nl line="3" coffset="17" offset="17">
.....

ちょっと読むのが辛いですが、これは misoca.js構文木(≠抽象構文木)がどのようになっているかをXMLアノテーションしたファイルになっています。

例えば $ という変数への参照は、以下のようになります。

<Expr id="7" sort="VarRef" read="true">
  <ident id="8" refid="8" nameId="8" pos="3 3">$</ident>
</Expr>

グローバル関数の抽出

JavaScriptが解析されXMLになったので、あとはグローバル変数を宣言している箇所を抽出します。

今回はQiita:teamに結果を貼りたかったので、Markdown形式で出力しています。

require 'rexml/document'

ARGV.each do |path|
  results = []

  doc = REXML::Document.new File.open(path)
  # window.xxx = .... を抽出する
  doc.elements.each('//Expr[@sort="Assign"]') do |assign|
    elem = assign.get_elements('./Expr[@sort="Dot"]/Expr[@sort="VarRef"]/ident').first
    if elem
      if elem.text  == 'window'
        t = assign.get_elements('./Expr[@sort="Dot"]/ident').first
        results << t.text if t
      end
    end
  end

  # トップレベルの関数宣言を抽出する
  doc.elements.each('/File/FunDec') do |decl|
    idents = decl.get_elements('./ident')
    results << idents.first.text
  end

  # トップレベルの変数代入を抽出する
  doc.elements.each('/File/VarDec/Expr[@sort="Assign"]') do |decl|
    idents = decl.get_elements('./ident')
    results << idents.first.text
  end

  unless results.empty?
    puts "## #{path}"
    puts ""
    puts results.map{|x| " * #{x}"}.join("\n")
  end
end

実行例

以下のようなJavaScriptコードを解析してみます。

// トップレベルの変数代入
var x = 1;

// 関数宣言
function f() {
}

(function() {
  // window.xxxx への代入
  window.y = 2;

  // ローカル変数
  var z = 3;
})()

実行してみます。

$ java -cp /usr/local/Sapid/class/sapid.jar org.sapid.model.MkJSXModel foo.js

$ ruby extract-global.rb SDB/JSX-model/foo.js.xml
## SDB/JSX-model/foo.js.xml

 * y
 * f
 * x

最後に

汎用の解析ツールを作成するのは大変ですが、特定の目的・コードベースに特化した解析器を作るのはそれほど大変ではありません。また、そういったツールは、既存のコードを改善していく際に強力なツールとなります。

本記事が、みなさまの既存コードを改善する際の参考になれば幸いです。