Stylus/mochaがやってるGlobal leakテストとNode.js Debugger

先日、Stylusのコードをいじってたらグローバルリークがあったので、修正してPullリクエストしました。(TJが即マージしてリリース済みだよ!)

リークを見つけたのは、StylusのテストコードにGlobalオブジェクト汚染を検出するテストがあってそれをたまたま走らせただけなのですが、このテストがシンプルかつ効果的でいいなと思いました。

https://github.com/LearnBoost/stylus/blob/master/test/run.js

// Testの初期化時にデフォルトのグローバルオブジェクトのキーを保存。
var globals = Object.keys(global);
...

// Testが終わったらグローバルオブジェクトの差分をチェック
function done() {
  Object.keys(global).forEach(function(name){
    if (!~globals.indexOf(name)) {
      console.error('  \033[31mglobal leak:\033[0m %s', name);
      ++failures;
    }
  });
  ...


JavaScriptってvarとかnewとか忘れただけでグローバルリークしちゃうので(CoffeeScriptなら以下略)、これテストフレームワークに入れて全メソッドでチェックしたらいんじゃない?と思って、Stylusの作者TJが作ってるテストフレームワークmochaを見てみたら、やっぱりGlobal leakをチェックする機能が入ってました。デフォルトでチェックしてくれるみたいです("―ignore-leaks"で抑制可能)。
使っているテストフレームワークにこの機能がなくても、単純なテストなので、setup()やteardown()に仕込んどくのがいいんじゃないでしょうか。


ちなみに、グローバルリークってだいたいnewかvarの付け忘れだと思うんですが、今回の"lineno"プロパティはいろんなクラスに継承されるNodeというベースクラスのコンストラクタでセットされるプロパティなので、newが漏れてる箇所を単純なgrepでは見つけにくいところでした。なので、

var Node = module.exports = function Node(){
  if (this === global) debugger; // newを忘れてたらここでbreakされる
  this.lineno = nodes.lineno;
  Object.defineProperty(this, 'filename', { writable: true, value: nodes.filename });
};

みたいに条件付きdebuggerを仕込んでテストをdebugで動かしたら一発で見つかりました。

$ node debug test/run.js
< debugger listening on port 5858
connecting... ok
debug> c

<     arithmetic.color
<   &#10004; arithmetic.color
<     arithmetic
<   &#10004; arithmetic
<     arithmetic.unary
break in lib/nodes/node.js:23
 21 
 22 var Node = module.exports = function Node(){
 23   if (this === global) debugger;
 24   this.lineno = nodes.lineno;
 25   Object.defineProperty(this, 'filename', { writable: true, value: nodes.filename });
debug> bt
#0 node.js:23:23
#1 boolean.js:23:8
#2 Boolean.negate boolean.js:78:10
#3 Evaluator.visitUnaryOp evaluator.js:428:31
#4 Visitor.visit index.js:28:39
#5 Evaluator.visit evaluator.js:74:18
#6 Evaluator.visitExpression evaluator.js:451:26
#7 Visitor.visit index.js:28:39
#8 Evaluator.visit evaluator.js:74:18
#9 Evaluator.visitProperty evaluator.js:489:22
debug> o
break in lib/nodes/boolean.js:24
 22 var Boolean = module.exports = function Boolean(val){
 23   Node.call(this);
 24   if (this.nodeName) {
 25     this.val = !!val;
 26   } else {
debug> o
break in lib/nodes/boolean.js:79
  77 Boolean.prototype.negate = function(){
  78   return Boolean(!this.val);   // newが抜けてる!!!
  79 };
  80 
  81 /**
debug> 

gdbライクなNode.jsのDebugger、覚えてるとちょっとしたときに便利ですね。