rubypluginsaliasredminealias-method

Improving on hacky ruby 'method_alias' fix for conflicting redmine plugins?


Using redmine 3.x, I have a dependency conflict between two plugins - redmine subtask list accordion and Subtask list inherited fields. Installing both raises 500 errors when trying to view an issue.

    ActionView::Template::Error (undefined method `sla_has_grandson_issues?' for #<#<Class:0x000056319e27d668>:0x00007f237ad02588>):
    1: <% if sla_has_grandson_issues?(@issue) %>
    2:   <%= content_for :header_tags do
    3:     stylesheet_link_tag(sla_use_css, :plugin => "redmine_subtask_list_accordion") +
    4:     javascript_include_tag("subtask_list_accordion" + (subtask_tree_client_processing? ? "_client" : ""), :plugin => "redmine_subtask_list_accordion")
  plugins/redmine_subtask_list_accordion/app/views/issues/_subtask_list_accordion_partial.html.erb:1:in `_292e8187f64bee60c61b7b15c99630ab'

After lots of trial and error we fixed the issue by adding the following to the original source code of the first plugin:

  included do
    alias_method :render_descendants_tree_original, :render_descendants_tree
    alias_method :render_descendants_tree, :switch_render_descendants_tree
    alias_method :sla_use_css, :sla_use_css
    alias_method :switch_render_descendants_tree, :switch_render_descendants_tree
    alias_method :render_descendants_tree_accordion, :render_descendants_tree_accordion
    alias_method :expand_tree_at_first?,  :expand_tree_at_first?
    alias_method :sla_has_grandson_issues?, :sla_has_grandson_issues?
    alias_method :subtask_tree_client_processing?, :subtask_tree_client_processing?
    alias_method :subtask_list_accordion_tree_render_32?, :subtask_list_accordion_tree_render_32?
    alias_method :subtask_list_accordion_tree_render_33?, :subtask_list_accordion_tree_render_33?
    alias_method :subtask_list_accordion_tree_render_34?, :subtask_list_accordion_tree_render_34?

This is the original code:

https://github.com/GEROMAX/redmine_subtask_list_accordion/blob/master/lib/redmine_subtask_list_accordion/patches/issues_helper_patch.rb

Which has the first two alias_method calls from the above source code.

By making an alias method with the same name for each method in the original class, the code works fine. However this seems like a hacky fix, and I don't understand why it works. Can someone explain why the fix works and how to rewrite it properly?


Solution

  • alias_method creates a copy of the named method in the context where it is called.

    Now, if the original method (second argument to alias_method) is defined somewhere up the inheritance chain and changed in any way later on, you still have that copy around that did not change. Which might explain the behavior you see.

    As for the rewriting: As a rule of thumb, throw out all method aliasing (in both plugins) and use Module#prepend. This SO Answer is a nice overview of good and bad techniques for (monkey) patching Ruby code.

    Patching Rails' view helpers as appears to be the case in your plugins can be tricky due to the way Rails handles them (and results may vary depending on code loading order). Wherever possible, avoid it or create new helper modules and add them to the relevant controllers using something like

    module RedmineSubtaskListAccordion
      module IssuesHelper
        def render_descendants_tree(issue)
          if sla_has_grandson_issues?(issue) &&  !subtask_tree_client_processing?
            render_descendants_tree_accordion(issue)
          else
            super # this will call the stock Redmine IssuesHelper#render_descendants_tree method
          end
        end
      end
    end
    
    # in init.rb:
    IssuesController.class_eval do
      helper RedmineSubtaskListAccordion::IssuesHelper
    end
    

    I happened to write a blog post about this exact thing a while ago. Hope it's OK to refer to that here.

    Obviously there may still be conflicts if two plugins expect a certain Redmine method they are changing to behave in the way it does in stock Redmine, but removing method aliasing and not trying to monkey patch already existing helpers in my experience helps to avoid many problems.

    Update: Most of the other methods in that particular module you linked to actually do not belong there, but could for example be mixed into the Issue model (like sla_has_grandson_issues? or declared as methods in the top level plugin namespace (all the settings and Redmine version related stuff - eg RedmineSubtaskListAccordion.tree_render_34?).