2013/08/20

JavaScriptのSource Mapの内部表現について

CoffeeScriptなどの別言語からコンパイルしたり、ClosureコンパイラなどでMinifyしたりしたソースをデバッグしているときなどに、生成されたJavaScriptソースコードから変換前のオリジナルソースの場所を知りたいときがあります。

それを知るための技術がSource Mapです。これがどのようにオリジナルのソースを参照しているのか気になったので調べてみました。

CoffeeScriptをコンパイルしたときのソースマップ

簡単な例として、フィボナッチのCoffeeScript版をソースとして用います (fibonacci.coffee)。

fib = (n) -> 
    if n == 0 or n == 1
        n
    else
        (fib n - 1) + (fib n - 2)

console.log fib 10

これを-cmでマップ付きでコンパイルした結果のfibonacci.jsが次になります。

// Generated by CoffeeScript 1.6.3
(function() {
  var fib;

  fib = function(n) {
    if (n === 0 || n === 1) {
      return n;
    } else {
      return (fib(n - 1)) + (fib(n - 2));
    }
  };

  console.log(fib(10));

}).call(this);

/*
//@ sourceMappingURL=fibonacci.map
*/

そのソースマップfibonacci.mapは次のようになりました。

{
  "version": 3,
  "file": "fibonacci.js",
  "sourceRoot": "",
  "sources": [
    "fibonacci.coffee"
  ],
  "names": [],
  "mappings": ";AAAA;CAAA,EAAA,GAAA;;CAAA,CAAA,CAAA,MAAO;CACH,GAAA,CAAQ;CAAR,YACI;MADJ;CAGK,EAAA,UAAD;MAJF;CAAN,EAAM;;CAAN,CAMA,CAAA,IAAO;CANP"
}

mappingの部分がそのマッピング情報データで、「生成コード → 元ソース」の対応情報になっています。

このデータは、生成ファイルの各行ごとに ; で区切られ、それぞれの行の各セグメントごとに , で区切られたデータとなっています。

そこで、生成ファイルとマッピングデータとの対応をわかりやすく行ごとに示したら、次のようになります。

行によっては、セグメントがなかったり、複数あったりすることがわかります。

セグメント情報

次はそれぞれのセグメントが、どのように元ソースを示しているのかを説明します。

まず、各セグメントはBase64でエンコードされていますので、これをデコードします。例えば、AAAAというセグメントなら、0, 0, 0, 0という4つの数値が得られます。

この4つの値はそれぞれ次のように意味を持っています。

ひとつめの値は生成コードの列を示します。上で示したように、セグメントは生成コードのどの行を示しているかは判るので、あとは列がわかればよいというわけです。

ふたつめ以降が元ソースの情報になります。ふたつめはソースIDで、ソースマップファイルの配列sourcesの添字を指定します。みっつめとよっつめでそれぞれソースの行と列を指定します。

というわけで、先ほどの例に戻ると、2行目のAAAAによって1行目1列目を指定していることになります。

また別の例として、5行目最後のMAAOを見てみましょう。

これはBase64では12, 0, 0, 14となります。ただし実際には、これをそのまま利用するのではなくて、次のような変換を行います。

12を2進数表現した001100において、最上位ビットと最下位ビットは特別な意味を持ちます。

最上位ビットはcontinuation bitですが、0なのでここではあまり気にしなくてよいのでよいです。

最下上位ビットはsign bitで、符号を示します (0なら + 、1なら −)。

残りのビット群はそのまま数値として利用します。

というわけで、最終的に得られる数値は6, 0, 0, 7という4つの数値が得られます。

これに基づいてマッピング位置を決めるわけですが、実はセグメント情報の数値はどれも現在値からの相対値になっています (だからsign bitで負の値が取れるようになっている)。

なお、ひとつめの生成コードの列は行ごとにクリアされて1列目に初期化されますが、ソースIDは初期値0、元ソースの行と列はどちらも初期値1です。

というわけで、先ほどの例に戻ると、5行目のMAAOより前では後ろ3文字がAAAのみなので、元ソースの添字、行、列のどれも変化していないことがわかります。

5行目について言えば、CAAAが3つあるので生成コードの列が1つずつ右に行くのを3回行ってから、MAAOを適用するので次のようなマッピングになります。

なんでこういうマッピングをしているのかはわかりません。CoffeeScriptのマッピングはちょっとおかしい気がします。

セグメント情報 詳細

セグメント情報の別のデコード例としてQAA2BHを見てみましょう。

continuation bitは可変長数値表現 (Variable-length quantity; VLQ)であり、大きい値を指定したい場合に利用します。

なお、デコードについてはソースコードを見たほうが速いです。

また、5つめの値はこのセグメントと関連のある名前 (識別子など) を示すもので、ソースマップファイルの配列nameの添字を指定します。

jQueryをminifyしたときのソースマップ

長くなったので、jQueryをminifyしたときのソースマップのマッピングを図示したものを示して終わりにします。

jQuery–2.0.3.min.map

{
  "version":3,
  "file":"jquery-2.0.3.min.js",
  "sources":["jquery-2.0.3.js"],
  "names":["window", "undefined", "rootjQuery", "readyList", "core_strundefined", "location", "document", ... , "jQuery", "_$", "$", ... ],
  "mappings": ";;;CAaA,SAAWA,EAAQC,WAOnB,GAECC,GAGAC,EAIAC,QAA2BH,WAG3BI,EAAWL,EAAOK,SAClBC,EAAWN,EAAOM,SAClBC,EAAUD,EAASE,gBAGnBC,..."
}

関連リンク

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。