class RuboCop::Cop::Layout::ClassStructure
Checks if the code style follows the ExpectedOrder configuration:
‘Categories` allows us to map macro names into a category.
Consider an example of code style that covers the following order:
-
Module inclusion (include, prepend, extend)
-
Constants
-
Associations (has_one, has_many)
-
Public attribute macros (attr_accessor, attr_writer, attr_reader)
-
Other macros (validates, validate)
-
Public class methods
-
Initializer
-
Public instance methods
-
Protected attribute macros (attr_accessor, attr_writer, attr_reader)
-
Protected instance methods
-
Private attribute macros (attr_accessor, attr_writer, attr_reader)
-
Private instance methods
You can configure the following order:
- source,yaml
Layout/ClassStructure: ExpectedOrder: - module_inclusion - constants - association - public_attribute_macros - public_delegate - macros - public_class_methods - initializer - public_methods - protected_attribute_macros - protected_methods - private_attribute_macros - private_delegate - private_methods
Instead of putting all literals in the expected order, is also possible to group categories of macros. Visibility levels are handled automatically.
- source,yaml
Layout/ClassStructure: Categories: association: - has_many - has_one attribute_macros: - attr_accessor - attr_reader - attr_writer macros: - validates - validate module_inclusion: - include - prepend - extend
@safety
Autocorrection is unsafe because class methods and module inclusion can behave differently, based on which methods or constants have already been defined. Constants will only be moved when they are assigned with literals.
@example
# bad # Expect extend be before constant class Person < ApplicationRecord has_many :orders ANSWER = 42 extend SomeModule include AnotherModule end # good class Person # extend and include go first extend SomeModule include AnotherModule # inner classes CustomError = Class.new(StandardError) # constants are next SOME_CONSTANT = 20 # afterwards we have public attribute macros attr_reader :name # followed by other macros (if any) validates :name # then we have public delegate macros delegate :to_s, to: :name # public class methods are next in line def self.some_method end # initialization goes between class methods and instance methods def initialize end # followed by other public instance methods def some_method end # protected attribute macros and methods go next protected attr_reader :protected_name def some_protected_method end # private attribute macros, delegate macros and methods # are grouped near the end private attr_reader :private_name delegate :some_private_delegate, to: :name def some_private_method end end
Constants
- HUMANIZED_NODE_TYPE
- MSG
Public Instance Methods
Validates code style on class declaration. Add offense when find a node out of expected order.
# File lib/rubocop/cop/layout/class_structure.rb, line 158 def on_class(class_node) previous = -1 walk_over_nested_class_definition(class_node) do |node, category| index = expected_order.index(category) if index < previous message = format(MSG, category: category, previous: expected_order[previous]) add_offense(node, message: message) { |corrector| autocorrect(corrector, node) } end previous = index end end
Private Instance Methods
Autocorrect by swapping between two nodes autocorrecting them
# File lib/rubocop/cop/layout/class_structure.rb, line 174 def autocorrect(corrector, node) previous = node.left_siblings.reverse.find do |sibling| !ignore_for_autocorrect?(node, sibling) end return unless previous current_range = source_range_with_comment(node) previous_range = source_range_with_comment(previous) corrector.insert_before(previous_range, current_range.source) corrector.remove(current_range) end
# File lib/rubocop/cop/layout/class_structure.rb, line 305 def begin_pos_with_comment(node) first_comment = nil (node.first_line - 1).downto(1) do |annotation_line| break unless (comment = processed_source.comment_at_line(annotation_line)) first_comment = comment if whole_line_comment_at_line?(annotation_line) end start_line_position(first_comment || node) end
# File lib/rubocop/cop/layout/class_structure.rb, line 328 def buffer processed_source.buffer end
Setting categories hash allow you to group methods in group to match in the {expected_order}.
# File lib/rubocop/cop/layout/class_structure.rb, line 340 def categories cop_config['Categories'] end
# File lib/rubocop/cop/layout/class_structure.rb, line 234 def class_elements(class_node) class_def = class_node.body return [] unless class_def if class_def.def_type? || class_def.send_type? [class_def] else class_def.children.compact end end
Classifies a node to match with something in the {expected_order} @param node to be analysed @return String
when the node type is a ‘:block` then
{classify} recursively with the first children
@return String
when the node type is a ‘:send` then {find_category}
by method name
@return String
otherwise trying to {humanize_node} of the current node
# File lib/rubocop/cop/layout/class_structure.rb, line 194 def classify(node) return node.to_s unless node.respond_to?(:type) case node.type when :block classify(node.send_node) when :send find_category(node) else humanize_node(node) end.to_s end
# File lib/rubocop/cop/layout/class_structure.rb, line 271 def dynamic_constant?(node) return false unless node.casgn_type? && node.namespace.nil? expression = node.expression expression.send_type? && !(expression.method?(:freeze) && expression.receiver&.recursive_basic_literal?) end
# File lib/rubocop/cop/layout/class_structure.rb, line 295 def end_position_for(node) if node.casgn_type? heredoc = find_heredoc(node) return heredoc.location.heredoc_end.end_pos + 1 if heredoc end end_line = buffer.line_for_position(node.source_range.end_pos) buffer.line_range(end_line).end_pos end
Load expected order from ‘ExpectedOrder` config. Define new terms in the expected order by adding new {categories}.
# File lib/rubocop/cop/layout/class_structure.rb, line 334 def expected_order cop_config['ExpectedOrder'] end
Categorize a node according to the {expected_order} Try to match {categories} values against the node’s method_name given also its visibility. @param node to be analysed. @return [String] with the key category or the ‘method_name` as string
# File lib/rubocop/cop/layout/class_structure.rb, line 212 def find_category(node) name = node.method_name.to_s category, = categories.find { |_, names| names.include?(name) } key = category || name visibility_key = if node.def_modifier? "#{name}_methods" else "#{node_visibility(node)}_#{key}" end expected_order.include?(visibility_key) ? visibility_key : key end
# File lib/rubocop/cop/layout/class_structure.rb, line 324 def find_heredoc(node) node.each_node(:str, :dstr, :xstr).find(&:heredoc?) end
# File lib/rubocop/cop/layout/class_structure.rb, line 262 def humanize_node(node) if node.def_type? return :initializer if node.method?(:initialize) return "#{node_visibility(node)}_methods" end HUMANIZED_NODE_TYPE[node.type] || node.type end
# File lib/rubocop/cop/layout/class_structure.rb, line 246 def ignore?(node, classification) classification.nil? || classification.to_s.end_with?('=') || expected_order.index(classification).nil? || private_constant?(node) end
# File lib/rubocop/cop/layout/class_structure.rb, line 253 def ignore_for_autocorrect?(node, sibling) classification = classify(node) sibling_class = classify(sibling) ignore?(sibling, sibling_class) || classification == sibling_class || dynamic_constant?(node) end
# File lib/rubocop/cop/layout/class_structure.rb, line 289 def marked_as_private_constant?(node, name) return false unless node.method?(:private_constant) node.arguments.any? { |arg| (arg.sym_type? || arg.str_type?) && arg.value == name } end
# File lib/rubocop/cop/layout/class_structure.rb, line 279 def private_constant?(node) return false unless node.casgn_type? && node.namespace.nil? return false unless (parent = node.parent) parent.each_child_node(:send) do |child_node| return true if marked_as_private_constant?(child_node, node.name) end false end
# File lib/rubocop/cop/layout/class_structure.rb, line 320 def start_line_position(node) buffer.line_range(node.loc.line).begin_pos - 1 end
# File lib/rubocop/cop/layout/class_structure.rb, line 225 def walk_over_nested_class_definition(class_node) class_elements(class_node).each do |node| classification = classify(node) next if ignore?(node, classification) yield node, classification end end
# File lib/rubocop/cop/layout/class_structure.rb, line 316 def whole_line_comment_at_line?(line) /\A\s*#/.match?(processed_source.lines[line - 1]) end