views:

45

answers:

2

How can I determine easily and programmatically whether a LocalJumpError arose from the caller's immediate failure to supply a needed block to a method, or from deeper within that method and others it invokes?

By "easily," I mean I'd like to avoid string inspection/regexen on $!.backtrace. A solution applicable to 1.8 and 1.9 is also preferred.

Motivation: When I botch a method call in ruby, it's usually because I mistyped the method (NoMethodError), got the number of arguments wrong (ArgumentError) or neglected to pass a necessary block (LocalJumpError).

For a proxying or decorating wrapper object in ruby, I'd like to discriminate these caller or API errors from the implementor or environment errors that can raise the same classes of error. For example:

...
def method_missing(sym, *args, &block)
  @wrapped.__send__(sym, *args, &block)
rescue NoMethodError
  raise MyApp::BadInvocation, "duh - no such method" unless @wrapped.respond_to?(sym)
  raise
rescue ArgumentError
  raise MyApp::BadInvocation, "duh - wrong arg count" \
    unless _args_fit_arity?(@wrapped.method(sym), args)
  raise
rescue LocalJumpError
  # XXX - what is the test?
  raise
end
+1  A: 

To find out whether the LocalJumpError was caused by the user forgetting to pass a block, you need to know two things: Whether the user supplied a block and whether the method needs a block. The first is easy: just check whether blk is nil. The second however is impossible (in plain ruby at least).

So I guess parsing the stack trace is your best bet.

sepp2k
+1 It had not occurred to me to check whether a block was actually supplied. (Still don't like parsing the backtrace *per se*, however.)
pilcrow
@sepp2k, I think I found a better bet ... will post in a moment.
pilcrow
@sepp2k, I think I win for strict accuracy, but you win for simplicity, which is a better criterion here. :)
pilcrow
A: 

One can examine the relative depth of the backtrace to make a best effort discriminate caller error from a subsequent error deeper in the call stack:

def lje_depth_from_send
  Class.new { def lje; yield end }.new.__send__ :lje
rescue LocalJumpError
  return $!.backtrace.size - caller(0).size
end

def method_missing(sym, *args, &block)
  ...
rescue LocalJumpError
  if !block_given? and ($!.backtrace.size - caller(0).size) == lje_depth_from_send
    raise MyApp::BadInvocation, "duh - you forgot to supply a block"
  end
  raise
end

Interestingly, this relative depth calculation changes from MRI 1.8 to MRI 1.9 -- the former tracks send, the latter seems to silently omit it (ala perl's goto &sub, perhaps?), for example. (Under 1.9, the LJE backtrace is shallower than the caller(0) stack, because 1.9 explicitly counts the rescue block as a discrete stack frame).

Now, this might not work under non-MRI, but I doubt parsing of the call stack would be portable from one implementation to another, either.

pilcrow