123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Style::MutableConstant

Relationships & Source Files
Namespace Children
Modules:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ::RuboCop::Cop::AutoCorrector, ::RuboCop::Cop::Base, ::RuboCop::ExcludeLimit, NodePattern::Macros, RuboCop::AST::Sexp
Instance Chain:
Inherits: RuboCop::Cop::Base
Defined in: lib/rubocop/cop/style/mutable_constant.rb

Overview

Checks whether some constant value isn’t a mutable literal (e.g. array or hash).

When the Recursive option is enabled, mutable literals nested inside arrays and hashes are also frozen, so an offense on the outermost unfrozen literal will autocorrect every nested mutable literal as well. When the outer literal already has .freeze appended, the cop descends into it and reports each outermost unfrozen literal underneath. The option is disabled by default to preserve existing behavior; opt in to get strict nested freezing.

Strict mode can be used to freeze all constants, rather than just literals. Strict mode is considered an experimental feature. It has not been updated with an exhaustive list of all methods that will produce frozen objects so there is a decent chance of getting some false positives. Luckily, there is no harm in freezing an already frozen object.

From Ruby 3.0, this cop honours the magic comment 'shareable_constant_value'. When this magic comment is set to any acceptable value other than none, it will suppress the offenses raised by this cop. It enforces frozen state.

Note
Regexp and Range literals are frozen objects since Ruby 3.0.
Note
From Ruby 3.0, interpolated strings are not frozen when # frozen-string-literal: true is used, so this cop enforces explicit freezing for such strings.
Note
From Ruby 3.0, this cop allows explicit freezing of constants when the shareable_constant_value directive is used.

Examples:

EnforcedStyle: literals (default)

# bad
CONST = [1, 2, 3]

# good
CONST = [1, 2, 3].freeze

# good
CONST = <<~TESTING.freeze
  This is a heredoc
TESTING

# good
CONST = Something.new

Recursive: false (default)

# good - only the outer container needs to be frozen
CONST = [{ a: [], b: 'foo' }].freeze

Recursive: true

# bad - nested mutable literals must be frozen too
CONST = [{ a: [], b: 'foo' }].freeze

# good
CONST = [{ a: [].freeze, b: 'foo'.freeze }.freeze].freeze

EnforcedStyle: strict

# bad
CONST = Something.new

# bad
CONST = Struct.new do
  def foo
    puts 1
  end
end

# good
CONST = Something.new.freeze

# good
CONST = Struct.new do
  def foo
    puts 1
  end
end.freeze
# Magic comment - shareable_constant_value: literal

# bad
CONST = [1, 2, 3]

# good
# shareable_constant_value: literal
CONST = [1, 2, 3]

Cop Safety Information:

  • This cop’s autocorrection is unsafe since any mutations on objects that are made frozen will change from being accepted to raising FrozenError, and will need to be manually refactored.

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::RuboCop::Cop::FrozenStringLiteral - Included

FROZEN_STRING_LITERAL_ENABLED, FROZEN_STRING_LITERAL_TYPES_RUBY27

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

SYMBOL_TO_STRING_CACHE

Class Attribute Summary

::RuboCop::Cop::AutoCorrector - Extended

::RuboCop::Cop::Base - Inherited

.gem_requirements, .lint?,
.support_autocorrect?

Returns if class supports autocorrect.

.support_multiple_source?

Override if your cop should be called repeatedly for multiple investigations Between calls to on_new_investigation and on_investigation_end, the result of processed_source will remain constant.

Class Method Summary

::RuboCop::Cop::Base - Inherited

.autocorrect_incompatible_with

List of cops that should not try to autocorrect at the same time as this cop.

.badge

Naming.

.callbacks_needed, .cop_name, .department,
.documentation_url

Returns a url to view this cops documentation online.

.exclude_from_registry

Call for abstract Cop classes.

.inherited,
.joining_forces

Override and return the Force class(es) you need to join.

.match?

Returns true if the cop name or the cop namespace matches any of the given names.

.new,
.requires_gem

Register a version requirement for the given gem name.

.restrict_on_send

::RuboCop::ExcludeLimit - Extended

exclude_limit

Sets up a configuration option to have an exclude limit tracked.

transform

Instance Attribute Summary

Instance Method Summary

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

::RuboCop::Cop::FrozenStringLiteral - Included

ShareableConstantValue - Included

#magic_comment_in_scope

Identifies the most recent magic comment with valid shareable constant values that’s in scope for this node.

#processed_source_till_node, #recent_shareable_value?, #shareable_constant_value_enabled?

::RuboCop::Cop::Base - Inherited

#add_global_offense

Adds an offense that has no particular location.

#add_offense

Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense.

#begin_investigation

Called before any investigation.

#callbacks_needed,
#cop_config

Configuration Helpers.

#cop_name, #excluded_file?,
#external_dependency_checksum

This method should be overridden when a cop’s behavior depends on state that lives outside of these locations:

#inspect,
#message

Gets called if no message is specified when calling add_offense or add_global_offense Cops are discouraged to override this; instead pass your message directly.

#name

Alias for Base#cop_name.

#offenses,
#on_investigation_end

Called after all on_…​

#on_new_investigation

Called before all on_…​

#on_other_file

Called instead of all on_…​

#parse

There should be very limited reasons for a Cop to do it’s own parsing.

#parser_engine,
#ready

Called between investigations.

#relevant_file?,
#target_gem_version

