lib/rubocop/cop/style/accessor_grouping.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for grouping of accessors in `class` and `module` bodies.
# By default it enforces accessors to be placed in grouped declarations,
# but it can be configured to enforce separating them in multiple declarations.
#
# NOTE: If there is a method call before the accessor method it is always allowed
# as it might be intended like Sorbet.
#
# @example EnforcedStyle: grouped (default)
# # bad
# class Foo
# attr_reader :bar
# attr_reader :bax
# attr_reader :baz
# end
#
# # good
# class Foo
# attr_reader :bar, :bax, :baz
# end
#
# # good
# class Foo
# # may be intended comment for bar.
# attr_reader :bar
#
# sig { returns(String) }
# attr_reader :bax
#
# may_be_intended_annotation :baz
# attr_reader :baz
# end
#
# @example EnforcedStyle: separated
# # bad
# class Foo
# attr_reader :bar, :baz
# end
#
# # good
# class Foo
# attr_reader :bar
# attr_reader :baz
# end
#
class AccessorGrouping < Base
include ConfigurableEnforcedStyle
include RangeHelp
include VisibilityHelp
extend AutoCorrector
GROUPED_MSG = 'Group together all `%<accessor>s` attributes.'
SEPARATED_MSG = 'Use one attribute per `%<accessor>s`.'
def on_class(node)
class_send_elements(node).each do |macro|
next unless macro.attribute_accessor?
check(macro)
end
end
alias on_sclass on_class
alias on_module on_class
private
def check(send_node)
return if previous_line_comment?(send_node) || !groupable_accessor?(send_node)
return unless (grouped_style? && groupable_sibling_accessors(send_node).size > 1) ||
(separated_style? && send_node.arguments.size > 1)
message = message(send_node)
add_offense(send_node, message: message) do |corrector|
autocorrect(corrector, send_node)
end
end
def autocorrect(corrector, node)
if (preferred_accessors = preferred_accessors(node))
corrector.replace(node, preferred_accessors)
else
range = range_with_surrounding_space(node.source_range, side: :left)
corrector.remove(range)
end
end
def previous_line_comment?(node)
comment_line?(processed_source[node.first_line - 2])
end
# rubocop:disable Metrics/CyclomaticComplexity
def groupable_accessor?(node)
return true unless (previous_expression = node.left_siblings.last)
# Accessors with Sorbet `sig { ... }` blocks shouldn't be groupable.
if previous_expression.block_type?
previous_expression.child_nodes.each do |child_node|
break previous_expression = child_node if child_node.send_type?
end
end
return true unless previous_expression.send_type?
previous_expression.attribute_accessor? ||
previous_expression.access_modifier? ||
node.first_line - previous_expression.last_line > 1 # there is a space between nodes
end
# rubocop:enable Metrics/CyclomaticComplexity
def class_send_elements(class_node)
class_def = class_node.body
if !class_def || class_def.def_type?
[]
elsif class_def.send_type?
[class_def]
else
class_def.each_child_node(:send).to_a
end
end
def grouped_style?
style == :grouped
end
def separated_style?
style == :separated
end
def groupable_sibling_accessors(send_node)
send_node.parent.each_child_node(:send).select do |sibling|
sibling.attribute_accessor? &&
sibling.method?(send_node.method_name) &&
node_visibility(sibling) == node_visibility(send_node) &&
groupable_accessor?(sibling) && !previous_line_comment?(sibling)
end
end
def message(send_node)
msg = grouped_style? ? GROUPED_MSG : SEPARATED_MSG
format(msg, accessor: send_node.method_name)
end
def preferred_accessors(node)
if grouped_style?
accessors = groupable_sibling_accessors(node)
group_accessors(node, accessors) if node.loc == accessors.first.loc
else
separate_accessors(node)
end
end
def group_accessors(node, accessors)
accessor_names = accessors.flat_map { |accessor| accessor.arguments.map(&:source) }.uniq
"#{node.method_name} #{accessor_names.join(', ')}"
end
def separate_accessors(node)
node.arguments.flat_map do |arg|
lines = [
*processed_source.ast_with_comments[arg].map(&:text),
"#{node.method_name} #{arg.source}"
]
if arg == node.first_argument
lines
else
indent = ' ' * node.loc.column
lines.map { |line| "#{indent}#{line}" }
end
end.join("\n")
end
end
end
end
end