# 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 `%s` attributes.' SEPARATED_MSG = 'Use one attribute per `%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