Rubocop: Style/MutableConstant should detect "late" freeze

Created on 23 May 2017  路  9Comments  路  Source: rubocop-hq/rubocop

I have a hash with a default value assigned to a constant like this:

ERROR_CODES = {
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
}
ERROR_CODES.default = :GenericError
ERROR_CODES.freeze

Although the hash is frozen, RuboCop objects that is is not. This is because it is not frozen upon assignment. IMO, RuboCop should also detect a "late" freeze, as shown in the example.

Most helpful comment

I use following style to avoid such offense.

error_codes = {
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
}
error_codes.default = :GenericError
ERROR_CODES = error_codes.freeze

All 9 comments

That's relatively hard to do, so I don't think we should bother with it. Obviously you can just move the freeze in your code.

Obviously you can just move the freeze in your code.

Sorry, I don't see how I could move that freezesomewhere else. 馃槙

default doesn't work for frozen hashes? I thought this was some Hash API, but I guess it's something from ActiveSupport...

No it doesn't. Calling Hash#default= is considered a modification of the hash.

The only workaround I can think of is:

ERROR_CODES = {
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
}.tap { |hash| hash.default = :GenericError }.freeze

but I'd rather avoid that hack.

In case you haven't considered, I like the following:

ERROR_CODES = Hash.new(:GenericError).merge!(
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
).freeze

@marcandre nice one, that would work. Unfortunately Rubycop doesn't enforce freeze in that case, it only does so for literals.

I use following style to avoid such offense.

error_codes = {
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
}
error_codes.default = :GenericError
ERROR_CODES = error_codes.freeze

Though I don't believe it is a good idea to kinda "Monkey patch" the hash default value.
I would live it as nil
You could still do:

ERROR_CODES = {
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
}
DEFAULT_ERROR = :GenericError

or

ERROR_CODES = {
  default: :GenericError
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
}

or

ERROR_CODES = {
  100 => :FooError,
  200 => :BarError,
  300 => :BazError
}

def some_single_source_of_truth_method(code)
  ERROR_CODES = {
    100 => :FooError,
    200 => :BarError,
    300 => :BazError
  }.fetch(code) { :GenericError }
end

There hasn't been much activity on this ticket and our Core Team is spread too thin on so many tasks, so I'll just close it for the sake of having a cleaner lists of tasks to focus on. We'd gladly review a PR, but it's unlikely that'd we're going to tackle this ourselves in the foreseeable future.

The problem seems to be pretty rare in practice, so probably just some exclusion would be fine for the people affected by it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kirrmann picture kirrmann  路  3Comments

david942j picture david942j  路  3Comments

deivid-rodriguez picture deivid-rodriguez  路  3Comments

bbatsov picture bbatsov  路  3Comments

NobodysNightmare picture NobodysNightmare  路  3Comments