Paul Danelli | Experienced Ruby Developer

Debugging in Ruby using 'method' and 'methods'

The methods method

The problem; My memory is not great and I can’t remember the name of every method defined on an instance

I was trying to work out the cause of the exception below and because of all the rescue calls built into DD Trace, ActiveAdmin, Rails etc, the output was being suppressed. So, I managed to find a place in the code where I could see the exception, but wanted to see all of the stacktrace output.

# An error occured here:
[128, 137] in ./ruby/2.7.2/lib/ruby/gems/2.7.0/gems/ddtrace-0.54.2/lib/ddtrace/contrib/action_pack/action_controller/instrumentation.rb
   128:                 status = datadog_response_status
   129:                 payload[:status] = status unless status.nil?
   130:                 result
   131:               # rubocop:disable Lint/RescueException
   132:               rescue Exception => e
=> 133:                 payload[:exception] = [e.class.name, e.message]
   134:                 payload[:exception_object] = e
   135:                 raise e
   136:               end
   137:             # rubocop:enable Lint/RescueException

# My error from the exception
$> e

<NoMethodError: undefined method `times' for 2.0:Float>

I can never remember what the method is called, so my first guess was…

$> e.trace

*** NoMethodError Exception: undefined method `trace' for #<NoMethodError: undefined method `times' for 2.0:Float>

Obviously thats wrong, so a totally random second guess:

$> e.stacktrace

*** NoMethodError Exception: undefined method `stacktrace' for #<NoMethodError: undefined method `times' for 2.0:Float>
Did you mean?  set_backtrace

That didn’t work and the suggested method wasn’t what I wanted either - now I’ve now decided that guessing isn’t helping. Luckily there are more logical approaches, like using Ruby’s methods method, which shows all methods on the class or instance (depending on what you call it on, there is an instance method as well as a class method):

methods gives us an array of symbols, ordered as they were included in the code, but its still an array, so we can sort it:

$> e.methods.sort

[:!, :!=, :!~, :<=>, :==, :===, :=~, :__bb_context, :__binding__, :__id__,
:__send__, :acts_like?, :args, :as_json, :backtrace, :backtrace_locations,
:blame_file!, :blamed_files, :blank?, :bullet_key, :bullet_primary_key_value,
:byebug, [...], :to_s, :to_yaml, :trust, :try, :try!, :unfriendly_id?,
:unloadable, :untaint, :untrust, :untrusted?, :with_options, :yield_self]

If a class includes a lot of modules, methods can return an unhelpfully large array of unsorted method names - but its still an array, so we also can search for similar method names:

$> e.methods.sort.grep /trace/

[:backtrace, :backtrace_locations, :set_backtrace]

Finally, we have the method that we were looking for… and I should probably have known that. Obviously, the array that backtrace returns is super long and mostly unhelpful, so I’ve shortened it below for the sake of brevity, but you can also use grep on the array values to do the same:

$> e.backtrace

["./api/app/helpers/collections/replacer.rb:35:in `create_adhoc_replacements!'", "./api/app/controllers/api/v2/collections_controller.rb:155:in `block in paused_replacements'", [...], ]

We can make the output slightly more useful by creating a string and printing that to the screen:

$> puts e.backtrace.join("\r\n")
./api/app/helpers/collections/replacer.rb:35:in `create_adhoc_replacements!'
./api/app/controllers/api/v2/collections_controller.rb:155:in `block in paused_replacements'
# ...

And with that, I now know the issue I have and can start to resolve it.

The method method

The problem; It’s annoying having to Google for method information online.

I was recently working with some dates and wanted to know what exactly the since method did - luckily Ruby provides a handy way of viewing source code for exactly this reason.

Below is the original code snippet I wanted to understand:

$> first_adhoc_collection = 1.week.since(final_collection)

Fri, 24 Jun 2022 07:52:25.000000000 UTC +00:00

I can get the class name, which means I could Google for the documentation:

