Finding the class name of an ancestor in the call stack in Ruby

2 months ago 10
ARTICLE AD BOX

I am trying write a method that interrogates the call stack to determine if it was called by a specific class. The caller in question would be dynamically generated (by ActiveRecord, as it happens), so I cannot rely on the file path in caller_locations. Basically, I need to replicate some of the output from the debugger backtrace command.

For a contrived example, consider the following code:

require 'debug' class Library def initialize @books = [] end def add_book(book) @books << book end def list_books @books.each do |book| puts book end end end class Book attr_reader :title, :author def initialize(title, author) @title = title @author = author author.add_book(self) end def to_s "#{title} by #{author}" end end class Author attr_reader :name def initialize(name) @name = name @books = [] end def add_book(book) @books << book end def to_s loc = caller_locations(1, 1)[0] debugger "#{name}" end end library = Library.new author = Author.new("Jane Austen") pnp = Book.new("Pride and Prejudice", author) emma = Book.new("Emma", author) library.add_book(pnp) library.add_book(emma) puts "Library catalog:\n" library.list_books

If I run this code, I can get what I want (that this method was called from class Book, or Library if I walk further) from the debugger, but not from caller_locations:

% ruby tmp/caller_example.rb (rdbg:/home/ubuntu/.rdbgrc) config set irb_console true irb_console = true # UI: Use IRB as the console (default: false) Library catalog: [42, 51] in tmp/caller_example.rb 42| @books << book 43| end 44| 45| def to_s 46| loc = caller_locations(1, 1)[0] => 47| debugger 48| "#{name}" 49| end 50| end 51| =>#0 Author#to_s at tmp/caller_example.rb:47 #1 Book#to_s at tmp/caller_example.rb:29 # and 6 frames (use `bt' command for all frames) irb:rdbg(Jane Austen):002> bt =>#0 Author#to_s at tmp/caller_example.rb:47 #1 Book#to_s at tmp/caller_example.rb:29 #2 [C] IO#puts at tmp/caller_example.rb:14 #3 [C] Kernel#puts at tmp/caller_example.rb:14 #4 block {|book=#<Book:0x00007f78d6d08698 @author=#<Autho...|} in list_books at tmp/caller_example.rb:14 #5 [C] Array#each at tmp/caller_example.rb:13 #6 Library#list_books at tmp/caller_example.rb:13 #7 <main> at tmp/caller_example.rb:63 irb:rdbg(Jane Austen):003> loc "tmp/caller_example.rb:29:in `to_s'" irb:rdbg(Jane Austen):004> (loc.methods - Object.methods).sort [:absolute_path, :base_label, :label, :lineno, :path] irb:rdbg(Jane Austen):005> (loc.methods - Object.methods).to_h {|m| [m, loc.send(m)]} {:label=>"to_s", :base_label=>"to_s", :path=>"tmp/caller_example.rb", :absolute_path=>"/home/[REDACTED]/tmp/caller_example.rb", :lineno=>29}

This example only pulls out the immediate (first) caller, but the real code will walk the call stack to look for a specific class.

I’ve looked at the binding_of_caller gem and debug_inspector gem, but I don’t really want to use them because:

Creating bindings for every caller down the stack seems rather expensive, especially when I don’t need the whole binding, just the class name. I’m going to be doing this a lot in a production environment. Hopefully only for a brief period of debugging, but I still need to be cognizant of the performance impact. I can’t believe I need to pull in a gem to get this information. (I will if I have to though.)

I’ve also taken a quick look at how the debugger gets this information but its internal API is a bit opaque. I figured I should ask here if anyone can short-circuit my journey down that rabbit hole before I actually dive in.

If binding_of_caller or debug_inspector is the only way forward, I might be able to mitigate the performance impact by walking the caller_locations stack and only getting Bindings for those that might be what I’m looking for.

Read Entire Article