Ruby 2.6でしか動かないGemを探す

この記事は、[表参道.rb #42](https://omotesandorb.connpass.com/event/113435/)でLTをした内容をベースに、加筆修正を行ったものです。発表資料はこちらにあります。

<iframe src="//www.slideshare.net/slideshow/embed_code/key/diM8IpuyDxb7pV" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/MakiToshio/searched-gems-which-supports-only-ruby-26" title="Searched gems which supports only ruby 2.6" target="_blank">Searched gems which supports only ruby 2.6</a> </strong> from <strong><a href="https://www.slideshare.net/MakiToshio" target="_blank">Maki Toshio</a></strong> </div>

# モチベーション

　Ruby 2.6がリリースされたため、私のローカルにも導入されましたが、具体的にRuby 2.6の新機能を使ってどのような事ができるようになったのかを知りたくなりました。変更を追うのであれば[Ruby2.6  Advent Calendar](https://qiita.com/advent-calendar/2018/ruby26)や、[プロと読み解く Ruby2.6 NEWSファイル](https://techlife.cookpad.com/entry/2018/12/25/110240)を読めばいいと思うのですが、まだリリースして２週間程度しか経っていない現時点で、Ruby 2.6でしか動かないGemはどのような使い方をしているのか、ということを調べてみると楽しそうだな、と思い、LTの発表テーマにすることにしました。

# 調べ方

　以下のようなスクリプトを用意します。
　事前に https://rubygems.org/latest_specs.4.8.gz をダウンロードして同一ディレクトリに置いておく必要があります。説明不要かと思いますが、latest_spec.4.8.gzはそのサイトに格納されているrubygemsのインデックス情報をすべて持っているファイルです。
　これを展開して、Marshal.loadしてあげることで、各Gemの情報が手に入るため、個別にgemspec情報が格納されている情報をダウンロードしてあげることで、[Gem::Specificationクラス](https://docs.ruby-lang.org/ja/latest/class/Gem=3a=3aSpecification.html)のインスタンスを参照することができます。

```ruby
require 'rubygems'
require 'open-uri'
require 'ruby-progressbar'

specs = Marshal.load(Gem.gunzip(File.read("./latest_specs.4.8.gz")))
total = specs.length

pb = ProgressBar.create(
  :title => "Searching",
  :starting_at => 0,
  :progress_mark => '>',
  :remainder_mark => '#',
  :format => '%t(%c/%C): S%BE :%t',
  :total => total,
  :length => 50
)

specs.each do |gem_name, gem_version, _|
  pb.progress += 1
  begin
    compressed = open("https://rubygems.org/quick/Marshal.4.8/#{gem_name}-#{gem_version}.gemspec.rz").read
    inflated = Gem.inflate(compressed)
    spec = Marshal.load(inflated)
    ruby1_8 = Gem::Version.new("1.8.7")
    ruby1_9 = Gem::Version.new("1.9.3")
    ruby2_0 = Gem::Version.new("2.0.0")
    ruby2_1 = Gem::Version.new("2.1.0")
    ruby2_2 = Gem::Version.new("2.2.0")
    ruby2_3 = Gem::Version.new("2.3.0")
    ruby2_4 = Gem::Version.new("2.4.0")
    ruby2_5 = Gem::Version.new("2.5.0")
    ruby2_5_1 = Gem::Version.new("2.5.1")
    ruby2_5_2 = Gem::Version.new("2.5.2")
    ruby2_5_3 = Gem::Version.new("2.5.3")
    ruby2_6 = Gem::Version.new("2.6.0")
    old_versions = [ruby1_8, ruby1_9, ruby2_0, ruby2_1,
                    ruby2_2, ruby2_3, ruby2_4, ruby2_5,
                    ruby2_5_1, ruby2_5_2, ruby2_5_3]
    required = spec.required_ruby_version
    if old_versions.all?{|v| !required.satisfied_by?(v) } && required.satisfied_by?(ruby2_6)
      puts "#{gem_name}-#{gem_version}"
    end
  rescue => e
    puts "error: #{gem_name}: #{e.message}"
  end
end
pb.finish
```

　その中で[required_ruby_version](https://docs.ruby-lang.org/ja/latest/class/Gem=3a=3aSpecification.html#I_REQUIRED_RUBY_VERSION)というメソッドに、Rubyの要求バージョンが[Gem::Requirement](https://docs.ruby-lang.org/ja/latest/class/Gem=3a=3aRequirement.html)の形式で格納されているため、この値を使って判定を行います。

　Gem::Requirement#satisfied_by?は、引数に指定した、バージョンが要求を満たしているかを判定するメソッドです。当初は`required_ruby_version.satisfied_by?(Gem::Version.new(2.6.0))`だけで大丈夫かと思ったのですが、それだと、`>= 2.0`のような指定をされているGemでも適合してしまうため、Ruby 1.8〜Ruby 2.5までのバージョンを用意し、それらの全てで`satisfied_by?`の結果がfalseになり、Ruby 2.6でsatisfied_by?がtrueになるGemを抽出しています。

# 実行結果

```
対象Gem数: 148736個(2018/1/10時点)
バッチ実行時間： 13時間
該当Gem数: 35個
```

　１個も見つからなかったら企画終了だったのですが、まずは見つかって一安心という結果です。

# 除外Gem

　見つかった35個のGemのうち、今回の調査対象から外したGemがあります。
　例えば[auther](https://rubygems.org/gems/auther)です。年１回required_ruby_versionをアップデートしているようで、最新のGemバージョンにおいては、最新のRubyバージョンしかサポートしないポリシーのようです。この作者のGemは全てこのポリシーになっており、困る人いないのかな…とも思いますが、今回は気にしないことにしました。

　これらのGemを除外することで、有意な結果になりました。

# Ruby2.6でしか動かないGem

## covered

rubygems: https://rubygems.org/gems/covered
github: https://github.com/ioquatix/covered

RubyVM::AbstractSyntaxTreeを使ったカバレッジツール。通常のアプローチと違って、eval内のコードもカバレッジを取得できるのが特徴のようです。

## function-composite

rubygems: https://rubygems.org/gems/function-composite
github: https://github.com/nobu/function-composite

Ruby2.6から導入された`Proc#<<`、`Proc#>>`の引数にSymbolも渡せるようにした拡張。作者はnobuさんでした。

```ruby
require 'function-composite'
using Function::Composite

p %w{72 101 108 108 111}.map(&:to_i >> :chr) #=> ["H", "e", "l", "l", "o"]

p %w{72 101 108 108 111}.map(&proc { |s| s.to_i } >> :chr) #=> ["H", "e", "l", "l", "o"]

h = { Alice: 30, Bob: 60, Cris: 90 }
p %w{Alice Bob Cris}.map(&(:to_sym >> h)) #=> [30, 60, 90]
```

## import_as

rubygems: https://rubygems.org/gems/import_as
github: https://github.com/hanachin/import_as

TypeScriptライクなimport文をRubyに取り込む拡張。

```ruby
# c.rb
class C
  def hi
    puts :hi
  end
end

# main.rb
require "import_as/core_ext"

import { C as C2 }.from "./c.rb"

C2.new.hi
# hi
```

RubyVM::AbstractSyntaxTreeを使って実現しているようです。

## pdfh

rubygems: https://rubygems.org/gems/pdfh
github: https://github.com/iax7/pdfh

　PDFのスクレイピングライブラリ。Initial Commitが2019/1/6で、そのためrequired_ruby_versionが2.6になったものと思われます。

## rvnc

rubygems: https://rubygems.org/gems/rvnc
github: https://github.com/siman-man/rvnc

 Rubyの変数一覧を出力してくれるツール。RubyVM::AbstractSyntaxTreeを使って実現しているようです。

```ruby
$global = 'global'
foo = 1
bar = 'hi'
BAZ = :baz
a, *b = [1, 2, 3]

class A
  @@test = 'test'

  def initialize
    @name = 'test'
  end
end
```

```
+---------+---------------+
| Name    | Location      |
+---------+---------------+
| $global | example.rb:1  |
| foo     | example.rb:2  |
| bar     | example.rb:3  |
| BAZ     | example.rb:4  |
| a       | example.rb:5  |
| b       | example.rb:5  |
| @@test  | example.rb:8  |
| @name   | example.rb:11 |
+---------+---------------+
```

# まとめ

　まだ公開されて２週間程度なので、Ruby2.6でしか動かないGemは少ないようですが、RubyVM::AbstractSyntaxTreeを使った黒魔術的なGemが人気のようです。これまで公開されていたGemがRuby2.6に対応するコードを追加したケースも入れると、もう少し引っかかりそうなのですが、今回のスクリプトでは検出できなかったので、何かいいアイデアがあれば是非教えてください。