$> 1.week.class

ActiveSupport::Duration

But, I would need to try and find the method and make sure I’m looking at the correct version of Rails, or I could use method.

method returns an instance of Method, which encapsulates all of the method’s information, including its location on disk, the line number and the method body. It takes a symbol as an argument, so it would be called like so:

$> 1.week.method(:since)

<Method: ActiveSupport::Duration#since(time=...) ./.asdf/installs/ruby/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.5.1/lib/active_support/duration.rb:409>

To see what other information is available to use, we can check the public_methods on the object:

# `false` prevents us from seeing inherited methods
$> 1.week.method(:since).public_methods(false)

=> [:clone, :original_name, :<<, :>>, :to_proc, :==, :===, :to_s, :receiver, :[], :call, :eql?, :name, :inspect, :duplicable?, :arity, :curry, :source_location, :parameters, :hash, :owner, :unbind, :super_method]

So, I could call source_location and see exactly where it is…

$> 1.week.method(:since).source_location

["./.asdf/installs/ruby/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.1.5.1/lib/active_support/duration.rb", 409]

Or I could just output the methods body and read through it using the source method:

$> puts 1.week.method(:since).source

def since(time = ::Time.current)
  sum(1, time)
end

This helpful method isn’t included in the list above, however since method is available on every instance, we can call it on the Method instance we had returned and see that the source method is defined elsewhere:

$> 1.week.method(:since).method(:source)
=> #<Method: Method(MethodSource::MethodExtensions)#source() ./.asdf/installs/ruby/2.7.2/lib/ruby/gems/2.7.0/gems/method_source-1.0.0/lib/method_source.rb:109>

So, back to the problem at hand; From the since method, we see it calls sum and so we need more information. So, method to the rescue again…

$> puts 1.week.method(:sum).source

def sum(sign, time = ::Time.current)
  unless time.acts_like?(:time) || time.acts_like?(:date)
    raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
  end

  if parts.empty?
    time.since(sign * value)
  else
    parts.inject(time) do |t, (type, number)|
      if type == :seconds
        t.since(sign * number)
      elsif type == :minutes
        t.since(sign * number * 60)
      elsif type == :hours
        t.since(sign * number * 3600)
      else
        t.advance(type => sign * number)
      end
    end
  end
end

But calling method to get back each method that is referenced seems like hard work, so an alternative is to just output the file entirely:

$> puts File.read(1.week.method(:sum).source_location.first)

# frozen_string_literal: true

require "active_support/core_ext/array/conversions"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/object/acts_like"
require "active_support/core_ext/string/filters"

module ActiveSupport
  # Provides accurate date and time measurements using Date#advance and
  # Time#advance, respectively. It mainly supports the methods on Numeric.
  #
  #   1.month.ago       # equivalent to Time.now.advance(months: -1)
  class Duration
    class Scalar < Numeric #:nodoc:
      attr_reader :value
      delegate :to_i, :to_f, :to_s, to: :value

      def initialize(value)
        @value = value
      end

      def coerce(other)
        [Scalar.new(other), self]
      end

      def since(time = ::Time.current)
        sum(1, time)
      end

# ... Removed for brevity

      private

      def sum(sign, time = ::Time.current)
        unless time.acts_like?(:time) || time.acts_like?(:date)
          raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
        end

        if parts.empty?
          time.since(sign * value)
        else
          parts.inject(time) do |t, (type, number)|
            if type == :seconds
              t.since(sign * number)
            elsif type == :minutes
              t.since(sign * number * 60)
            elsif type == :hours
              t.since(sign * number * 3600)
            else
              t.advance(type => sign * number)
            end
          end
        end
      end
    end
  end
end

Ruby is a beautifully flexible language that allows you to monkey patch methods, include modules and use inheritance to extend your classes. However, with this flexibility it can quickly become hard to work out exactly what is being executed on the server. Thankfully, it also provides mechanisms for us to work that out, like method and methods.