lib/rubocop/cop/style/static_class.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for places where classes with only class methods can be
# replaced with a module. Classes should be used only when it makes sense to create
# instances out of them.
#
# @safety
# This cop is unsafe, because it is possible that this class is a parent
# for some other subclass, monkey-patched with instance methods or
# a dummy instance is instantiated from it somewhere.
#
# @example
# # bad
# class SomeClass
# def self.some_method
# # body omitted
# end
#
# def self.some_other_method
# # body omitted
# end
# end
#
# # good
# module SomeModule
# module_function
#
# def some_method
# # body omitted
# end
#
# def some_other_method
# # body omitted
# end
# end
#
# # good - has instance method
# class SomeClass
# def instance_method; end
# def self.class_method; end
# end
#
class StaticClass < Base
include RangeHelp
include VisibilityHelp
extend AutoCorrector
MSG = 'Prefer modules to classes with only class methods.'
def on_class(class_node)
return if class_node.parent_class
return unless class_convertible_to_module?(class_node)
add_offense(class_node) do |corrector|
autocorrect(corrector, class_node)
end
end
private
def autocorrect(corrector, class_node)
corrector.replace(class_node.loc.keyword, 'module')
corrector.insert_after(class_node.loc.name, "\nmodule_function\n")
class_elements(class_node).each do |node|
if node.defs_type?
autocorrect_def(corrector, node)
elsif node.sclass_type?
autocorrect_sclass(corrector, node)
end
end
end
def autocorrect_def(corrector, node)
corrector.remove(
range_between(node.receiver.source_range.begin_pos, node.loc.name.begin_pos)
)
end
def autocorrect_sclass(corrector, node)
corrector.remove(
range_between(node.loc.keyword.begin_pos, node.identifier.source_range.end_pos)
)
corrector.remove(node.loc.end)
end
def class_convertible_to_module?(class_node)
nodes = class_elements(class_node)
return false if nodes.empty?
nodes.all? do |node|
(node_visibility(node) == :public && node.defs_type?) ||
sclass_convertible_to_module?(node) ||
node.equals_asgn? ||
extend_call?(node)
end
end
def extend_call?(node)
node.send_type? && node.method?(:extend)
end
def sclass_convertible_to_module?(node)
return false unless node.sclass_type?
class_elements(node).all? do |child|
node_visibility(child) == :public && (child.def_type? || child.equals_asgn?)
end
end
def class_elements(class_node)
class_def = class_node.body
if !class_def
[]
elsif class_def.begin_type?
class_def.children
else
[class_def]
end
end
end
end
end
end