Search code examples
ruby-on-railsrubyruby-on-rails-2ruby-2.3

How can I change this super call to pass the arguments explicitly?


I'm running a legacy Rails 2.3 (yes, I know) application on a new VM. It's running Ruby v2.3.1. I've made all of the recommended changes to get this old app running on this version. Everything runs great, and much faster, except for this code. This code is from the old Advanced Recipes for Rails book and is error_handling_form_builder.rb.

First, here's the form declaration:

<% form_for(:review, :builder => ErrorHandlingFormBuilder) do |f| %>
    <%= f.collection_select :system_id, @systems, :id, :name, { :include_blank => "Select a Standard"}, { :onchange => remote_function(:url => { :action => :makes }, :submit => :text, :method => 'post') } %>
    <div id="categoriesdiv">
    </div>
<% end %>

This code works fine if I remove the builder param. Here's the code from ErrorHandlingFormBuilder:

helpers = field_helpers +
          %w(date_select datetime_select calendar_date_select time_select collection_select) +
          %w(collection_select select country_select time_zone_select) -
          %w(label fields_for)

helpers.each do |name|
  # We don't want to have a label for a hidden field
  next if name=="hidden_field"
  define_method name do |field, *args|
    options = args.detect {|argument| argument.is_a?(Hash)} || {}
    build_shell(field, options) do
      super # This call fails
    end
  end
end

The call to super above fails with following error:

implicit argument passing of super from method defined by define_method()
is not supported. Specify all arguments explicitly.

Looking at the code, I'm not sure how to change it.

I know I have to call super with the explicit arguments, but I tried this:

super(field, options)

and I get:

wrong number of arguments (given 2, expected 4..6)

I also tried:

build_shell(field, options) do
  super(*args)
end

but I get:

undefined method `map' for :id:Symbol
Did you mean?  tap

It seems like it's looking for the collection attribute 2nd in the list of *args. But it's first. If i pass field as the first argument though, I get a Stack Level Too Deep.


Solution

  • super without an argument list resends the current message, starting message dispatch at the superclass, passing along the exact same arguments that were passed to the current method.

    In your case, the arguments being passed to the current method are (field, *args), so super without an argument list would pass along (field, *args), but unfortunately super without an argument list doesn't work inside define_method, so you have to pass along the arguments explicitly using super(field, *args).

    This will call the method with the name given by name in the superclass passing along (field, *args) just as if you had called super without an argument list.

    Looking at the code, I'm not sure how to change it.

    Simple: since super without an argument list implicitly passes along the arguments exactly as they were passed, the explicit equivalent to that is to just copy&paste the parameter list from the method definition into the argument list of super. In this case the method definition is define_method(name) do |field, *args| … end, so the parameter list is (field, *args), and you just copy that into the argument list for super: super(field, *args).

    It may or may not be possible (and desirable) to pass different arguments, depending on how the method is designed, and how you process the arguments, but this always works. (Or, put it another way: if this doesn't work, then the original code using just super also doesn't work. The only exception is when you want to pass along a block and you don't capture that block into a Proc, you will have to modify the method signature to add an &blk parameter.)