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:

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

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 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
Also aliased as: on_sclass
on_sclass(class_node)
Alias for: on_class

Private Instance Methods

autocorrect(corrector, node) click to toggle source

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
begin_pos_with_comment(node) click to toggle source
# 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
buffer() click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 328
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 340
def categories
  cop_config['Categories']
end
class_elements(class_node) click to toggle source
# 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
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 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
dynamic_constant?(node) click to toggle source
# 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
end_position_for(node) click to toggle source
# 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
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 334
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 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
find_heredoc(node) click to toggle source
# File lib/rubocop/cop/layout/class_structure.rb, line 324
def find_heredoc(node)
  node.each_node(:str, :dstr, :xstr).find(&:heredoc?)
end
humanize_node(node) click to toggle source
# 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
ignore?(node, classification) click to toggle source
# 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
ignore_for_autocorrect?(node, sibling) click to toggle source
# 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
marked_as_private_constant?(node, name) click to toggle source
# 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
private_constant?(node) click to toggle source
# 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
start_line_position(node) click to toggle source
# 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
walk_over_nested_class_definition(class_node) { |node, classification| ... } click to toggle source
# 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
whole_line_comment_at_line?(line) click to toggle source
# 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