Finding where a method is defined in Ruby using IRB

Finding where a method is defined in Ruby using IRB

Tracing method definitions in Ruby via IRB: Clear and concise examples

ยท

10 min read

After many years in Ruby, I am still fascinated by the flexibility, simplicity, and power of IRB and rails console. Even with IDEs getting better, I like the experience of using IRB or rails console to explore the code.

In this article, I will show how to find the source where a method is defined.

There are three ways to find out where a method is defined in Ruby using IRB:

  • using the source_location defined on the Method object

  • using the IRB show_source command

  • displaying information about an object using ls (for displaying the object but not the source file)

Before we start, here is my IRB configuration:

# .irbrc content
# I use the Dracula theme for IRB from https://github.com/katsyoshi/irb-theme-dracula
require "irb/theme/dracula/light"

All code executed below is run inside irb.

A simple Ruby script

Let's assume the following object:

# frozen_string_literal: true

module Universe
  module Galaxy
    class Star
      attr_reader :name
      attr_writer :name

      def initialize(name)
        @name = name
      end
    end
  end
end

Now let's start IRB and instantiate an object out of it:

require_relative "universe/galaxy/star"
star = Universe::Galaxy::Star.new("Some Other Sun")

Finding available methods and instance variables

When in IRB you can use ls to display the available methods and instance variables:

ls star
# => 
# Universe::Galaxy::Star#methods: name  name=
# instance variables: @name

While this does not provide a source file and a line number you will see in examples below that it can correctly identify in case of inheritance for example where a method is defined.

Finding the source using Method#source_location

Executing star.method(:name).source_location will return the proper source location with the specific line number:

star.method(:name) # This will return a Method object
star.method(:name).source_location 

# => 
# ["universe/galaxy/star.rb", 6]

What about the setter? It will work in this case too:

star.method(:name=).source_location 
# => 
# ["universe/galaxy/star.rb", 7]

Finding the source using show_source from IRB

When in IRB you can use show_source to display the source for a method:

show_source star.name

The response will be:

From: universe/galaxy/star.rb:6

      attr_reader :name
      attr_writer :name

This will work for the setter too so executing the following command:

show_source star.name=

Will return the following response:

From: universe/galaxy/star.rb:7

      attr_writer :name

A more complex Ruby script

What if we add inheritance? How will discovering the source work in this case?
Let's now create a new class:

module Universe
  module Galaxy
    class SimpleSun < Star
      def initialize
        super('Sun')
      end

      def our_planet
        Planet.new('Earth')
      end

      def name
        puts "SimpleSun#{@name}"
      end
    end
  end
end

Let's load this in an IRB session:

require_relative "universe/galaxy/simple_sun"
simple_sun = Universe::Galaxy::SimpleSun.new

Finding available methods and instance variables

Let's explore with ls:

ls simple_sun
# => 
# Universe::Galaxy::SimpleSun#methods: name  our_planet
# Universe::Galaxy::Star#methods: name=
# instance variables: @name

Notice that it correctly showed that name is from Star and was not overwritten in the SimpleSun while name= is correctly identified as being from Universe::Galaxy::Star

Finding source location with Method#source_location

Using source_location on name will show the source being in simple_sun.rb while using source_location on name= will show the source being star.rb:

simple_sun.method(:name).source_location
# => 
# ["universe/galaxy/simple_sun.rb", 14]

simple_sun.method(:name=).source_location
# =>
# ["universe/galaxy/star.rb", 7]

Finding source location with show_source IRB command

Using show_source will correctly display the source code for both name and for name= (inherited from Star).

Executing:

show_source simple_sun.name

Will return the following response:

From: universe/galaxy/simple_sun.rb:14

      def name
        puts "SimpleSun#{@name}"
      end

While executing:

show_source simple_sun.name=

Will return the following response:

From: universe/galaxy/star.rb:7

      attr_writer :name

An extra option for show_source

Let's execute again the show_source but this time on the initializer where in SimpleSun I wrote super('Sun') so I am calling super with an argument:

show_source Universe::Galaxy::SimpleSun#initialize

This will return:

From: universe/galaxy/simple_sun.rb:6

      def initialize
        super('Sun')
      end

What if you want to see the super method? show_source supports a flag -s that will help displaying the super class method. You can stack as many -s as you like eg. -sssss

In our case there is one single super added there so I will execute:

show_source Universe::Galaxy::SimpleSun#initialize -s

And this will return:

From: universe/galaxy/star.rb:9

      def initialize(name)
        @name = name
      end

A Ruby script using define_method

Now let's complicate a bit the things and define methods dynamically with define_method:

require_relative "star"

module Universe
  module Galaxy
    class TwinSun < Star
      NAME_ALTERNATIVES = %i(twin_helios twin_sun twin_sol twin_solis)

      def initialize
        super('TwinSun')
      end

      def our_planet
        'Twin Earth'
      end

      NAME_ALTERNATIVES.each do |name|
        define_method "#{name}?" do
          true
        end

        define_method name do
          name
        end
      end
    end
  end
