Flymake for Javascript

Emacs の Flymake 機能を Javascript 向けに使う.
Javascript checker としては jslint と jshint が有名だが,jslint は必要以上に厳しいということなので jshint を使う.

手順

準備

Emacs から Javascript を使うために nodejs と npm をインストールする.インストール方法は他の記事に詳しく書かれているので割愛.

次に npm から jshint をインストールする.

npm install jshint
jshint を呼び出す Javascript ファイルを作成

以下の Javascript ファイル javascript_flycheck.js を Emacs から参照できるところに実行権限付きで置く.

#!/usr/bin/env node

var boolOptions = {
  asi         : 'automatic semicolon insertion should be tolerated',
  bitwise     : 'bitwise operators should not be allowed',
  boss        : 'advanced usage of assignments should be allowed',
  browser     : 'the standard browser globals should be predefined',
  couch       : 'CouchDB globals should be predefined',
  curly       : 'curly braces around blocks should be required (even in if/for/while)',
  debug       : 'debugger statements should be allowed',
  devel       : 'logging globals should be predefined (console, alert, etc.)',
  dojo        : 'Dojo Toolkit globals should be predefined',
  eqeqeq      : '=== should be required',
  eqnull      : '== null comparisons should be tolerated',
  es5         : 'ES5 syntax should be allowed',
  evil        : 'eval should be allowed',
  expr        : 'ExpressionStatement should be allowed as Programs',
  forin       : 'for in statements must filter',
  globalstrict: 'global "use strict"; should be allowed (also enables \'strict\')',
  immed       : 'immediate invocations must be wrapped in parens',
  jquery      : 'jQuery globals should be predefined',
  latedef     : 'the use before definition should not be tolerated',
  laxbreak    : 'line breaks should not be checked',
  loopfunc    : 'functions should be allowed to be defined within loops',
  mootools    : 'MooTools globals should be predefined',
  newcap      : 'constructor names must be capitalized',
  noarg       : 'arguments.caller and arguments.callee should be disallowed',
  node        : 'the Node.js environment globals should be predefined',
  noempty     : 'empty blocks should be disallowed',
  nonew       : 'using `new` for side-effects should be disallowed',
  nomen       : 'names should be checked',
  onevar      : 'only one var statement per function should be allowed',
  passfail    : 'the scan should stop on first error',
  plusplus    : 'increment/decrement should not be allowed',
  prototypejs : 'Prototype and Scriptaculous globals should be predefined',
  regexdash   : 'unescaped last dash (-) inside brackets should be tolerated',
  regexp      : 'the . should not be allowed in regexp literals',
  rhino       : 'the Rhino environment globals should be predefined',
  undef       : 'variables should be declared before used',
  scripturl   : 'script-targeted URLs should be tolerated',
  shadow      : 'variable shadowing should be tolerated',
  strict      : 'require the "use strict"; pragma',
  sub         : 'all forms of subscript notation are tolerated',
  supernew    : '`new function () { ... };` and `new Object;` should be tolerated',
  trailing    : 'trailing whitespace rules apply',
  white       : 'strict whitespace rules apply',
  wsh         : 'if the Windows Scripting Host environment globals should be predefined'
};

function getOptions(argv) {
  var options = {};
  for (var opt in boolOptions) {
    if (argv[opt]) options[opt] = true;
  }
  return options;
}

function getPredefinedVariables(argv) {
  var predefs = argv.predef == null ? [] : argv.predef;
  if (!Array.isArray(predefs)) predefs = [predefs];

  predefs.forEach(function (predef) {
    if (typeof predef !== 'string') {
      console.log('"predef" option must be given a predefined variable name');
      process.exit(1);
    }
  });

  return predefs;
}

function getSource(argv, callback) {
  var fs = require('fs'),
      path = require('path');

  var filename = argv._[0];
  var sourceStreamer;
  if (filename && path.existsSync(filename)) {
    sourceStreamer = fs.createReadStream(filename, { encoding: 'utf8' });
  }
  else {
    sourceStreamer = process.stdin;
    process.stdin.resume();
  }

  var source = '';
  sourceStreamer
    .on('data', function(str) {
      source += str;
    })
    .on('end', function() {
      callback(source, filename);
    });
}

