Handling Ruby 2.7 deprecations warnings

Posted on 2020-02-03 by Karol Bąk Comments
ruby
rails

In Ruby 3.0, positional arguments and keyword arguments will be separated. For now ruby 2.7 brings deprecations warnings that notifies us that our code should be updated. In most cases adding or removing ** should fix the problem but the question is - how to find all occurrences in the code?

If you have high test coverage it should be pretty easy - just run the specs and fix warnings one by one. What if you don’t? Well, there is another solution.

Custom warnings handling

Ruby allows us to customize warnings handling by implementing Warning module with warn method:

module Warning
  def warn(msg)
    # our custom warning logic
  end
end

If you are using Rails you can create warning.rb file in lib directory and include it right after Bundler.require(*Rails.groups) in your application.rb:

Bundler.require(*Rails.groups)
require_relative '../lib/warning'

You have to explicitly require it since autoloading won’t work in this case, because we are overwriting standard library.

If you are using error tracking services like Honeybadger, Sentry or Rollbar you can use them to notify you about warnings (in my example I’ll use Raven gem from Sentry).

Simple solution would look like this:

module Warning
  class DeprecationWarning < StandardError; end

  def warn(msg)
    Raven.capture_exception(DeprecationWarning.new(msg))
  end
end

After deploying it to staging/production we will receive notifications about each warning. Be careful! Make sure that your warn implementation isn’t causing any warnings! If it is, it will create an infinite loop!

Let’s take a look at an example:

/Users/kukicola/.rvm/gems/ruby-2.7.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:115: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/kukicola/.rvm/gems/ruby-2.7.0/gems/activerecord-6.0.2.1/lib/active_record/relation.rb:27: warning: The called method `initialize' is defined here

In this case warning contains two parts. Our method will be called two times - one for each line. What is really important for us is the path of the invalid method call, so in this case:

/Users/kukicola/.rvm/gems/ruby-2.7.0/gems/activerecord-6.0.2.1/lib/active_record/relation/delegation.rb:115

We can update our code to extract path from the first line and skip the second one:

module Warning
  class DeprecationWarning < StandardError; end

  def warn(msg)
    return unless msg.include?('deprecated')

    path = msg.split(':')[0..1].join(':')
    Raven.capture_exception(DeprecationWarning.new(path))
  end
end

Our solution is almost perfect! There is only one case left - what if our app is causing some different warnings? We can keep original implementation for them using alias_method. It’s not pretty but since it’s a module we can’t call super:

module Warning
  class DeprecationWarning < StandardError; end

  alias_method :original_warn, :warn

  def warn(msg)
    return original_warn(msg) unless msg.include?('deprecated')

    path = msg.split(':')[0..1].join(':')
    Raven.capture_exception(DeprecationWarning.new(path))
  end
end

Deploy and fix warnings as they occur

Since Ruby 2.7 does not change arguments behavior it should be safe to deploy it to production environment. With custom Warning implementation you can fix warnings one by one as they occur. Keep in mind that you’ll probably see a lot of warnings that comes from gems. It is a good moment to update them as well!