Project

General

Profile

Actions

Feature #21543

open

Point ArgumentError to the call site

Added by mame (Yusuke Endoh) about 3 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:122964]

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

Actions

Also available in: Atom PDF

Like0