Line Noise: Rails enum scope overwrites existing method

You know when you are running your code, and you see a bunch of messages, and you have no idea what they are? And then one day, you just get used to them...

I'm reminded of this article:

It was my first day at work.

As I was talking to [the receptionist], I noticed a bright orange extension cord plugged in to the wall right behind the desk

I asked her, "What's with the orange extension cord?"

She replied, "What orange extension cord?"

I pointed it out and said, "That one, right behind you."

"Oh," she said as she turned, "That's been there a while."

I thought to myself, "Really? It's bright orange!"

This past week, the orange extension cord I decided to figure out was this one:

W, [2021-02-16T20:03:39.269073 #38]  WARN -- : Creating scope :open. Overwriting existing method Flake.open.
W, [2021-02-16T20:03:39.319617 #38]  WARN -- : Creating scope :open. Overwriting existing method FlakeCandidate.open.

I had primarily noticed in the Rails production console, but not as frequently in development. Rails does try to eagerly load all your classes in production, so that makes sense it wouldn't come up unless you are specifically using the class in development.

The classes in question:

class Flake < ApplicationRecor
  enum github_issue_status: { open: 0, closed: 1 }
end


class FlakeCandidate < ApplicationController
  enum github_issue_status: { open: 0, closed: 1 }
end

Step 1: Reproduce

This is easy enough, just access the class:

> Flake
Creating scope :open. Overwriting existing method Flake.open.
=> Flake (call 'Flake.connection' to establish a connection)

Step 2: Investigate

Unfortunately, this particular message doesn't give us a ton more insight to where it's happening. If you have big classes, have several enums, and one of which has open, it'd be easy to not even know where to begin.

It's easy to google things. It's hard to google the correct thing. If you search the exact message as is, you may end up with unrelated results based on what's in your models.

You can guess that the model and method name are getting substituted into an error message. One thing you can do is remove your local references from it (file paths, class names), and then quote the fragments of the original message. Add some keywords, and I end up with:

rails enum "Creating scope" "Overwriting existing method"

The first result is super promising: Rails warning: Overwriting existing method <model_name>.open · Issue #31234 · rails/rails

There is this comment from @matthewd:

Something’s defining an open method on the Issue class

The something is Ruby, and it's on Kernel.

I'm not sure how we can make the warning clearer; you seem to have interpreted it as a claim that something is overwriting your scope ("does not actually overwrite the method"), while that's the opposite of what it means to convey: your scope is itself overwriting the existing open method.

Thinking back to when I first saw this, I think I interpreted it the same way: something else was overriding the scope I defined. I agree the error is kind of confusing, and it's also hard to make it more accurate.

What makes this even trickier is that the method is coming from something you don't even think of as being part of the class! I have been doing Ruby for awhile, and I don't often stop to think how methods from Kernel are available everywhere.

I was curious if there was a way to show this, without having to know that Kernel is included, and that it has an open method. Turns out pry's show-source can show you!

> show-source User#open

From: /Users/technicalpickles/.rbenv/versions/2.6.6/lib/ruby/2.6.0/open-uri.rb @ line 29:
Owner: Kernel
Visibility: private
Number of lines: 11

def open(name, *rest, &block) # :doc:
  if name.respond_to?(:open)
    name.open(*rest, &block)
  elsif name.respond_to?(:to_str) &&
        %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
        (uri = URI.parse(name)).respond_to?(:open)
    uri.open(*rest, &block)
  else
    open_uri_original_open(name, *rest, &block)
  end
end

Step 3: Fix

@bf4 was kind enough to leave a comment for future googlers:

For future googlers:

since the output comes from the logger, we can either wrap the enum definition in logger.silence (which may not be available) or something like that, or what is probably more correct, what I've done, it to undef the offending method before

  class << self; undef_method :open; end # the enum overrides the Kernel#open method which we don't care about
  enum status: { open: 0, closed: 1 }

I confirmed this works. I already have two models that would need it though, and it's not really useful for telling you why it's needed without having a code comment.

I ended up with a class method on my ApplicationRecord:

class ApplicationRecord < ActiveRecord::Base
	
  # rest of class goes here

  # This is needed to avoid a warning like "Creating scope :open. Overwriting existing method Flake.open"
  # when you have class with an enum with a value of 'open'
  #
  # See https://github.com/rails/rails/issues/31234 for an explanation
  def self.undefine_kernel_open!
    class << self
      undef_method :open
    end
  end

Now I can call it whenever I need it like:

class Flake < ApplicationRecor
  undefine_kernel_open!
  enum github_issue_status: { open: 0, closed: 1 }
end


class FlakeCandidate < ApplicationController
  undefine_kernel_open!
  enum github_issue_status: { open: 0, closed: 1 }
end

I like this a lot better because it gives future readers (including myself!) better context for understanding what it's doing.

Takeaways

Here's what I learned (or relearned):

  • it can be satisfying to take 15 minutes to understand what some random message you see
  • use pry's show-source to find where a method is defined
  • Kernel is like everywhere!
  • Issues on the rails/rails are a treasure trove of knowledge