function jshintyfy(source, filename, options, predefs) {
  filename = filename || '<stdin>';
  options.predef = predefs;

  var JSHINT = require('jshint').JSHINT;
  if (JSHINT(source, options, predefs)) return;

  var errors = JSHINT.errors;
  errors.forEach(function (error) {
    if (!error) return;

    console.log(
      filename + ':' +
      error.line + ':' +
      error.character + ':' +
      error.reason
    );
  });
}

function main() {
  var optimist =
    require('optimist')
    .boolean(Object.keys(boolOptions))
    .describe(boolOptions)
    .string('predef')
    .describe('predef', 'specify predefined variable name')
    .boolean('help')
    .describe('help', 'display this information');

  var argv = optimist.argv;
  if (argv.help) {
    optimist.showHelp();
    process.exit(0);
  }

  var options = getOptions(argv),
      predefs = getPredefinedVariables(argv);

  getSource(argv, function(source, filename) {
    jshintyfy(source, filename, options, predefs);
  });
}

main();
javascript_flycheck.js を使って flymake

以下の elisp を評価する.
"~/.emacs.d/script/flymake/javascript_flycheck.js" は環境依存なので,javascript_flycheck.js を置いた場所によって適時変更.

(defun flymake-javascript-init ()
  (let* ((temp-file (flymake-init-create-temp-buffer-copy
                     'flymake-create-temp-inplace)))
    (list (expand-file-name "~/.emacs.d/script/flymake/javascript_flycheck.js")
          (append (mapcar (lambda (opt) (format "--%s" opt))
                           flymake-javascript-bool-options)
                   (mapcar (lambda (var) (format "--predef=%s" var))
                           flymake-javascript-predefined-variables)
                   (list temp-file)))))

(defvar flymake-javascript-bool-options ())
(defvar flymake-javascript-predefined-variables ())

(add-to-list 'flymake-allowed-file-name-masks
             '("\\.js$" flymake-javascript-init))

(add-to-list 'flymake-err-line-patterns
             '("\\(.+\\.js\\):\\([[:digit:]]+\\):\\([[:digit:]]+*\\):\\(.+\\)$" 1 2 3 4))
設定

js-mode-hook などを使って flymake-mode を有効にすれば OK.
jshint のオプションを使ったり,グローバル変数を前もって定義する場合は,flymake-javascript-bool-options や flymake-javascript-predefined-variables にオプションや変数名を追加する.
オプションは javascript_flycheck.js --help で見れる.

他の Javascript 向け flymake

jshint-mode というのがあったが,jshint を使って Javascript をチェックするローカルサーバを立ち上げ,Emacs からはそのサーバにリクエストを投げるという構成になっている.これは http proxy が設定されていると利用できなかったため,ローカルで使える flymake を書いた.

改良Flymake for OCaml

問題

Eclipse などの統合開発環境には構文エラーがあると、その場でエラー箇所やメッセージを表示してくれる機能がある。
Emacs で同じ機能を実現するには、Flymake を使用するのが一番手っ取り早い。
詳しくは EmacsWiki の [Flymake の項を参考に。

しかし Flymake は列番号の出力に対応していないので、OCaml のような関数型言語で型エラーが発生すると、どの部分でエラーが起きているのか非常にわかりにくい。

対策

そこで、Flymake OCaml を改良して列番号をメッセージとして表示するようにしてみた。
すると、エラーと表示メッセージは次のようになる。

  • エラー

  • メッセージ

変更した ocaml_flycheck.pl は次の通り。

#!/usr/bin/env perl
# ocaml_flycheck.pl

use strict;
use warnings;

### Please rewrite the following 2 variables 
### ($ocamlc, @ocamlc_options)

my $ocamlc = 'ocamlc';          # where is ocamlc
my @ocamlc_options  = ('-c -thread unix.cma threads.cma graphics.cma'); # e.g. ('-fglasgow-exts');
my @ocamlc_packages = ();

### the following should not been edited ###

use File::Temp qw /tempfile tempdir/;
File::Temp->safe_level( File::Temp::HIGH );

my ($source, $base_dir) = @ARGV;

my @command = ($ocamlc);

while (@ocamlc_options) {
  push(@command, shift @ocamlc_options);
}

push (@command,    $source);

while (@ocamlc_packages) {
  push(@command, '-package');
  push(@command, shift @ocamlc_packages);
}

my $dir = tempdir( CLEANUP => 1 );
my ($fh, $filename) = tempfile( DIR => $dir );

system("@command >$filename 2>&1");

open(MESSAGE, $filename);
my $column = "";
while (<MESSAGE>) {
  # example message  {File "robocupenv.ml", line 133, characters 6-10:
  if (/^File "(\S+\.ml[yilp]?)", line (\d+), characters (\d+)-(\d+):\s?(.*)/) {
    print $column;
    my $error = (<MESSAGE>);       # get the next line
    chomp $error;
    print "\n"; 
    print "$1:$2:$3:";
    $column = " [$3-$4]";
    if ($error =~ /Warning(.*)/) {
	    print "$error";
    } else {
	    print "$error ";
    }
    next;
  }
  if (/\s+(.*)/) {
    my $rest = $1;
    chomp $rest;
    print $rest;
    print " ";
    next;
  }
}

close($fh);
print "$column\n";

ちなみに列番号の出力を最後にしてるのは、Flymake の仕様上

  1. 列番号の出力フォーマットが用意されていない
  2. メッセージの先頭が Warning であれば警告、そうでなければエラーと判別するためメッセージの先頭部分は変更できない

という理由による。

多相関数と部分型と value restriction

http://d.hatena.ne.jp/eagletmt/20100716 を見ての感想ですが特に意味のあることを書いているわけではありません.
本来はコメント欄に書くべきことなのかもしれないですが,せっかく開設したのでこちらに書きます.
あと別に疑問に答えるとかそんな大層なもんではないです.すいません.

reference と subtyping

整数の型 Int と数字の型 Number の間には

Int <: Number

という部分型関係が成り立っているとすると,このとき

Int ref <: Number ref

という関係は成り立たない.逆に

ref Number <: ref Int

という関係も成り立たない.
ref Int の値を ref Number の場所で使うことを考えればすぐわかることだけど,
実は ref Int は ref Int 自身とだけ部分型関係を持つ.つまり ref Int に関して成り立つ部分型関係は

ref Int <: ref Int

だけということになる.
もちろんこれは Int と Number に限った話ではなく,

A <: B

という部分型関係にある任意の型でも成り立つ話.

ここら辺の情報は TaPL の Subtyping の章が詳しかったはず.

reference と polymorphic function

もちろん関数にも部分型のための規則はある.
が,多相関数(例えば id)とそれをインスタンス化した後の関数(例えば succ)に対しては部分型関係(のようなもの)が成り立つんじゃないかなあというのが今回思ったこと.
まあそのこと自体はインスタンス関係を表す記号 ≦ にもよく表われているし,単相関数を多相関数を(単相関数へインスタンス化できれば)置き換えるできることからもすぐわかると思う.

多相関数にも(本来の部分型関係とは違う)部分型ちっくな関係が成り立つのなら,それは reference に適用できそう.
例えば

let f x = x;;       (* f : ∀a.a->a *)
let r = ref f;;     (* r : ∀a.a->a ref *)
r := (+);;          (* ∀a.a->a ref ≠ int->int ref なのでエラー *)
r := (fun x -> x);; (* reference は参照している値の型とだけ部分型関係が成り立つので OK *)
let g = !r;;        (* g : ∀a.a->a *)

この制限を使うと,value restriction は必要なくなる(と思う).

しかし,この場合リストのような多相的な type constructor を使うときに問題が起こる.

let l1 = ref [];;   (* ∀a.[a] ref *)
l1 := [1];;         (* エラー.l1 は ∀a.[a] という型の値しか代入できない *)
let l2 = 1::!l1;;   (* これは OK *)

特に2行目みたいな使い方ができないというのは参照を使う上ではけっこう大きな制限じゃないだろうか.
なので,ref で残った型変数は一般化せずに単相の型としておくという value restriction はけっこう妥当な解決策なのかなと思う.

# value restriction 近辺の議論は一昔前にけっこうあったそうなので,論文を調べれば色々出てきて面白そうだなーとか思ったり.
# 確か TaPL の Type Reconstruction の章に参考文献が載っていたような...


追記:
例が不適切だったのを修正