こんにちは、Misoca開発チームのmzpです。ゴールデン・ウィークは北海道で過していました。
最近、JavaScript関連の技術がどんどんでてきてますね。
それはそれとして、数年前から続いているコードベースだと、グローバル変数を利用していたりjQueryを直接利用していたりといった箇所がいくつか残っています。 Misocaでも2〜3年前に書かれたJavaScriptが不用意にグローバル変数を利用していて、メンテナンスが難しい状態になっていました。
少し前にそういったJavaScriptをからグローバル変数を除去し、メンテナンス性を向上させたので、今回はそのときの話を紹介したいと思います。
手法の選定
グローバル変数を抽出するには主に2通りの方法が考えられます。
- 実際にJavaScriptを実行しその前後でwindowオブジェクトに増えたプロパティを調べる。 minify等でコードが変形されている場合でも検出できるが、十分なテストケースがないと検出漏れが発生してしてしまう。
- JavaScriptのコードを解析し、そこからグローバル変数を抽出する。テストケースなしでも検出できるが、minify等でコードが変形されている場合、検出漏れ、誤検出が発生する可能性がある。
今回は、画面上の要素と強く結びついていて単体で実行が困難なJavaScriptが多数あることや、対象のコードがminifyされていないことなどから、後者の手法を採用しました。
なお前者の手法は動的解析、後者の手法は静的解析と呼ばれています。
グローバル変数の抽出
Sapidのビルド
今回は解析器として Sapidを利用します。
Spaidはソフトウェア解析器を作るためのプラットファームであり、様々な言語のソースコードを解析し、シンタックスツリーやデターフローなどを得ることができます。 私が把握している範囲では、以下の言語に対応しています。
- Java7
- C
- JavaScript
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
最後に
汎用の解析ツールを作成するのは大変ですが、特定の目的・コードベースに特化した解析器を作るのはそれほど大変ではありません。また、そういったツールは、既存のコードを改善していく際に強力なツールとなります。
本記事が、みなさまの既存コードを改善する際の参考になれば幸いです。