end

With our new class defined, let's start the IRB and instantiate twin_sun:

require_relative "universe/galaxy/twin_sun"
twin_sun = Universe::Galaxy::TwinSun.new

Finding available methods and instance variables

And we can explore with ls:

ls twin_sun
# => 
# Universe::Galaxy::TwinSun#methods:
#   our_planet  twin_helios  twin_helios?  twin_sol  twin_sol?  twin_solis  twin_solis?  twin_sun  twin_sun?
# Universe::Galaxy::Star#methods: name  name=
# instance variables: @name

You can notice that it shows all the defined method too when doing ls twin_sun.

ls also correctly identifies that name and name= are from the parent Star object.

Finding source location with Method#source_location

What about source_location?

Let's first try a method defined directly on the TwinSun and notice it shows the file and the line number:

twin_sun.method(:our_planet).source_location
# => 
# ["universe/galaxy/twin_sun.rb", 12]

Now let's try to identify one of the methods created with define_method:

twin_sun.method(:twin_sun?).source_location
# => 
# ["universe/galaxy/twin_sun.rb", 17]

And opening that directly from IRB with edit "universe/galaxy/twin_sun.rb" will show the exact line where define_method #{name}? is written:

Finding source location with show_source IRB command

If you want to directly get a glimpse in IRB of the source code, you can use show_source

show_source twin_sun.twin_sun?

The result will be:

From: universe/galaxy/twin_sun.rb:17

        define_method "#{name}?" do
          true
        end

This is super cool => it is showing the place where define_method is used to create the twin_sun? method:

Rails

I am using for this a newly generated Rails app (using Ruby 3.2.2 and Rails 7.1) where I just added two models. They are very simple:

# db/schema.db

create_table "galaxies", force: :cascade do |t|
  t.string "name"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

create_table "stars", force: :cascade do |t|
  t.string "name"
  t.integer "galaxy_id", null: false
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["galaxy_id"], name: "index_stars_on_galaxy_id"
end

add_foreign_key "stars", "galaxies"

# app/models/galaxy.rb

class Galaxy < ApplicationRecord
  has_many :stars
end

# app/models/star.rb 

class Star < ApplicationRecord
  belongs_to :galaxy
end

Let's start the Rails console with rails c and try to find the source code or location for some methods.

Assume there is already a record for Galaxy with name Andromeda and a couple of stars in that galaxy: Alpheratz and Mirach.

Finding using source_location

We might want to load the galaxy with name Andromeda by using Galaxy.find_by but where is that method defined?

Galaxy.method(:find_by).source_location
# => 
# [".gem/ruby/3.2.2/gems/activerecord-7.1.2/lib/active_record/core.rb", 256]

And if we open that file, we will get:

What about exists? from Active Record? That works too indicating where exists? is defined:

Galaxy.method(:exists?).source_location
# => 
# [".gem/ruby/3.2.2/gems/activerecord-7.1.2/lib/active_record/querying.rb", 23]

Finding the source using show_source

show_source Galaxy.find_by

The result of executing that line will be:

From: .gem/ruby/3.2.2/gems/activerecord-7.1.2/lib/active_record/core.rb:256

      def find_by(*args) # :nodoc:
        return super if scope_attributes?

        hash = args.first
        return super unless Hash === hash

        hash = hash.each_with_object({}) do |(key, value), h|

# ...

I only included here the first few lines from the find_by method but you can notice that it correctly displayed the source code.

Finding source code for params in the Rails controller

I generated a simple scaffolding for Galaxies and I will use the debug gem (included by default in Rails 7.1) to start an IRB session in the context of the index action:

# app/controllers/galaxies_controller.rb

class GalaxiesController < ApplicationController
  before_action :set_galaxy, only: %i[ show edit update destroy ]

  # GET /galaxies or /galaxies.json
  def index
    binding.irb
    @galaxies = Galaxy.all
  end

# ... more code

Using source_location

Knowing that params is a method (can be easily checked with self.methods.include?(:params) inside the action) I can do the following:

self.method(:params).source_location # works also without self
# => 

[".gem/ruby/3.2.2/gems/actionpack-7.1.2/lib/action_controller/metal/strong_parameters.rb", 1281]

Using show_source

show_source params
# => 

From: .gem/ruby/3.2.2/gems/actionpack-7.1.2/lib/action_controller/metal/strong_parameters.rb:1281

    def params
      @_params ||= begin
        context = {
          controller: self.class.name,
          action: action_name,
          request: request,
          params: request.filtered_parameters
        }
        Parameters.new(request.parameters, context)
      end
    end

Exploring params with ls

You can find out more information about params if you use ls command:

