hitode909の日記

趣味はマリンスポーツですの日記です

統計的なインデントチェッカー

ソースコードを入れると統計的にインデントの変な箇所を見つけて教えてくれる便利ツールを作った.


昨日,統計的にインデントするのを作ったけど,実用には耐えないくらいの精度だった.それの続き.昨日の読んでない人はまずこれを読んでください.


昨日のやつだと,もとのインデントを無視してプログラム全体をフォーマットしようとするので,失敗が許されない.
変なインデントを探して教えてくれるくらいなら,出てきたレポートを人が見て直すかどうか判断できるので,ちょっとましだろうと思った.


あらかじめソースコードをいろいろ与えて,こういうデータを作っておく.

;,s,{"0":96,"-4":1}
;,},{"-4":754,"-8":15,"-15":1,"-2":4,"-9":1,"-12":1,"-19":1}
;,$,{"0":408,"-4":14,"3":2,"-2":1}

昨日は最頻値だけ取っておいたけど,あとで確率も見たくなったので,どれが何回出たかも保存してる.
前の行の最後の文字が;で,次の行がsのとき,変化なしが96回,4文字左に行くのが1回だった,ということを示している.


入力されたソースコードを1行ずつ見ていって,実際のインデントの変化と,あらかじめ集めたデータでのインデントの変化と比べて,違いがあって,かつデータでのインデントの変化の確率が高ければ,インデントを変更しませんかっていう表示を出す.
たとえば,コードを見ていって,前の行の最後の文字が;で,次の行がsのとき,4文字左にインデントしてたら,4文字左にインデントする確率は1/97だけど,インデントしない確率は96/97なので,インデントしないように変えませんか,という表示を出す.


Plackのインデント情報を使って,Plack::Responseのインデントに変なところがないか見てみる.lint.rbっていうのを実行する.

% ruby lint.rb --stats examples/learned_plack.txt ~/co/Plack/lib/Plack/Response.pm
~/co/Plack/lib/Plack/Response.pm line 114, 3(0.004705882352941176) should 0(0.96)
      my $body = $self->body;
-        $body = [] unless defined $body;
+     $body = [] unless defined $body;

~/co/Plack/lib/Plack/Response.pm line 115, -3(0.0045662100456621) should 0(0.9680365296803652)
         $body = [] unless defined $body;
-     if (!ref $body or Scalar::Util::blessed($body) && overload::Method($body, q("")) && !$body->can('getline')) {
+        if (!ref $body or Scalar::Util::blessed($body) && overload::Method($body, q("")) && !$body->can('getline')) {

~/co/Plack/lib/Plack/Response.pm line 160, 15(0.1111111111111111) should 0(0.8888888888888888)
          return sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
-                        $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec);
+         $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec);

~/co/Plack/lib/Plack/Response.pm line 162, -19(0.001287001287001287) should -4(0.9703989703989704)
                         $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec);
-     }
+                    }


1つめはここの2行目のmy の行を,

    my $body = $self->body;
       $body = [] unless defined $body;
    if (!ref $body or Scalar::Util::blessed($body) && overload::Method($body, q("")) && !$body->can('getline')) {

こうしましょうって言ってる.

    my $body = $self->body;
    $body = [] unless defined $body;
    if (!ref $body or Scalar::Util::blessed($body) && overload::Method($body, q("")) && !$body->can('getline')) {

my の分インデントする流儀もあるけど,インデント3つする確率は0.004で,インデントしない確率は0.96ということなので,しない方に揃えたほうがいいと思う.


3つめは,ここの最後の}を,

    if ($expires =~ /^\d+$/) {
        # all numbers -> epoch date
        # (cookies use '-' as date separator, HTTP uses ' ')
        my($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($expires);
        $year += 1900;

        return sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
                       $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec);

    }

こうしましょうと言ってる.

    if ($expires =~ /^\d+$/) {
        # all numbers -> epoch date
        # (cookies use '-' as date separator, HTTP uses ' ')
        my($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($expires);
        $year += 1900;

        return sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
                       $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec);

                   }

この}はifを閉じる}なので,これはおかしい.前後の行しか見てないので,こういうのは誤判定する.


こう書き換えれば怒られなくなる.

    if ($expires =~ /^\d+$/) {
        # all numbers -> epoch date
        # (cookies use '-' as date separator, HTTP uses ' ')
        my($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($expires);
        $year += 1900;

        return sprintf(
            "%s, %02d-%s-%04d %02d:%02d:%02d GMT",
            $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec
        );

    }

さっきのとどっちがいいかというと,好みの問題だけど,僕はこっちのほうが好きで,統計的にも,こっちのほうがよく出てくる.


PODの中ではインデント2つでコードを表すみたいな感じで,Perlのコードとはインデントの意味がちがうので,誤判定することがある.
PODの中はPOD用のインデントのデータを使って調べるとか,PODはあらかじめ消しておいて対象外にするとか,するほうがよいと思う.

~/co/Plack/lib/Plack/Util.pm line 403, -2(0.027777777777777776) should 0(0.9722222222222222)
    if ( Plack::Util::is_real_fh($fh) ) { }
- returns true if a given C<$fh> is a real file handle that has a file
+   returns true if a given C<$fh> is a real file handle that has a file


これまでは,インデントのスタイルは,好きに書いたり,空気を読んで書いたり,コーディング規約を決めてなるべく守ったりしてた.
今回のように,プロジェクト内ではとか,世間ではどうなってるか機械が調べてくれると,ここはこうインデントしてるほうが多いようなので,そうしましょう,みたいにできて,便利だと思う.人の考えることが減る.
いまは確率が0.8以上だったら出すみたいな雑な感じだけど,分散も見て決めるとかすると,誤判定を減らせると思う.もうちょっとなんとかしたい.
いま文字列処理だけでやってるので,うまくいくかどうかは別にして,どんな言語でも動くということになってる.Perl用に調整とかしてない.字句解析すればカンマを最初につけるかとか,ここでスペースとか,もっと細かいこともできるはずだけど,そうするとPerlでしか使えなくなる.ただの文字列処理でやってるのがかっこいいと思う.また,いきなり字句解析すると手に負えなくなるだろうから,まずは改行だけでなんとかしようとしている,というのもある.