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:

You can configure the following order:

“`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.

“`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

“`

@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

@see rubystyle.guide#consistent-classes

Constants

HUMANIZED_NODE_TYPE
MSG
VISIBILITY_SCOPES

Public Instance Methods

autocorrect(node) click to toggle source

Autocorrect by swapping between two nodes autocorrecting them

# File lib/rubocop/cop/layout/class_structure.rb, line 165
def autocorrect(node)
  node_classification = classify(node)
  previous = left_siblings_of(node).find do |sibling|
    classification = classify(sibling)
    !ignore?(classification) && node_classification != classification
  end

  current_range = source_range_with_comment(node)
  previous_range = source_range_with_comment(previous)

  lambda do |corrector|
    corrector.insert_before(previous_range, current_range.source)
    corrector.remove(current_range)
  end
end
on_class(class_node) click to toggle source

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 151
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)
    end
    previous = index
  end
end

Private Instance Methods

begin_pos_with_comment(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 303
def begin_pos_with_comment(node)
  annotation_line = node.first_line - 1
  first_comment = nil

  processed_source.comments_before_line(annotation_line)
                  .reverse_each do |comment|
    if comment.location.line == annotation_line
      first_comment = comment
      annotation_line -= 1
    end
  end

  start_line_position(first_comment || node)
end
buffer() click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 322
def buffer
  processed_source.buffer
end
categories() click to toggle source

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 334
def categories
  cop_config['Categories']
end
class_elements(class_node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 225
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
classify(node) click to toggle source

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 190
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
end_position_for(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 298
def end_position_for(node)
  end_line = buffer.line_for_position(node.loc.expression.end_pos)
  buffer.line_range(end_line).end_pos
end
expected_order() click to toggle source

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 328
def expected_order
  cop_config['ExpectedOrder']
end
find_category(node) click to toggle source

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 208
def find_category(node)
  name = node.method_name.to_s
  category, = categories.find { |_, names| names.include?(name) }
  key = category || name
  visibility_key = "#{node_visibility(node)}_#{key}"
  expected_order.include?(visibility_key) ? visibility_key : key
end
find_visibility_end(node) click to toggle source

Navigate to find the last protected method

# File lib/rubocop/cop/layout/class_structure.rb, line 255
def find_visibility_end(node)
  possible_visibilities = VISIBILITY_SCOPES - [node_visibility(node)]
  right = right_siblings_of(node)
  right.find do |child_node|
    possible_visibilities.include?(node_visibility(child_node))
  end || right.last
end
find_visibility_start(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 248
def find_visibility_start(node)
  left_siblings_of(node)
    .reverse
    .find(&method(:visibility_block?))
end
humanize_node(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 275
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
ignore?(classification) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 237
def ignore?(classification)
  classification.nil? ||
    classification.to_s.end_with?('=') ||
    expected_order.index(classification).nil?
end
left_siblings_of(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 271
def left_siblings_of(node)
  siblings_of(node)[0, node.sibling_index]
end
node_visibility(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 243
def node_visibility(node)
  scope = find_visibility_start(node)
  scope&.method_name || :public
end
right_siblings_of(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 267
def right_siblings_of(node)
  siblings_of(node)[node.sibling_index..-1]
end
siblings_of(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 263
def siblings_of(node)
  node.parent.children
end
source_range_with_comment(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 284
def source_range_with_comment(node)
  begin_pos, end_pos =
    if node.def_type?
      start_node = find_visibility_start(node) || node
      end_node = find_visibility_end(node) || node
      [begin_pos_with_comment(start_node),
       end_position_for(end_node) + 1]
    else
      [begin_pos_with_comment(node), end_position_for(node)]
    end

  Parser::Source::Range.new(buffer, begin_pos, end_pos)
end
start_line_position(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 318
def start_line_position(node)
  buffer.line_range(node.loc.line).begin_pos - 1
end
walk_over_nested_class_definition(class_node) { |node, classification| ... } click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 216
def walk_over_nested_class_definition(class_node)
  class_elements(class_node).each do |node|
    classification = classify(node)
    next if ignore?(classification)

    yield node, classification
  end
end