class RuboCop::Cop::ThreadSafety::MutableClassInstanceVariable

This cop checks whether some class instance variable isn't a mutable literal (e.g. array or hash).

It is based on Style/MutableConstant from RuboCop. See github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/style/mutable_constant.rb

Class instance variables are a risk to threaded code as they are shared between threads. A mutable object such as an array or hash may be updated via an attr_reader so would not be detected by the ThreadSafety/ClassAndModuleAttributes cop.

Strict mode can be used to freeze all class instance variables, 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.

@example EnforcedStyle: literals (default)

# bad
class Model
  @list = [1, 2, 3]
end

# good
class Model
  @list = [1, 2, 3].freeze
end

# good
class Model
  @var = <<~TESTING.freeze
    This is a heredoc
  TESTING
end

# good
class Model
  @var = Something.new
end

@example EnforcedStyle: strict

# bad
class Model
  @var = Something.new
end

# bad
class Model
  @var = Struct.new do
    def foo
      puts 1
    end
  end
end

# good
class Model
  @var = Something.new.freeze
end

# good
class Model
  @var = Struct.new do
    def foo
      puts 1
    end
  end.freeze
end

Constants

FROZEN_STRING_LITERAL_TYPES_RUBY27
FROZEN_STRING_LITERAL_TYPES_RUBY30
MSG

Public Instance Methods

autocorrect(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 111
def autocorrect(node)
  expr = node.source_range

  lambda do |corrector|
    splat_value = splat_value(node)
    if splat_value
      correct_splat_expansion(corrector, expr, splat_value)
    elsif node.array_type? && !node.bracketed?
      corrector.insert_before(expr, '[')
      corrector.insert_after(expr, ']')
    elsif requires_parentheses?(node)
      corrector.insert_before(expr, '(')
      corrector.insert_after(expr, ')')
    end

    corrector.insert_after(expr, '.freeze')
  end
end
on_ivasgn(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 83
def on_ivasgn(node)
  return unless in_class?(node)

  _, value = *node
  on_assignment(value)
end
on_masgn(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 98
def on_masgn(node)
  return unless in_class?(node)

  mlhs, values = *node
  return unless values.array_type?

  mlhs.to_a.zip(values.to_a).each do |lhs, value|
    next unless lhs.ivasgn_type?

    on_assignment(value)
  end
end
on_or_asgn(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 90
def on_or_asgn(node)
  lhs, value = *node
  return unless lhs&.ivasgn_type?
  return unless in_class?(node)

  on_assignment(value)
end

Private Instance Methods

check(value) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 158
def check(value)
  return unless mutable_literal?(value) ||
                range_enclosed_in_parentheses?(value)
  return if frozen_string_literal?(value)

  add_offense(value)
end
container?(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 175
def container?(node)
  return true if define_singleton_method?(node)

  %i[def defs class module].include?(node.type)
end
correct_splat_expansion(corrector, expr, splat_value) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 200
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
frozen_string_literal?(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 132
def frozen_string_literal?(node)
  literal_types = if target_ruby_version >= 3.0
                    FROZEN_STRING_LITERAL_TYPES_RUBY30
                  else
                    FROZEN_STRING_LITERAL_TYPES_RUBY27
                  end
  literal_types.include?(node.type) && frozen_string_literals_enabled?
end
immutable_literal?(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 187
def immutable_literal?(node)
  node.nil? || node.immutable_literal?
end
in_class?(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 166
def in_class?(node)
  container = node.ancestors.find do |ancestor|
    container?(ancestor)
  end
  return false if container.nil?

  %i[class module].include?(container.type)
end
mutable_literal?(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 181
def mutable_literal?(node)
  return if node.nil?

  node.mutable_literal? || range_type?(node)
end
on_assignment(value) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 141
def on_assignment(value)
  if style == :strict
    strict_check(value)
  else
    check(value)
  end
end
range_type?(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 196
def range_type?(node)
  node.erange_type? || node.irange_type?
end
requires_parentheses?(node) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 191
def requires_parentheses?(node)
  range_type?(node) ||
    (node.send_type? && node.loc.dot.nil?)
end
strict_check(value) click to toggle source
# File lib/rubocop/cop/thread_safety/mutable_class_instance_variable.rb, line 149
def strict_check(value)
  return if immutable_literal?(value)
  return if operation_produces_immutable_object?(value)
  return if operation_produces_threadsafe_object?(value)
  return if frozen_string_literal?(value)

  add_offense(value)
end