#15726·rubocop

Add an EnforcedStyle to Style/MutableConstant that forbids .freeze on literals

Author: mvelikovCreated Sep 18, 2026Updated Sep 18, 2026
Labelsfeature request

Before submitting

  • I have searched the existing issues and this hasn't been requested yet.

Cop name

Style/MutableConstant

Is your feature request related to a problem?

Style/MutableConstant can only ask for more freezing (literals, strict, Recursive). There is no way to enforce the opposite convention.

Some codebases treat the constant name itself as the contract. Ruby already warns on reassignment (warning: already initialized constant FOO) and in-place mutation of a constant is a bug in those codebases whether or not .freeze is present. For them .freeze on a literal is ceremony with no runtime effect they rely on, in the same category as trailing commas, semicolons or parentheses around arguments: a matter of taste that RuboCop lets each project pick a side on and then enforces consistently.

Describe the solution you'd like

A third EnforcedStyle value for Style/MutableConstant, tentatively unfrozen_literals:

  • Registers an offense for .freeze called on a mutable literal (array, hash, string, heredoc) assigned to a constant and autocorrects by removing it.
  • Leaves .freeze on non-literal values alone (Something.new.freeze, OTHER_CONST.freeze), since that cannot be expressed any other way.
  • Honours Recursive: true by also reporting nested frozen literals.
  • Does not change the default style.

This mirrors how other Style cops already offer both directions of a preference, for example Style/FrozenStringLiteralComment with always and never or Style/TrailingCommaInArrayLiteral with comma and no_comma.

Code examples

ruby
# EnforcedStyle: unfrozen_literals

# bad
CONST = [1, 2, 3].freeze
CONST = 'str'.freeze
CONST = { a: 1 }.freeze

# good
CONST = [1, 2, 3]
CONST = 'str'
CONST = { a: 1 }

# good - freezing a non-literal value is still allowed
CONST = Something.new.freeze

Describe alternatives you've considered

  • Enabled: false, which stops the cop from asking for .freeze but cannot enforce the no-freeze convention or autocorrect toward it. .freeze keeps creeping back through habit and has to be caught in code review by hand.
  • A custom cop, which every such project has to write and maintain separately.

Style/RedundantFreeze does not cover this. It removes .freeze from objects that are immutable anyway (integers, symbols, regexps). .freeze on an array, hash or string literal is not redundant in that sense, so nothing flags it.

Additional context

Edit: I have fast-forwarded and opened the PR - https://github.com/rubocop/rubocop/pull/15727.

I have a branch with the implementation, specs, a CLI spec proving it converges with Style/RedundantFreeze in one -A pass. I run it against an 8800-file Rails monolith (161 offenses in 88 files, all autocorrected, every changed line differing only by the removed .freeze). Happy to open the PR if the idea is welcome and open to a different name for the style.