Skip to content

Commit

Permalink
show warning for unused block
Browse files Browse the repository at this point in the history
With verbopse mode (-w), the interpreter shows a warning if
a block is passed to a method which does not use the given block.

Warning on:

* the invoked method is written in C
* the invoked method is not `initialize`
* not invoked with `super`
* the first time on the call-site with the invoked method
  (`obj.foo{}` will be warned once if `foo` is same method)

`Primitive.attr! :use_block` is introduced to declare that primitive
functions (written in C) will use passed block.
  • Loading branch information
ko1 committed Mar 29, 2024
1 parent 38331c8 commit 85d536a
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 12 deletions.
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,14 @@ See GitHub releases like [GitHub Releases of Logger](https://github.com/ruby/log

## JIT

## Miscellaneous changes

* Passing a block to a method which doesn't use the passed block will show
a warning on verbose mode (`-w`).
[[Feature #15554]]

[Feature #13557]: https://bugs.ruby-lang.org/issues/13557
[Feature #15554]: https://bugs.ruby-lang.org/issues/15554
[Feature #16495]: https://bugs.ruby-lang.org/issues/16495
[Feature #18980]: https://bugs.ruby-lang.org/issues/18980
[Feature #19117]: https://bugs.ruby-lang.org/issues/19117
Expand Down
2 changes: 2 additions & 0 deletions array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class Array
# Related: #each_index, #reverse_each.
def each
Primitive.attr! :inline_block
Primitive.attr! :use_block

unless defined?(yield)
return Primitive.cexpr! 'SIZED_ENUMERATOR(self, 0, 0, ary_enum_length)'
end
Expand Down
14 changes: 13 additions & 1 deletion compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -2104,6 +2104,7 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons
if (block_id) {
body->param.block_start = arg_size++;
body->param.flags.has_block = TRUE;
body->param.flags.use_block = 1;
}

iseq_calc_param_size(iseq);
Expand Down Expand Up @@ -5942,6 +5943,7 @@ defined_expr0(rb_iseq_t *iseq, LINK_ANCHOR *const ret,
ADD_INSN(ret, line_node, putnil);
ADD_INSN3(ret, line_node, defined, INT2FIX(DEFINED_YIELD), 0,
PUSH_VAL(DEFINED_YIELD));
ISEQ_BODY(ISEQ_BODY(iseq)->local_iseq)->param.flags.use_block = 1;
return;

case NODE_BACK_REF:
Expand Down Expand Up @@ -8660,6 +8662,9 @@ compile_builtin_attr(rb_iseq_t *iseq, const NODE *node)
else if (strcmp(RSTRING_PTR(string), "inline_block") == 0) {
ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_INLINE_BLOCK;
}
else if (strcmp(RSTRING_PTR(string), "use_block") == 0) {
ISEQ_BODY(iseq)->param.flags.use_block = 1;
}
else {
goto unknown_arg;
}
Expand Down Expand Up @@ -9513,6 +9518,8 @@ compile_super(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i
}
else {
/* NODE_ZSUPER */
ISEQ_BODY(ISEQ_BODY(iseq)->local_iseq)->param.flags.use_block = 1;

int i;
const rb_iseq_t *liseq = body->local_iseq;
const struct rb_iseq_constant_body *const local_body = ISEQ_BODY(liseq);
Expand Down Expand Up @@ -9646,6 +9653,7 @@ compile_yield(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i

ADD_SEQ(ret, args);
ADD_INSN1(ret, node, invokeblock, new_callinfo(iseq, 0, FIX2INT(argc), flag, keywords, FALSE));
ISEQ_BODY(ISEQ_BODY(iseq)->local_iseq)->param.flags.use_block = 1;

if (popped) {
ADD_INSN(ret, node, pop);
Expand Down Expand Up @@ -12732,7 +12740,10 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq)
(body->param.flags.has_block << 6) |
(body->param.flags.ambiguous_param0 << 7) |
(body->param.flags.accepts_no_kwarg << 8) |
(body->param.flags.ruby2_keywords << 9);
(body->param.flags.ruby2_keywords << 9) |
(body->param.flags.anon_rest << 10) |
(body->param.flags.anon_kwrest << 11) |
(body->param.flags.use_block << 12);

#if IBF_ISEQ_ENABLE_LOCAL_BUFFER
# define IBF_BODY_OFFSET(x) (x)
Expand Down Expand Up @@ -12948,6 +12959,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset)
load_body->param.flags.ruby2_keywords = (param_flags >> 9) & 1;
load_body->param.flags.anon_rest = (param_flags >> 10) & 1;
load_body->param.flags.anon_kwrest = (param_flags >> 11) & 1;
load_body->param.flags.use_block = (param_flags >> 12) & 1;
load_body->param.size = param_size;
load_body->param.lead_num = param_lead_num;
load_body->param.opt_num = param_opt_num;
Expand Down
1 change: 1 addition & 0 deletions dir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ def self.[](*args, base: nil, sort: true)
# specifies that patterns may match short names if they exist; Windows only.
#
def self.glob(pattern, _flags = 0, flags: _flags, base: nil, sort: true)
Primitive.attr! :use_block
Primitive.dir_s_glob(pattern, flags, base, sort)
end
end
Expand Down
5 changes: 5 additions & 0 deletions iseq.c
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@ iseq_location_setup(rb_iseq_t *iseq, VALUE name, VALUE path, VALUE realpath, int
RB_OBJ_WRITE(iseq, &loc->label, name);
RB_OBJ_WRITE(iseq, &loc->base_label, name);
loc->first_lineno = first_lineno;

if (ISEQ_BODY(iseq)->local_iseq == iseq && strcmp(RSTRING_PTR(name), "initialize") == 0) {
ISEQ_BODY(iseq)->param.flags.use_block = 1;
}

if (code_location) {
loc->node_id = node_id;
loc->code_location = *code_location;
Expand Down
4 changes: 2 additions & 2 deletions lib/optparse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ def self.candidate(key, icase = false, pat = nil, &block)
candidates
end

def candidate(key, icase = false, pat = nil)
def candidate(key, icase = false, pat = nil, &)
Completion.candidate(key, icase, pat, &method(:each))
end

Expand Down Expand Up @@ -739,7 +739,7 @@ class RequiredArgument < self
#
# Raises an exception if argument is not present.
#
def parse(arg, argv)
def parse(arg, argv, &)
unless arg
raise MissingArgument if argv.empty?
arg = argv.shift
Expand Down
2 changes: 1 addition & 1 deletion lib/rdoc/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ def display(method_attr) # :nodoc:
# This method exists to make it easy to work with Context subclasses that
# aren't part of RDoc.

def each_ancestor # :nodoc:
def each_ancestor(&) # :nodoc:
end

##
Expand Down
1 change: 1 addition & 0 deletions pack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class String
# returns that array.
# See {Packed Data}[rdoc-ref:packed_data.rdoc].
def unpack(fmt, offset: 0)
Primitive.attr! :use_block
Primitive.pack_unpack(fmt, offset)
end

Expand Down
59 changes: 59 additions & 0 deletions test/ruby/test_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1623,4 +1623,63 @@ def test_kwarg_eval_memory_leak
end
RUBY
end

def test_warn_unused_block
assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status|
def foo = nil
foo{} # warn
send(:foo){} # warn
b = Proc.new{}
foo(&b) # warn
RUBY
assert_equal 3, err.size
err = err.join
assert_match /-:2: warning/, err
assert_match /-:3: warning/, err
assert_match /-:5: warning/, err
end

assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status|
def foo = nil
10.times{foo{}} # warn once
RUBY
assert_equal 1, err.size
end

assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status|
def foo = nil; b = nil
foo(&b) # no warning
1.object_id{} # no warning because it is written in C
class C
def initialize
end
end
C.new{} # no warning
RUBY
assert_equal 0, err.size
end

assert_in_out_err '-w', <<-'RUBY' do |_out, err, _status|
class C0
def foo = nil
def bar = nil
def baz = nil
end
class C1 < C0
def foo = super
def bar = super()
def baz(&_) = super(&_)
end
C1.new.foo{} # no warning
C1.new.bar{} # warning
C1.new.baz{} # no warning
RUBY
assert_equal 1, err.size
assert_match /-:14: warning.+bar/, err.join
end
end
end
2 changes: 1 addition & 1 deletion test/ruby/test_optimization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ def test_string_freeze_block
begin;
class String
undef freeze
def freeze
def freeze(&)
block_given?
end
end
Expand Down
2 changes: 1 addition & 1 deletion tool/mk_builtin_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

SUBLIBS = {}
REQUIRED = {}
BUILTIN_ATTRS = %w[leaf inline_block]
BUILTIN_ATTRS = %w[leaf inline_block use_block]

def string_literal(lit, str = [])
while lit
Expand Down
2 changes: 1 addition & 1 deletion tool/test/testunit/test_parallel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def test_done

result = Marshal.load($1.chomp.unpack1("m"))
assert_equal(5, result[0])
pend "TODO: result[1] returns 17. We should investigate it" do
pend "TODO: result[1] returns 17. We should investigate it" do # TODO: misusage of pend (pend doens't use given block)
assert_equal(12, result[1])
end
assert_kind_of(Array,result[2])
Expand Down
5 changes: 5 additions & 0 deletions trace_point.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class TracePoint
# Access from other threads is also forbidden.
#
def self.new(*events)
Primitive.attr! :use_block
Primitive.tracepoint_new_s(events)
end

Expand Down Expand Up @@ -131,6 +132,7 @@ def self.stat
# trace.enabled? #=> true
#
def self.trace(*events)
Primitive.attr! :use_block
Primitive.tracepoint_trace_s(events)
end

Expand Down Expand Up @@ -196,6 +198,7 @@ def self.trace(*events)
# out calls by itself from :line handler, otherwise it will call itself infinitely).
#
def self.allow_reentry
Primitive.attr! :use_block
Primitive.tracepoint_allow_reentry
end

Expand Down Expand Up @@ -258,6 +261,7 @@ def self.allow_reentry
# #=> RuntimeError: access from outside
#
def enable(target: nil, target_line: nil, target_thread: :default)
Primitive.attr! :use_block
Primitive.tracepoint_enable_m(target, target_line, target_thread)
end

Expand Down Expand Up @@ -294,6 +298,7 @@ def enable(target: nil, target_line: nil, target_thread: :default)
# trace.disable { p tp.lineno }
# #=> RuntimeError: access from outside
def disable
Primitive.attr! :use_block
Primitive.tracepoint_disable_m
end

Expand Down
10 changes: 5 additions & 5 deletions vm_backtrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ location_lineno_m(VALUE self)

VALUE rb_mod_name0(VALUE klass, bool *permanent);

static VALUE
gen_method_name(VALUE owner, VALUE name)
VALUE
rb_gen_method_name(VALUE owner, VALUE name)
{
bool permanent;
if (RB_TYPE_P(owner, T_CLASS) || RB_TYPE_P(owner, T_MODULE)) {
Expand Down Expand Up @@ -235,7 +235,7 @@ calculate_iseq_label(VALUE owner, const rb_iseq_t *iseq)
case ISEQ_TYPE_MAIN:
return ISEQ_BODY(iseq)->location.label;
case ISEQ_TYPE_METHOD:
return gen_method_name(owner, ISEQ_BODY(iseq)->location.label);
return rb_gen_method_name(owner, ISEQ_BODY(iseq)->location.label);
case ISEQ_TYPE_BLOCK:
case ISEQ_TYPE_PLAIN: {
int level = 0;
Expand Down Expand Up @@ -269,7 +269,7 @@ static VALUE
location_label(rb_backtrace_location_t *loc)
{
if (loc->cme && loc->cme->def->type == VM_METHOD_TYPE_CFUNC) {
return gen_method_name(loc->cme->owner, rb_id2str(loc->cme->def->original_id));
return rb_gen_method_name(loc->cme->owner, rb_id2str(loc->cme->def->original_id));
}
else {
VALUE owner = Qnil;
Expand Down Expand Up @@ -457,7 +457,7 @@ location_to_str(rb_backtrace_location_t *loc)
file = GET_VM()->progname;
lineno = 0;
}
name = gen_method_name(loc->cme->owner, rb_id2str(loc->cme->def->original_id));
name = rb_gen_method_name(loc->cme->owner, rb_id2str(loc->cme->def->original_id));
}
else {
file = rb_iseq_path(loc->iseq);
Expand Down
1 change: 1 addition & 0 deletions vm_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ struct rb_iseq_constant_body {
unsigned int ruby2_keywords: 1;
unsigned int anon_rest: 1;
unsigned int anon_kwrest: 1;
unsigned int use_block: 1;
} flags;

unsigned int size;
Expand Down
57 changes: 57 additions & 0 deletions vm_insnhelper.c
Original file line number Diff line number Diff line change
Expand Up @@ -2966,6 +2966,57 @@ vm_call_single_noarg_leaf_builtin(rb_execution_context_t *ec, rb_control_frame_t
return builtin_invoker0(ec, calling->recv, NULL, (rb_insn_func_t)bf->func_ptr);
}

VALUE rb_gen_method_name(VALUE owner, VALUE name); // in vm_backtrace.c

static void
warn_unused_block(const rb_callable_method_entry_t *cme, const rb_iseq_t *iseq, void *pc)
{
static st_table *dup_check_table = NULL;

st_data_t key = 0;
union {
VALUE v;
unsigned char b[SIZEOF_VALUE];
} k1 = {
.v = (VALUE)pc,
}, k2 = {
.v = (VALUE)cme->def,
};

// make unique key from pc and me->def pointer
for (int i=0; i<SIZEOF_VALUE; i++) {
// fprintf(stderr, "k1:%3d k2:%3d\n", k1.b[i], k2.b[SIZEOF_VALUE-1-i]);
key |= (st_data_t)(k1.b[i] ^ k2.b[SIZEOF_VALUE-1-i]) << (8 * i);
}

if (0) {
fprintf(stderr, "SIZEOF_VALUE:%d\n", SIZEOF_VALUE);
fprintf(stderr, "pc:%p def:%p\n", pc, cme->def);
fprintf(stderr, "key:%p\n", (void *)key);
}

if (!dup_check_table) {
dup_check_table = st_init_numtable();
}

// duplication check
if (st_insert(dup_check_table, key, 1)) {
// already shown
}
else {
VALUE m_loc = rb_method_entry_location((const rb_method_entry_t *)cme);
VALUE name = rb_gen_method_name(cme->defined_class, ISEQ_BODY(iseq)->location.base_label);

if (!NIL_P(m_loc)) {
rb_warning("the passed block for '%"PRIsVALUE"' defined at %"PRIsVALUE":%"PRIsVALUE" may be ignored",
name, RARRAY_AREF(m_loc, 0), RARRAY_AREF(m_loc, 1));
}
else {
rb_warning("the block may be ignored because '%"PRIsVALUE"' does not use a block", name);
}
}
}

static inline int
vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling,
const rb_iseq_t *iseq, VALUE *argv, int param_size, int local_size)
Expand All @@ -2974,6 +3025,12 @@ vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling,
const struct rb_callcache *cc = calling->cc;
bool cacheable_ci = vm_ci_markable(ci);

if (UNLIKELY(!ISEQ_BODY(iseq)->param.flags.use_block &&
calling->block_handler != VM_BLOCK_HANDLER_NONE &&
!(vm_ci_flag(calling->cd->ci) & VM_CALL_SUPER))) {
warn_unused_block(vm_cc_cme(cc), iseq, (void *)ec->cfp->pc);
}

if (LIKELY(!(vm_ci_flag(ci) & VM_CALL_KW_SPLAT))) {
if (LIKELY(rb_simple_iseq_p(iseq))) {
rb_control_frame_t *cfp = ec->cfp;
Expand Down

0 comments on commit 85d536a

Please sign in to comment.