irb(#<GalaxiesController:0x000000...):004> ls params
ActionController::Parameters#methods:
  ==                    []               []=               always_permitted_parameters  always_permitted_parameters=  as_json      compact
  compact!              compact_blank    compact_blank!    converted_arrays             deep_dup                      deep_merge?  deep_transform_keys
  deep_transform_keys!  delete           delete_if         dig                          each                          each_key     each_pair
  each_value            empty?           encode_with       eql?                         except                        exclude?     extract!
  extract_value         fetch            has_key?          has_value?                   hash                          include?     init_with
  inspect               keep_if          key?              keys                         member?                       merge        merge!
  permit                permit!          permitted?        reject                       reject!                       require      required
  reverse_merge         reverse_merge!   select            select!                      slice                         slice!       stringify_keys
  to_h                  to_hash          to_param          to_query                     to_s                          to_unsafe_h  to_unsafe_hash
  transform_keys        transform_keys!  transform_values  transform_values!            value?                        values       values_at
  with_defaults         with_defaults!   without
ActiveSupport::DeepMergeable#methods: deep_merge  deep_merge!
instance variables: @logging_context  @parameters  @permitted
class variables:
  @@action_on_unpermitted_parameters  @@allow_deprecated_parameters_hash_equality  @@always_permitted_parameters  @@permit_all_parameters

When show_source and source_location does not work

When methods are defined with method_missing

In case you have your behavior defined with method_missing then source_location and show_source will not work:

# frozen_string_literal: true

module Universe
  module Galaxy
    class Sun < Star
      DYNAMIC_METHODS = %i(life?).freeze

      def initialize
        super('Sun')
      end

      def our_planet
        'Earth'
      end

      private

      def respond_to_missing?(name, include_private = false)
        DYNAMIC_METHODS.include?(name) || super
      end

      def method_missing(name, *args)
        return super unless DYNAMIC_METHODS.include?(name)

        if name == :life?
          return true
        end
      end
    end
  end
end

In this case if you load this into IRB:

require_relative "universe/galaxy/sun"
sun = Universe::Galaxy::Sun.new

You can notice that you can execute sun.life? and even check sun.respond_to?(:life?) :

sun.life? # => true
sun.respond_to?(:life?) # => true

You will then notice that life? is not a method when investigating with:

sun.methods.include?(:life?) # => false
sun.private_methods.include?(:life?) # => false
Universe::Galaxy::Sun.public_methods.include?(:life?) #  false
Universe::Galaxy::Sun.protected_methods.include?(:life?) # false
Universe::Galaxy::Sun.private_methods.include?(:life?) # false 
Universe::Galaxy::Sun.singleton_methods.include?(:life?) # false
Universe::Galaxy::Sun.instance_methods.include?(:life?) # false

You can still get the Method object for it:

sun.method(:life?)
# => #<Method: Universe::Galaxy::Sun#life?(*)>

But source_location or show_source will not work:

sun.method(:life?).source_location
# => nil

show_source sun.life?
# => Error: Couldn't locate a definition for sun.life?

It is also not displayed when using ls on the sun object:

ls sun
# => 
# Universe::Galaxy::Sun#methods: our_planet
# Universe::Galaxy::Star#methods: name  name=
# instance variables: @name

On the Kernel, BasicObject and Object methods

In general source_location and show_source will show you the Ruby code that defined a method, but will not show you any C code behind any method that you are using in Ruby.

So for example the following will not work:

# respond_to? is inherited from Object

sun.method(:respond_to?)
# => #<Method: Universe::Galaxy::Sun(Kernel)#respond_to?(*)>

show_source sun.respond_to?
# => Error: Couldn't locate a definition for sun.respond_to?

sun.method(:respond_to?).source_location
# => nil

# == is inherited from BasicObject 

sun.method(:==) 
# => #<Method: Universe::Galaxy::Sun(BasicObject)#==(_)>

sun.method(:==).source_location
# => nil

show_source sun.==
# Error: Couldn't locate a definition for sun.==

If you want to make this work you have to use Ruby TracePoint but that is out of the scope of this article.

Conclusion

I think Ruby has now very good tools to navigate to the source of a method either from the class or from the instantiated object. Using IRB and debug gem makes this navigation easy, and I would recommend anyone learning Ruby to keep an irb or rails console session open. That way, whenever you have a question about how some particular method works, you can navigate to the source of that method and read the code.


I would like to thank Stephen Margheim and Stan Lo for reviewing an early version of this article and providing valuable feedback.


Enjoyed this article?

๐Ÿ‘‰ Join my Short Ruby News newsletter for weekly Ruby updates from the community and visit rubyandrails.info, a directory with learning content about Ruby.

๐Ÿ‘ Subscribe to my Ruby and Ruby on rails courses over email at learn.shortruby.com - effortless learning anytime, anywhere

๐Ÿค Let's connect on Ruby.social or Linkedin or Twitter where I post mainly about Ruby and Rails.

๐ŸŽฅ Follow me on my YouTube channel for short videos about Ruby

Did you find this article valuable?

Support Lucian Ghinda by becoming a sponsor. Any amount is appreciated!