Ruby | StringScanner で字句解析

# Ruby | StringScanner で字句解析

## 概要
StringScanner で字句解析を行います

## StringScanner って？
Ruby の標準ライブラリ。

スキャンする文字列と「スキャンポインタ」をセットで管理して、
正規表現でテキストをスキャンできます。

## サンプル
### 仕様
RuboCop の警告出力を字句解析します。
RuboCopのエラーフォーマットは simple を指定します。
※RuboCopのエラーフォーマットについては下記にまとめてあります。
＝＞[RuboCop | 警告の出力フォーマット指定](http://qiita.com/tbpgr/items/177932080f694d085582)

各警告内容をモデル「 RubocopWarning 」に設定します。
RubocopWarning は以下のインスタンス変数を持ちます。

* file
* type
* row
* column
* error

RubocopWarning に設定した各警告を配列に格納し、
最後の pp で標準出力します。

※RuboCopの simple フォーマットは以下のようになります。

~~~bash
== lib/hige.rb ==
C:  1:  1: Missing top-level class documentation comment.
# : 
# : 略
# : 
C:  6:  4: Final newline missing.

2 files inspected, 21 offenses detected
~~~

※わざわざ StringScanner を使うような処理ではないですが、説明のためということで黙認していただければ。
そもそも、 RuboCop は出力フォーマットとして JSON をサポートしてますし。

### RubyCop警告出力用の対象プログラム

~~~ruby:lib/hoge.rb
class Hoge
 def h(a)
  b
   [].inject(&:+)
  end
end
~~~

~~~ruby:lib/hige.rb
class Hige
 def h(a)
  b
   [].collect(&:+)
   [].map do|e| e;end
  end
end
~~~

### StringScanner利用プログラム

~~~ruby:ruby_string_scanner.rb
require 'pp'
require 'strscan'

class RubocopWarningParseError < StandardError; end
class RubocopWarning
  attr_accessor :file, :type, :row, :column, :error
end

rubocop_messag = ARGV.first
s = StringScanner.new(rubocop_messag)
file = ""

rubocop_warnings = []
rw = RubocopWarning.new

row_line_count = 0
until s.eos?
  case
  when s.scan(%r{== (?<file>.*) ==\n})
    file = s[:file]
  when s.scan(%r{(?<type>[CEFRW]): +})
    rw.type = s[:type]
  when s.scan(%r{(?<row_or_column>\d+):( )+})
    if row_line_count == 0
      rw.row = s[:row_or_column]
      row_line_count = 1
    else
      rw.column = s[:row_or_column]
      row_line_count = 0
    end
  when s.scan(%r{(?<error>.*)\n})
    rw.error = s[:error]
    rw.file = file
    rubocop_warnings << rw if rw.type
    rw = RubocopWarning.new
  else
    fail RubocopWarningParseError, 'parse error'
  end
end

pp rubocop_warnings
~~~

## 出力

~~~bash
$ rubocop lib --format simple | xargs -0 ruby ruby_string_scanner.rb
[#<RubocopWarning:0x0000060038a1c0
  @column="1",
  @error="Missing top-level class documentation comment.",
  @file="lib/hige.rb",
  @row="1",
  @type="C">,
 #<RubocopWarning:0x00000600389ef0
  @column="1",
  @error="Carriage return character detected.",
  @file="lib/hige.rb",
  @row="1",
  @type="C">,
 #<RubocopWarning:0x00000600389d88
  @column="1",
  @error="Use 2 (not 1) spaces for indentation.",
  @file="lib/hige.rb",
  @row="2",
  @type="C">,
 #<RubocopWarning:0x00000600389bf8
  @column="8",
  @error=
   "Unused method argument - a. If it's necessary, use _ or _a as an argument name to indicate that it won't be used. You can also write as h(*) if you want the method to accept any arguments but don't care about them.",
  @file="lib/hige.rb",
  @row="2",
  @type="W">,
 #<RubocopWarning:0x00000600389a68
  @column="2",
  @error="Use 2 (not 1) spaces for indentation.",
  @file="lib/hige.rb",
  @row="3",
  @type="C">,
 #<RubocopWarning:0x00000600389900
  @column="4",
  @error="Inconsistent indentation detected.",
  @file="lib/hige.rb",
  @row="4",
  @type="C">,
 #<RubocopWarning:0x00000600389798
  @column="7",
  @error="Prefer map over collect.",
  @file="lib/hige.rb",
  @row="4",
  @type="C">,
 #<RubocopWarning:0x00000600389630
  @column="4",
  @error="Inconsistent indentation detected.",
  @file="lib/hige.rb",
  @row="5",
  @type="C">,
 #<RubocopWarning:0x000006003894c8
  @column="11",
  @error="Prefer {...} over do...end for single-line blocks.",
  @file="lib/hige.rb",
  @row="5",
  @type="C">,
 #<RubocopWarning:0x00000600389338
  @column="18",
  @error="Space missing after semicolon.",
  @file="lib/hige.rb",
  @row="5",
  @type="C">,
 #<RubocopWarning:0x000006003891d0
  @column="3",
  @error="end at 6, 2 is not aligned with def at 2, 1",
  @file="lib/hige.rb",
  @row="6",
  @type="W">,
 #<RubocopWarning:0x00000600389040
  @column="4",
  @error="Final newline missing.",
  @file="lib/hige.rb",
  @row="7",
  @type="C">,
 #<RubocopWarning:0x00000600388ed8
  @column="1",
  @error="Missing top-level class documentation comment.",
  @file="lib/hoge.rb",
  @row="1",
  @type="C">,
 #<RubocopWarning:0x00000600388cd0
  @column="1",
  @error="Carriage return character detected.",
  @file="lib/hoge.rb",
  @row="1",
  @type="C">,
 #<RubocopWarning:0x00000600388b68
  @column="1",
  @error="Use 2 (not 1) spaces for indentation.",
  @file="lib/hoge.rb",
  @row="2",
  @type="C">,
 #<RubocopWarning:0x00000600388a00
  @column="8",
  @error=
   "Unused method argument - a. If it's necessary, use _ or _a as an argument name to indicate that it won't be used. You can also write as h(*) if you want the method to accept any arguments but don't care about them.",
  @file="lib/hoge.rb",
  @row="2",
  @type="W">,
 #<RubocopWarning:0x00000600388870
  @column="2",
  @error="Use 2 (not 1) spaces for indentation.",
  @file="lib/hoge.rb",
  @row="3",
  @type="C">,
 #<RubocopWarning:0x00000600388708
  @column="4",
  @error="Inconsistent indentation detected.",
  @file="lib/hoge.rb",
  @row="4",
  @type="C">,
 #<RubocopWarning:0x000006003885a0
  @column="7",
  @error="Prefer reduce over inject.",
  @file="lib/hoge.rb",
  @row="4",
  @type="C">,
 #<RubocopWarning:0x00000600388438
  @column="3",
  @error="end at 5, 2 is not aligned with def at 2, 1",
  @file="lib/hoge.rb",
  @row="5",
  @type="W">,
 #<RubocopWarning:0x000006003882a8
  @column="4",
  @error="Final newline missing.",
  @file="lib/hoge.rb",
  @row="6",
  @type="C">]
~~~

## 参照
http://docs.ruby-lang.org/ja/2.1.0/class/StringScanner.html