tDiary の1.8と1.9の互換性を向上したい
2年前にtDiaryの1.8と1.9の混在を試した時に、Stringの仕様変更に対応して以下のようなコードを書いた。
class String def method_missing(name, *args, &block) each_line.__send__(name, *args, &block) end end
これはruby 1.9でStringがEnumerableでなくなった (eachメソッドを持たなくなった) ことに対応するため。
# ruby1.8.7 irb(main):001:0> "aaa\nbbb".map => ["aaa\n", "bbb"]
# ruby1.9.1 irb(main):001:0> "aaa\nbbb".map NoMethodError: undefined method `map' for "aaa\nbbb":String from (irb):1 from /opt/local/bin/irb1.9:12:in `<main>'
既存コードに手を入れたくなかったので、String#mapなどのEnumerable系のメソッドが呼ばれた場合に、method_missingを使ってeach_line経由で呼び出すようにしてた。
いま見てもこれはひどいコードなので、もう少し互換性をあげようと思う。
method_missingで反応するメソッドを絞り込む
元のコードはすべての存在しないメソッドに対してeach_line経由に変換してる。 このままだと "string".mapp のようなtypoにも反応してしまってバグの温床になる。 そこで、Enumerable系のメソッドだけに反応するようにして、それ以外はsuperを呼び出してちゃんと例外を返すようにした。
class String def method_missing(name, *args, &block) if each_line.respond_to?(name) each_line.__send__(name, *args, &block) else super end end end
1.8系でもString#linesメソッドを使うようにする
これまでのように
body.map { ... }
とするのではなく、
body.lines.map { ... }
と書くようにする。 2年前にリリースされたruby1.8.7ではString#linesが追加されているので、1.8.7と1.9.1だと互換性が保たれている。 もちろん、1.8.6以前でも動くように compatible.rb に String#lines を定義しておく。
unless "".respond_to?('lines') class String alias_method :lines, :to_a end end
String#eachが呼び出されるとログに記録する
linesを使うようにするため、String#eachが呼ばれるたびにログに呼び出し元を記録するようにした。
class String def method_missing(name, *args, &block) if each_line.respond_to?(name) require 'logger' log_file = "deprecated.log" @@tdiary_compatibile_logger ||= Logger.new(log_file) @@tdiary_compatibile_logger.warn("called deprecated method: String##{name}") @@tdiary_compatibile_logger.warn(caller.join("\n")) each_line.__send__(name, *args, &block) else super end end end
これを埋め込んでおいて、ログに出力された箇所をlinesを使うように置き換えていけばOK(本当は、テストコードで検出できるのが一番いいんだけどね)。 …と思って、早速ここの日記に埋め込んでみたけど、まだ1つもログが出力されない。 もしかして、もうmethod_missingを使ったモンキーパッチは不要なのかも。
進展があったら、GitHubのブランチでちまちま作業していく予定。