Skip to content

Research

I love to research about codebase as data and prototyping ideas several times doesn't fit in simple shortcuts.

Here is my first research that worth sharing:

Combining Runtime metadata with AST complex searches

This example covers how to find RSpec allow combined with and_return missing the with clause specifying the nested parameters.

Here is the gist if you want to go straight and run it.

Scenario for simple example:

Given I have the following class:

class Account
  def withdraw(value)
    if @total >= value
      @total -= value
      :ok
    else
      :not_allowed
    end
  end
end

And I'm testing it with allow and some possibilities:

# bad
allow(Account).to receive(:withdraw).and_return(:ok)
# good
allow(Account).to receive(:withdraw).with(100).and_return(:ok)

Objective: find all bad cases of any class that does not respect the method parameters signature.

First, let's understand the method signature of a method:

Account.instance_method(:withdraw).parameters
# => [[:req, :value]]

Now, we can build a small script to use the node pattern to match the proper specs that are using such pattern and later visit their method signatures.

Fast.class_eval do
  # Captures class and method name when find syntax like:
  # `allow(...).to receive(...)` that does not end with `.with(...)`
  pattern_with_captures = <<~FAST
  (send (send nil allow (const nil $_)) to
    (send (send nil receive (sym $_)) !with))
  FAST

  pattern = expression(pattern_with_captures.tr('$',''))

  ruby_files_from('spec').each do |file|
    results = search_file(pattern, file) || [] rescue next
    results.each do |n|
      clazz, method = capture(n, pattern_with_captures)
      if klazz = Object.const_get(clazz.to_s) rescue nil
        if klazz.respond_to?(method)
          params = klazz.method(method).parameters
          if params.any?{|e|e.first == :req}
            code = n.loc.expression
            range = [code.first_line, code.last_line].uniq.join(",")
            boom_message = "BOOM! #{clazz}.#{method} does not include the REQUIRED parameters!"
            puts boom_message, "#{file}:#{range}", code.source
          end
        end
      end
    end
  end
end

Preload your environment before run the script

Keep in mind that you should run it with your environment preloaded otherwise it will skip the classes. You can add elses for const_get and respond_to and report weird cases if your environment is not preloading properly.