Returns a gems locked versions (i.e.

#target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #matches_absolute_include_pattern?, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

This class inherits a constructor from RuboCop::Cop::Base

Instance Attribute Details

#recursive?Boolean (readonly, private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 273

def recursive?
  cop_config.fetch('Recursive', false)
end

Instance Method Details

#autocorrect(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 209

def autocorrect(corrector, node)
  expr = node.source_range

  splat_value = splat_value(node)
  if splat_value
    correct_splat_expansion(corrector, expr, splat_value)
    corrector.insert_after(expr, '.freeze')
    return
  end

  if node.array_type? && !node.bracketed?
    corrector.wrap(expr, '[', ']')
  elsif requires_parentheses?(node)
    corrector.wrap(expr, '(', ')')
  end

  corrector.insert_after(expr, '.freeze')

  freeze_nested_literals(corrector, node) if recursive?
end

#correct_splat_expansion(corrector, expr, splat_value) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 301

def correct_splat_expansion(corrector, expr, splat_value)
  if range_enclosed_in_parentheses?(splat_value)
    corrector.replace(expr, "#{splat_value.source}.to_a")
  else
    corrector.replace(expr, "(#{splat_value.source}).to_a")
  end
end

#explicitly_frozen_literal?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 267

def explicitly_frozen_literal?(node)
  return false unless node.send_type? && node.method?(:freeze)

  node.receiver && mutable_literal?(node.receiver)
end

#freezable_nested_literal?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 243

def freezable_nested_literal?(node)
  return false if frozen_string_literal?(node)
  return false if shareable_constant_value?(node)

  mutable_literal?(node)
end

#freeze_nested_literals(corrector, node) (private)

Recursively freezes every nested mutable literal inside an array or hash literal. Already-frozen subtrees are not re-frozen, but their children are still inspected for unfrozen literals deeper down.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 233

def freeze_nested_literals(corrector, node)
  literal_children(node).each do |child|
    if explicitly_frozen_literal?(child)
      freeze_nested_literals(corrector, child.receiver)
    elsif freezable_nested_literal?(child)
      autocorrect(corrector, child)
    end
  end
end

#frozen_regexp_or_range_literals?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 293

def frozen_regexp_or_range_literals?(node)
  target_ruby_version >= 3.0 && node.type?(:regexp, :range)
end

#immutable_literal?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 283

def immutable_literal?(node)
  frozen_regexp_or_range_literals?(node) || node.immutable_literal?
end

#literal_check(value) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 196

def literal_check(value)
  return unless mutable_or_unfrozen_range?(value)
  return if frozen_string_literal?(value)
  return if shareable_constant_value?(value)

  true
end

#literal_children(node) (private)

Returns the child literals of an array or hash node that may themselves need freezing. For hashes, both keys and values are included. Percent-literal arrays (e.g. %w(a b)) are skipped because .freeze cannot be appended to their contents.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 254

def literal_children(node)
  case node.type
  when :array
    return [] if node.percent_literal?

    node.children
  when :hash
    node.children.flat_map { |child| child.pair_type? ? child.children : [] }
  else
    []
  end
end

#mutable_literal?(value) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 277

def mutable_literal?(value)
  return false if frozen_regexp_or_range_literals?(value)

  value.mutable_literal?
end

#mutable_nodes(value, &block) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 173

def mutable_nodes(value, &block)
  if recursive? && explicitly_frozen_literal?(value)
    literal_children(value.receiver).flat_map { |c| mutable_nodes(c, &block) }
  else
    node_offending = yield(value)

    if node_offending
      [value]
    else
      []
    end
  end
end

#mutable_or_unfrozen_range?(value) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 204

def mutable_or_unfrozen_range?(value)
  mutable_literal?(value) ||
    (target_ruby_version <= 2.7 && range_enclosed_in_parentheses?(value))
end

#on_assignment(value) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 159

def on_assignment(value)
  nodes = mutable_nodes(value) do |node|
    if style == :strict
      strict_check(node)
    else
      literal_check(node)
    end
  end

  nodes.each do |node|
    add_offense(node) { |corrector| autocorrect(corrector, node) }
  end
end

#on_casgn(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 146

def on_casgn(node)
  if node.expression.nil? # This is only the case for `CONST += ...` or similar
    parent = node.parent
    return unless parent.or_asgn_type? # We only care about `CONST ||= ...`

    on_assignment(parent.children.last)
  else
    on_assignment(node.expression)
  end
end

#operation_produces_immutable_object?(node) (private)

Some of these patterns may not actually return an immutable object, but we want to consider them immutable for this cop.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 317

def_node_matcher :operation_produces_immutable_object?, <<~PATTERN
  {
    (const _ _)
    (send (const {nil? cbase} :Struct) :new ...)
    (block (send (const {nil? cbase} :Struct) :new ...) ...)
    (send _ :freeze)
    (send {float int} {:+ :- :* :** :/ :% :<<} _)
    (send _ {:+ :- :* :** :/ :%} {float int})
    (send _ {:== :=== :!= :<= :>= :< :>} _)
    (send (const {nil? cbase} :ENV) :[] _)
    (or (send (const {nil? cbase} :ENV) :[] _) _)
    (send _ {:count :length :size} ...)
    (block (send _ {:count :length :size} ...) ...)
  }
PATTERN

#range_enclosed_in_parentheses?(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 334

def_node_matcher :range_enclosed_in_parentheses?, <<~PATTERN
  (begin (range _ _))
PATTERN

#requires_parentheses?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 297

def requires_parentheses?(node)
  node.range_type? || (node.send_type? && node.loc.dot.nil?)
end

#shareable_constant_value?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 287

def shareable_constant_value?(node)
  return false if target_ruby_version < 3.0

  recent_shareable_value? node
end

#splat_value(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 310

def_node_matcher :splat_value, <<~PATTERN
  (array (splat $_))
PATTERN

#strict_check(value) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/mutable_constant.rb', line 187

def strict_check(value)
  return if immutable_literal?(value)
  return if operation_produces_immutable_object?(value)
  return if frozen_string_literal?(value)
  return if shareable_constant_value?(value)

  true
end