[ruby-core:117663] [Ruby master Feature#15554] warn/error passing a block to a method which never use a block
From:
"Dan0042 (Daniel DeLorme) via ruby-core" <ruby-core@...>
Date:
2024-04-23 21:38:38 UTC
List:
ruby-core #117663
Issue #15554 has been updated by Dan0042 (Daniel DeLorme).
byroot (Jean Boussier) wrote in #note-53:
> @ko1 here's the result (I ran it on the 5 most important sub components)
I think it's worth mentioning there's 67 warnings in that output but "only"=
25 uniques. Acceptable for a codebase as big as Rails?
In order to understand the false positives I had a look at the "add_bind" w=
arnings. And at first I thought the ignored block was actually a bug! It to=
ok me half an hour of looking through the code to finally understand what i=
t did and why it was ok to ignore the block. So while it's a false positive=
, this mirrors my experience in #note-45 that making the arguments more exp=
licit would really help understanding of the code. my 2=A2
----------------------------------------
Feature #15554: warn/error passing a block to a method which never use a bl=
ock
https://bugs.ruby-lang.org/issues/15554#change-108071
* Author: ko1 (Koichi Sasada)
* Status: Closed
* Assignee: matz (Yukihiro Matsumoto)
----------------------------------------
# Abstract
Warn or raise an ArgumentError if block is passed to a method which does no=
t use a block.
In other words, detect "block user methods" implicitly and only "block user=
methods" can accept a block.
# Background
Sometimes, we pass a block to a method which ignores the passed block accid=
entally.
```
def my_open(name)
open(name)
end
# user hopes it works as Kernel#open which invokes a block with opened file.
my_open(name){|f| important_work_with f }
# but simply ignored...
```
To solve this issue, this feature request propose showing warnings or raisi=
ng an exception on such case.
Last developer's meeting, matz proposed `&nil` which declares this method n=
ever receive a block. It is explicit, but it is tough to add this `&nil` pa=
rameter declaration to all of methods (do you want to add it to `def []=3D(=
i, e, &nil)`?).
(I agree `&nil` is valuable on some situations)
# Spec
## Define "use a block" methods
We need to define which method accepts a block and which method does not.
* (1) method has a block parameter (`&b`)
* (2) method body has `yield'
* (3) method body has `super` (ZSUPER in internal terminology) or `super(..=
.)`
* (4) method body has singleton method (optional)
(1) and (2) is very clear. I need to explain about (3) and (4).
(3). `super` (ZSUPER) passes all parameters as arguments. So there is no su=
rprise that which can accept `block`.
However `super(...)` also passes a block if no explicit block passing (like=
`super(){}` or `super(&b)`) are written.
I'm not sure we need to continue this strange specification, but to keep co=
mpatibility depending this spec, I add this rule.
(4). surprisingly, the following code invoke a block:
```
def foo
class << Object.new
yield
end
end
foo{ p :ok } #=3D> :ok
```
I'm also not sure we need to keep this spec, but to allow this spec, I adde=
d (4) rule.
Strictly speaking, it is not required, but we don't keep the link from sing=
leton class ISeq to lexical parent iseq now, so I added it.
## Exceptional cases
A method called by `super` doesn`t warn warning even if this method doesn't=
use a block.
The rule (3) can pass blocks easily and there are many methods don`t use a =
block.
So my patch ignores callings by `super`.
## corner cases
There are several cases to use block without (1)-(4) rules.
### `Proc.new/proc/lambda` without a block
Now it was deprecated in r66772 (commit:9f1fb0a17febc59356d58cef5e98db61a3c=
03550).
Related discussion: [Bug #15539]
### `block_given?`
`block_given?` expects block, but I believe we use it with `yield` or a blo=
ck parameter.
If you know the usecase without them, please tell us.
### `yield` in `eval`
We can't know `yield` (or (3), (4) rule) in an `eval` evaluating string at =
calling time.
```
def foo
eval('yield`)
end
foo{} # at calling time,
# we can't know the method foo can accept a block or not.
```
So I added a warning to use `yield` in `eval` like that: `test.rb:4: warnin=
g: use yield in eval will not be supported in Ruby 3.`
Workaround is use a block parameter explicitly.
```
def foo &b
eval('b.call')
end
foo{ p :ok }
```
# Implementation
Strategy is:
* [compile time] introduce `iseq::has_yield` field and check it if the iseq=
(or child iseq) contains `yield` (or something)
* [calling time] if block is given, check `iseq::has_yield` flag and show w=
arning (or raise an exception)
https://gist.github.com/ko1/c9148ad0224bf5befa3cc76ed2220c0b
On this patch, now it raises an error to make it easy to detect.
It is easy to switch to show the warning.
# Evaluation and discussion
I tried to avoid ruby's tests.
https://gist.github.com/ko1/37483e7940cdc4390bf8eb0001883786
Here is a patch.
There are several patterns to avoid warnings.
## tests for `block_given?`, `Proc.new` (and similar) without block
Add a dummy block parameter.
It is test-specific issue.
## empty `each`
Some tests add `each` methods do not `yield`, like: `def each; end`.
Maybe test-specific issue, and adding a dummy block parameter.
## Subtyping / duck typing
https://github.com/ruby/ruby/blob/c01a5ee85e2d6a7128cccafb143bfa694284ca87/=
lib/optparse.rb#L698
This `parse` method doesn't use `yield`, but other sub-type's `parse` metho=
ds use.
## `super` with `new` method
https://gist.github.com/ko1/37483e7940cdc4390bf8eb0001883786#file-tests-pat=
ch-L61
This method override `Class#new` method and introduce a hook with block (yi=
eld a block in this hook code).
https://github.com/ruby/ruby/blob/trunk/lib/rubygems/package/tar_writer.rb#=
L81
In this method, call `super` and it also passing a block. However, called `=
initialize` doesn't use a block.
## Change robustness
This change reduce robustness for API change.
`Delegator` requires to support `__getobj__` for client classes.
Now `__getobj__` should accept block but most of `__getobj__` clients do no=
t call given block.
https://github.com/ruby/ruby/blob/trunk/lib/delegate.rb#L80
This is because of delegator.rb's API change.
https://gist.github.com/ko1/37483e7940cdc4390bf8eb0001883786#file-tests-pat=
ch-L86
Nobu says calling block is not required (ignoring a block is no problem) so=
it is not a bug for delegator client classes.
## Found issues.
```
[ 2945/20449] Rinda::TestRingServer#test_do_reply =3D 0.00 s
1) Error:
Rinda::TestRingServer#test_do_reply:
ArgumentError: passing block to the method "with_timeout" (defined at /home=
/ko1/src/ruby/trunk/test/rinda/test_rinda.rb:787) is never used.
/home/ko1/src/ruby/trunk/test/rinda/test_rinda.rb:635:in `test_do_reply'
[ 2946/20449] Rinda::TestRingServer#test_do_reply_local =3D 0.00 s
2) Error:
Rinda::TestRingServer#test_do_reply_local:
ArgumentError: passing block to the method "with_timeout" (defined at /home=
/ko1/src/ruby/trunk/test/rinda/test_rinda.rb:787) is never used.
/home/ko1/src/ruby/trunk/test/rinda/test_rinda.rb:657:in `test_do_reply=
_local'
[10024/20449] TestGemRequestSetGemDependencyAPI#test_platform_mswin =3D 0.0=
1 s
3) Error:
TestGemRequestSetGemDependencyAPI#test_platform_mswin:
ArgumentError: passing block to the method "util_set_arch" (defined at /hom=
e/ko1/src/ruby/trunk/lib/rubygems/test_case.rb:1053) is never used.
/home/ko1/src/ruby/trunk/test/rubygems/test_gem_request_set_gem_depende=
ncy_api.rb:655:in `test_platform_mswin'
[10025/20449] TestGemRequestSetGemDependencyAPI#test_platforms =3D 0.01 s
4) Error:
TestGemRequestSetGemDependencyAPI#test_platforms:
ArgumentError: passing block to the method "util_set_arch" (defined at /hom=
e/ko1/src/ruby/trunk/lib/rubygems/test_case.rb:1053) is never used.
/home/ko1/src/ruby/trunk/test/rubygems/test_gem_request_set_gem_depende=
ncy_api.rb:711:in `test_platforms'
```
These 4 detection show the problem. `with_timeout` method (used in Rinda te=
st) and `util_set_arch` method (used in Rubygems test) simply ignore the gi=
ven block.
So these tests are simply ignored.
I reported them. (https://github.com/rubygems/rubygems/issues/2601)
## raise an error or show a warning?
At least, Ruby 2.7 should show warning for this kind of violation with `-w`.
How about for Ruby3?
--=20
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- [email protected]
To unsubscribe send an email to [email protected]
ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-c=
ore.ml.ruby-lang.org/