開発版の Ruby 3.4 に『ブロック引数を利用しないメソッドにブロック引数を渡すと警告を出力する』対応が入った
結構前の話になるんですが開発版の Ruby 3.4 で次のように『ブロック引数を渡したときにそのメソッドがブロック引数を利用していないと警告がでる』という対応が入りました。
例えば以下のように hoge
メソッドでブロック引数は利用していないんですが、このメソッドを呼び出すときにブロック引数を渡すと警告が出るようになります。
# このメソッドではブロック引数は利用していない
def hoge
end
# メソッド内でブロック引数を利用していない場合に警告がでる
# warning: the passed block for 'Object#hoge' defined at test.rb:1 may be ignored
hoge {}
また、この警告は『 -w
オプションがついてるときのみ』出力されます。
# test.rb
def hoge
puts "hello"
end
hoge {}
# -w がない場合は警告はでない
$ ruby test.rb
hello
# -w がある場合に警告が出る
$ ruby -w test.rb
test.rb:6: warning: the block passed to 'Object#hoge' defined at test.rb:2 may be ignored
hello
提案自体は5年以上前(Ruby 2.6 のリリース後)からされてはいたんですが開発版の Ruby 3.4 で対応が入った形になります。
背景
この対応の議論は Feature #15554: warn/error passing a block to a method which never use a block で行われてます。
背景としては以下のように『ブロック引数が呼ばれることを期待するが実際にはブロック引数は呼ばれない』みたいなコードを書いてしまう可能性があります。
このような問題を解決したいというのが今回の対応のモチベーションになります。
def my_open(name)
open(name)
end
# Kernel#open と同じような感じでブロック引数が渡せることを期待するが
# #my_open ではブロック引数を利用していないので意図する動作が行われない
my_open(name){ |f| important_work_with f }
この対応をするにあたりどこまで対応するのか、ルールや実装はどうするのかの議論がチケット内で行われています。
全体的なルール
以下のケースに該当する場合は『ブロック引数が利用されている』という風に定義されます。
- (1) メソッドの引数にブロック引数がある場合(
hoge(&block)
) - (2) メソッド内で
yield
を利用している場合 - (3) メソッド内で
super
を利用したときにブロック引数をフォワードしている場合 -
(4) メソッド内でシングルトンメソッドを利用している場合- これはチケットの説明で言及されているんですが Ruby 3.0 時点ですでに無効な構文となっているので特に考えなくてもよさそうです
参照: https://bugs.ruby-lang.org/issues/15554#Define-use-a-block-methods
なので逆に上に該当しないメソッドの場合は『ブロック引数が利用されていない』と解釈がされてブロック引数を渡すと警告が出力されます。
それぞれ詳しく解説していきます。
hoge(&block)
)
(1) メソッドの引数にブロック引数がある場合( 次のようにメソッドの引数でブロック引数がある場合は『ブロック引数を利用している』と定義されます。
なので以下の場合では警告は出力されません。
# これはブロック引数がある
def hoge(&block)
end
# ブロック引数があるので警告はでない
# no warning
hoge {}
逆にいうと『ブロック引数がない場合』は『ブロック引数を利用していない』と解釈されてブロック引数を渡すと警告が出力されます。
# これはブロック引数がない
def hoge
end
# ブロック引数がないメソッドにブロック引数を渡すと警告がでる
# warning: warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
hoge {}
# send でも同様に警告が出る
# warning: warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
send(:hoge) {}
# & 渡しでも警告が出る
# warning: warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
hoge(&:to_s)
また eval
経由で呼び出した場合にも警告がでます。
def hoge
end
# warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
eval("hoge {}")
ちなみに同じメソッドが複数回呼び出される場合は『1回のみ』警告が出力されます。
def hoge
end
(1..10).each {
# この場合、警告は1回のみ出力される
# warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
hoge {}
}
def hoge
end
# 以下の場合も1回のみ警告が出力される
# warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
hoge {}
# no warning
hoge {}
ここら辺のルールは深堀できていないので認識がちょっと違うかも…?(別ファイルの場合はどうなるのか、とか。
yield
を利用している場合
(2) メソッド内で ブロック引数を受け取らないがブロック内で yield
を利用している場合には『ブロック引数を利用している』と解釈されます。
なので以下のような場合は警告は出力されません。
# ブロック引数はないが内部で yield を利用している
def hoge
yield
end
# ブロック引数を利用しているので警告は出ない
# no warning
hoge {}
ただし、現状の実装では eval
内で yield
を呼び出した場合は警告が出力されます。
def hoge
eval("yield")
end
# warning: warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
hoge {}
これに関しては今後どうなるんですかねー。
super
を利用したときにブロック引数をフォワードしている場合
(3) メソッド内で super
関連はちょっとややこしいです。
次のように super
を呼び出した場合はいずれも『(暗黙的に)自身のメソッドに渡されたブロック引数を親メソッドに渡す』という挙動になります。
class Super
def case1; yield end
def case2; yield end
def case3; yield end
end
class Sub < Super
# 以下の書き方はいずれも自身に渡されたブロック引数を super に渡した状態になる
def case1
super
end
def case2
super()
end
def case3(&block)
super(&block)
end
end
sub = Sub.new
# ここで渡したブロック引数はサブクラスのメソッドを経由して親クラスのメソッドにフォワードされる
sub.case1 { pp "case1" } # => "case1"
sub.case2 { pp "case2" } # => "case2"
sub.case3 { pp "case3" } # => "case3"
なので上記のような super
の書き方であれば『ブロック引数を利用している』と解釈されて警告は出力されません。
一方で以下のような super
の書き方の場合は super
を呼び出すときに『明示的に』ブロック引数を渡しています。
つまり『(暗黙的に)自身のメソッドに渡されたブロック引数を親メソッドに渡さない』という挙動になり『自身で受け取ったブロック引数は利用していない』と解釈されます。
なので以下の場合は警告が出力されます。
class Super
def case1; end
def case2; end
def case3; end
end
class Sub < Super
# 以下の書き方はいずれも自身に渡されたブロック引数は super に渡さずに
# super を呼び出すタイミングで別のブロック引数を渡している
def case1
super(&:nil)
end
def case2
super() {}
end
def case3
super {}
end
end
sub = Sub.new
# 以下で渡したブロック引数はいずれも呼び出されることはないので警告が出る
# warning: the block passed to 'Sub#case1' defined at test.rb:10 may be ignored
sub.case1 { pp "case1" } # => nil
# warning: the block passed to 'Sub#case2' defined at test.rb:14 may be ignored
sub.case2 { pp "case2" } # => nil
# warning: the block passed to 'Sub#case3' defined at test.rb:18 may be ignored
sub.case3 { pp "case3" } # => nil
これはそもそも super
がかなり特殊な挙動になっているので個別に対応しているような形になっています。
(4) メソッド内でシングルトンメソッドを利用している場合(今は無効な構文)
これはチケットの説明で言及されている例なんですが Ruby 2.7 時点では有効な構文でした。
しかし Ruby 3.0 で無効な構文になったようです。
def hoge
class << Object.new
yield
end
end
hoge { pp "hello" }
# Ruby 2.7 => "hello"
# Ruby 3.0 => error: Invalid yield (SyntaxError)
なので現時点では特に考慮しなくてもよさそうです。
次からは少し細かい話になります。
ダックタイピング的なメソッドを呼び出した場合にどうするのか問題
以下のように同じ名前のメソッドを呼び出したときに片方でブロック引数を利用している場合は警告が出力されません。
class X
def hoge
yield
end
end
class Y < X
def hoge
end
end
x, y = X.new, Y.new
# no warning
[x, y].map { _1.hoge {} }
これは元々警告が出力されるようになっていたんですが、誤検知が多いので relax unused block warning for duck typing by ko1 · Pull Request #10559 · ruby/ruby で別途『警告が出力されないように』対応されました。
例えば Rails だと Object#try(arg, &block)
ではなくて NilClass#try(arg)
を呼び出した場合とかで誤検知の話が上がったみたいです。
参照: https://bugs.ruby-lang.org/issues/15554#note-28
ただ、上記の対応の影響で以下のような呼び出しの場合でも警告は出力されません。
class X
def hoge
yield
end
end
class Y < X
def hoge
end
end
# Y#hoge だけを呼び出した場合でも警告がでない
# no warning
Y.new.hoge {}
条件としては『同じ名前のメソッドでいずれかのメソッドでブロック引数が利用されていれば〜』みたいな判定になっているぽい?
これに関しては以下のコメントでも言及されています。
参照: https://bugs.ruby-lang.org/issues/15554#note-32
このあたりの対応を厳密にどこまでやるのかは難しそうですねえ。
個人的には https://bugs.ruby-lang.org/issues/15554#note-40 でも言及されているんですが、ダックピングするメソッドのシグネチャは一緒にする方が自然ではある(し、実際位置引数などは一緒のシグネチャにする必要がある)のでブロック引数も一緒にするほうが自然なんじゃないかな、という気はしますねえ。
class X
def hoge(a, &block)
block.call a + a
end
end
class Y < X
# a を利用しない場合でも引数を受け取る必要があるので仮引数は定義しておく
def hoge(a = nil, &)
# ここでは引数は参照しない
end
end
この話の延長線上で実験的に厳格モードを追加して Rails のテストでどれだけ影響があるのか検証しているようです。
参照
- https://bugs.ruby-lang.org/issues/15554#note-42
RUBY_TRY_UNUSED_BLOCK_WARNING_STRICT
by ko1 · Pull Request #10578 · ruby/ruby
この厳格モードはあくまでも検証用なのでリリース前には削除される予定です。
それ以外の細かいケース
Ruby 以外で実装されているメソッドにブロック引数を渡した場合
例えば組み込みのメソッドなどの C言語のレイヤーで実装されている場合は警告が出力されません。
# object_id は本来ブロック引数を受け取るようなメソッドではないが警告は出ない
# no warning
1.object_id {}
&nil
をブロック引数として渡した場合
ブロック引数を利用していないメソッドに &nil
を渡した場合には警告は出力されません。
def hoge
end
# これは警告はでる
# warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
hoge(&:to_s)
# こっちは警告が出ない
# no warning
hoge(&nil)
&nil
はそもそも『メソッドにブロック引数を渡していない』という風に解釈されるので警告がでない感じですね。
def hoge
block_given?
end
# これはブロック引数を渡している
pp hoge(&:so_s) # => true
# これはブロック引数を渡していないと解釈される
pp hoge(&nil) # => false
#block_given?
を利用した場合
メソッド内で メソッド内で #block_given?
だけを利用した場合は『ブロック引数を利用していない』と解釈されます。
なので以下のような場合は警告が出力されます。
def hoge
block_given?
end
# warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
hoge {}
これは『 #block_given?
は yield
やブロック引数と一緒に利用していることを想定している』ので特に対応はされていないようですね。
参照: https://bugs.ruby-lang.org/issues/15554#block_given
ただ、以下のようなリアルケースで意図しない警告が出力されている、という報告がありました。
def distribution(key, value = UNSPECIFIED, sample_rate: nil, tags: nil, no_prefix: false, &block)
# ブロック引数の有無に関するエラーハンドリングを別のメソッドで行っている
check_block_or_numeric_value(value, &block)
check_tags_and_sample_rate(sample_rate, tags)
super
end
private
# このメソッドでは #block_given? を用いてエラーハンドリングを行っている
def check_block_or_numeric_value(value)
if block_given?
raise ArgumentError, "The value argument should not be set when providing a block" unless value == UNSPECIFIED
else
raise ArgumentError, "The value argument should be a number, got #{value.inspect}" unless value.is_a?(Numeric)
end
end
参照: https://bugs.ruby-lang.org/issues/15554#note-26
super
経由でメソッドを呼び出した場合
super
メソッドではブロック引数は利用していないが super
を呼び出すときにブロック引数を渡しても警告は出力されません。
class Super
def hoge; end
end
class Sub < Super
def hoge
# 親メソッドではブロック引数を利用していないが警告はでない
super {}
end
end
Sub.new.hoge
これは意図してないような気もするんですが super
がかなり特殊だからなんですかね?
#instance_eval / class_eval
でメソッドを定義した場合
eval("yield")
だと『ブロック引数を利用していない』と解釈されていたんですが #instance_eval / class_eval
経由でメソッド定義した場合は『ブロック引数を利用している』と解釈されるので警告は出力されないようですね。
class X; end
X.class_eval <<~EOS
def hoge
yield
end
EOS
x = X.new
# no warning
x.hoge {}
x.instance_eval <<~EOS
def foo
yield
end
EOS
# no warning
x.foo {}
yield
を消すと警告が出力されるようになります。
class X; end
X.class_eval <<~EOS
def hoge
# yield
end
EOS
x = X.new
# warning: the block passed to 'X#hoge' defined at (eval at test.rb:3):1 may be ignored
x.hoge {}
x.instance_eval <<~EOS
def foo
# yield
end
EOS
# warning: the block passed to 'foo' defined at (eval at test.rb:13):1 may be ignored
x.foo {}
Method, UnboundMethod
経由でメソッドを呼び出した場合
Method#call
などでメソッドを呼び出したときには警告は出力されません。
class X
def hoge
end
end
x = X.new
# 以下は2つとも警告は出ない
# no warning
x.method(:hoge).call {}
# no warning
X.instance_method(:hoge).bind(x).call {}
これは『ブロック引数が呼び出されるメソッド(上記だと #hoge
)に直接渡されるのではなくて Method#call
に渡されているから』になるんですかねー。
(...)
で引数をフォワードした場合
(...)
でブロック引数を含む引数をフォワードした場合にも警告が出力されます。
def hoge
end
def foo(...)
hoge(...)
end
# warning: the block passed to 'Object#hoge' defined at test.rb:1 may be ignored
foo {}
まとめ
警告がでるケース
# ブロック引数がなかったり yield を呼び出してないメソッドにブロック引数を渡すと警告が出る
def case1
end
# warning: the block passed to 'Object#hoge' defined at test.rb:2 may be ignored
case1 {}
# また block_given? を参照しただけではブロック引数は利用されていないと判断されるのでこれも同様に警告が出る
def case2
block_given?
end
# warning: the block passed to 'Object#case2' defined at test.rb:9 may be ignored
case2 {}
# eval("yeild") もブロック引数を利用しているとは現状はみなしていない
def case3
eval("yield")
end
# warning: the block passed to 'Object#case3' defined at test.rb:18 may be ignored
case3 {}
# super を呼び出したときに暗黙的にブロック引数が渡されなかった場合
# super では一部の書き方で暗黙的にブロック引数が渡されるんですが、それに該当しない場合になる
class Super
def case4; end
def case5; end
def case6; end
end
class Sub < Super
# 以下の書き方はいずれも自身に渡されたブロック引数は super に渡さずに
# super を呼び出すタイミングで別のブロック引数を渡している
def case4
super(&:nil)
end
def case5
super() {}
end
def case6
super {}
end
end
sub = Sub.new
# 以下で渡したブロック引数はいずれも呼び出されることはないので警告が出る
# warning: the block passed to 'Sub#case4' defined at test.rb:37 may be ignored
sub.case4 { pp "case1" } # => nil
# warning: the block passed to 'Sub#case5' defined at test.rb:41 may be ignored
sub.case5 { pp "case2" } # => nil
# warning: the block passed to 'Sub#case6' defined at test.rb:45 may be ignored
sub.case6 { pp "case3" } # => nil
警告がでないケース
ブロック引数を渡したメソッドが『ブロック引数を利用している場合』には警告は出力されません。
# ブロック引数が定義されている場合
def case1(&block)
end
# no warning
case1 {}
# ブロック引数はないが内部で yield を使用している場合
def case2(&block)
end
# no warning
case2 {}
# ブロック引数は利用していないが &nil を渡した場合
# &nil はブロック引数を渡していないことになる
def case3
end
# no warning
case3(&nil)
# super を呼び出したときに暗黙的に super にブロック引数が渡される場合
class Super
def case4; yield end
def case5; yield end
def case6; yield end
end
class Sub < Super
# 以下の書き方はいずれも自身に渡されたブロック引数を super に渡した状態になる
def case4
super
end
def case5
super()
end
def case6(&block)
super(&block)
end
end
sub = Sub.new
# ここで渡したブロック引数はサブクラスのメソッドを経由して親クラスのメソッドにフォワードされる
# no warning
sub.case4 { pp "case1" } # => "case1"
# no warning
sub.case5 { pp "case2" } # => "case2"
# no warning
sub.case6 { pp "case3" } # => "case3"
# 同名のメソッドがある時にいずれかのメソッドでブロック引数を利用している場合
class X
# こっちではブロック引数を利用している
def case7
yield
end
end
class Y
# こっちではブロック引数を利用していない
def case7
end
end
# いずれかの同名のメソッドでブロック引数を利用している場合は警告は出ない
# no warning
Y.new.case7
# 親メソッドがブロック引数を利用していなくても super にメソッドを渡すと
class Super
def hoge; end
end
class Sub < Super
def hoge
# 親メソッドではブロック引数を利用していないが警告はでない
super {}
end
end
Sub.new.hoge
所感
ざっと触ってみた所感ですが基本的には『明らかにブロック引数を利用していない』ケースのみ警告が出ている感じではあるのでリアルケースだとそこまで誤検知はなさそう…?
メソッド内で eval("yield")
を呼び出した場合のみ『ブロック引数を利用していない』と解釈される部分が気になるぐらいですかねー。
普段はこういう書き方はしないと思うんですがライブラリ内部とかで特殊な書き方をしている場合にこのケースでどれだけ困るのかは気になるところ。
まあこのあたりは実際にプロダクトで動かしてみないとわからないところではありますねえ。
実際にこの機能が Ruby 3.4 に入ったあとにもどこまで警告を厳格化するのか、みたいな議論が引き続き行われています。
個人的には厳格モードがあったほうがいろいろと検知しやすそうな気はしますがどこまでノイズになるのかも試してみないところには見えてこなさそうですねえ。
以下、チケットのコメントで気になったところ。
- 実際に意図しないコードが検知できた
- https://bugs.ruby-lang.org/issues/15554#note-27
-
SOME_LIST.excluding(things).each {}
と書きたかったが実際にはSOME_LIST.excluding(things) {}
と書かれていたらしい
- Rails のテストケースから警告を削除してみた
- https://bugs.ruby-lang.org/issues/15554#note-28
- ダックタイピング関連でいくつか誤検知があったらしい
-
Object#try(arg, &block)
ではなくてNilClass#try(arg)
を呼び出した場合とか - これは relax unused block warning for duck typing by ko1 · Pull Request #10559 · ruby/ruby で警告がでないように対応された
-
Discussion