この記事は、表参道.rb #42でLTをした内容をベースに、加筆修正を行ったものです。発表資料はこちらにあります。
モチベーション
Ruby 2.6がリリースされたため、私のローカルにも導入されましたが、具体的にRuby 2.6の新機能を使ってどのような事ができるようになったのかを知りたくなりました。変更を追うのであればRuby2.6 Advent Calendarや、プロと読み解く Ruby2.6 NEWSファイルを読めばいいと思うのですが、まだリリースして2週間程度しか経っていない現時点で、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クラスのインスタンスを参照することができます。
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というメソッドに、Rubyの要求バージョンがGem::Requirementの形式で格納されているため、この値を使って判定を行います。
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個
1個も見つからなかったら企画終了だったのですが、まずは見つかって一安心という結果です。
除外Gem
見つかった35個のGemのうち、今回の調査対象から外したGemがあります。
例えばautherです。年1回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さんでした。
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に取り込む拡張。
# 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を使って実現しているようです。
$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 |
+---------+---------------+
まとめ
まだ公開されて2週間程度なので、Ruby2.6でしか動かないGemは少ないようですが、RubyVM::AbstractSyntaxTreeを使った黒魔術的なGemが人気のようです。これまで公開されていたGemがRuby2.6に対応するコードを追加したケースも入れると、もう少し引っかかりそうなのですが、今回のスクリプトでは検出できなかったので、何かいいアイデアがあれば是非教えてください。