Feature #21543
openPoint ArgumentError to the call site
Description
Consider this buggy code:
def foo(x, y)
end
foo(1)
The resulting error is:
$ ruby test.rb
test.rb:1:in `foo': wrong number of arguments (given 1, expected 2) (ArgumentError)
from test.rb:4:in `<main>'
I want this to be:
$ ruby test.rb
test.rb:4:in `<main>': wrong number of arguments (given 1, expected 2) (ArgumentError)
for `foo' at test.rb:1
Problem¶
In my experience, the code that needs fixing is almost always the caller, not the callee. In the above case, one would change foo(1)
to something like foo(1, 2)
.
My typical debugging workflow starts with opening the first location shown in the backtrace. This takes me to the def
line of the method. However, I can barely remember a single time I've ever had to fix the method definition itself in this scenario. I often find myself sighing, going back to the backtrace, finding the second location (the caller), and opening that file instead.
I think this inconvenience is a significant loss of productivity.
I understand that the current behavior is designed because developers need to check the callee (the method definition) to see what arguments are required for the fix.
However, I believe the primary location in a backtrace should point to the code that is most likely to need modification. There are cases where developers can fix the call site without re-checking the method definition. Even when they do need to check the callee to recall the expected arguments, it feels more natural to first understand "what arguments am I currently passing?" at the call site before asking "what arguments should I be passing?". This workflow makes it easier to spot the mistake.
Proposal¶
I propose that the backtrace for an ArgumentError should point to the caller's location (the call site) as shown in the above. The location of the callee (the method definition) could instead be included within the error message itself.
For a longer backtrace, the output would look like this:
class TestClass
def foo(x, y)
end
def bar
foo(1)
end
def main
bar
end
end
TestClass.new.main
$ ruby test.rb
test.rb:6:in 'TestClass#bar': wrong number of arguments (given 1, expected 2) (ArgumentError)
for TestClass#foo at test.rb:2
from test.rb:10:in 'TestClass#main'
from test.rb:14:in '<main>'
What do you think of this proposal? I have attached a proof-of-concept patch, though it will require more work before it is merge-ready.
diff --git a/vm_args.c b/vm_args.c
index 44be6f54c5..986cb3e4c1 100644
--- a/vm_args.c
+++ b/vm_args.c
@@ -980,17 +980,7 @@ raise_argument_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb
{
VALUE at;
- if (iseq) {
- vm_push_frame(ec, iseq, VM_FRAME_MAGIC_DUMMY | VM_ENV_FLAG_LOCAL, Qnil /* self */,
- VM_BLOCK_HANDLER_NONE /* specval*/, (VALUE) cme /* me or cref */,
- ISEQ_BODY(iseq)->iseq_encoded,
- ec->cfp->sp, 0, 0 /* stack_max */);
- at = rb_ec_backtrace_object(ec);
- rb_vm_pop_frame(ec);
- }
- else {
- at = rb_ec_backtrace_object(ec);
- }
+ at = rb_ec_backtrace_object(ec);
rb_ivar_set(exc, idBt_locations, at);
rb_exc_set_backtrace(exc, at);
@@ -1000,7 +990,15 @@ raise_argument_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb
static void
argument_arity_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb_callable_method_entry_t *cme, const int miss_argc, const int min_argc, const int max_argc)
{
- VALUE exc = rb_arity_error_new(miss_argc, min_argc, max_argc);
+ VALUE rb_gen_method_name(VALUE owner, VALUE name);
+ VALUE mname = rb_gen_method_name(cme->owner, rb_id2str(cme->def->original_id));
+ const rb_iseq_t *miseq = cme->def->body.iseq.iseqptr;
+ VALUE file = Qfalse, lineno = 0;
+ if (miseq) {
+ file = rb_iseq_path(miseq);
+ lineno = rb_iseq_first_lineno(miseq);
+ }
+ VALUE exc = rb_arity_error_new(miss_argc, min_argc, max_argc, mname, file, lineno);
if (ISEQ_BODY(iseq)->param.flags.has_kw) {
const struct rb_iseq_param_keyword *const kw = ISEQ_BODY(iseq)->param.keyword;
const ID *keywords = kw->table;
diff --git a/vm_insnhelper.c b/vm_insnhelper.c
index 3aca1bc24f..a5ad2f0562 100644
--- a/vm_insnhelper.c
+++ b/vm_insnhelper.c
@@ -484,7 +484,7 @@ rb_vm_push_frame_fname(rb_execution_context_t *ec, VALUE fname)
/* method dispatch */
static inline VALUE
-rb_arity_error_new(int argc, int min, int max)
+rb_arity_error_new(int argc, int min, int max, VALUE mname, VALUE file, VALUE lineno)
{
VALUE err_mess = rb_sprintf("wrong number of arguments (given %d, expected %d", argc, min);
if (min == max) {
@@ -497,13 +497,19 @@ rb_arity_error_new(int argc, int min, int max)
rb_str_catf(err_mess, "..%d", max);
}
rb_str_cat_cstr(err_mess, ")");
+ if (mname) {
+ rb_str_catf(err_mess, "\nfor %"PRIsVALUE, mname);
+ if (file) {
+ rb_str_catf(err_mess, " at %"PRIsVALUE":%"PRIsVALUE, file, lineno);
+ }
+ }
return rb_exc_new3(rb_eArgError, err_mess);
}
void
rb_error_arity(int argc, int min, int max)
{
- rb_exc_raise(rb_arity_error_new(argc, min, max));
+ rb_exc_raise(rb_arity_error_new(argc, min, max, Qfalse, Qfalse, INT2FIX(0)));
}
/* lvar */
No data to display