123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Layout::ClassStructure

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ::RuboCop::Cop::AutoCorrector, ::RuboCop::Cop::Base, ::RuboCop::ExcludeLimit, NodePattern::Macros, RuboCop::AST::Sexp
Instance Chain:
Inherits: RuboCop::Cop::Base
Defined in: lib/rubocop/cop/layout/class_structure.rb

Overview

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

Note
Simply enabling the cop with Enabled: true will not use the example order shown below. To enforce the order of macros like attr_reader, you must define both ExpectedOrder and Categories.

You can configure the following order:

 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, it is also possible to group categories of macros. Visibility levels are handled automatically.

 Layout/ClassStructure:
   Categories:
     association:
       - has_many
       - has_one
     attribute_macros:
       - attr_accessor
       - attr_reader
       - attr_writer
     macros:
       - validates
       - validate
     module_inclusion:
       - include
       - prepend
       - extend

If you only set ExpectedOrder without defining Categories, macros such as attr_reader or has_many will not be recognized as part of a category, and their order will not be validated. For example, the following will NOT raise any offenses, even if the order is incorrect:

Layout/ClassStructure:
  Enabled: true
  ExpectedOrder:
    - public_attribute_macros
    - initializer

To make it work as expected, you must also specify Categories like this:

Layout/ClassStructure:
  ExpectedOrder:
    - public_attribute_macros
    - initializer
  Categories:
    attribute_macros:
      - attr_reader
      - attr_writer
      - attr_accessor

Examples:

# 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

Cop Safety Information:

  • 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.

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::RuboCop::Cop::VisibilityHelp - Included

VISIBILITY_SCOPES

Class Attribute Summary

::RuboCop::Cop::AutoCorrector - Extended

::RuboCop::Cop::Base - Inherited

.gem_requirements, .lint?,
.support_autocorrect?

Returns if class supports autocorrect.

.support_multiple_source?

Override if your cop should be called repeatedly for multiple investigations Between calls to on_new_investigation and on_investigation_end, the result of processed_source will remain constant.

Class Method Summary

::RuboCop::Cop::Base - Inherited

.autocorrect_incompatible_with

List of cops that should not try to autocorrect at the same time as this cop.

.badge

Naming.

.callbacks_needed, .cop_name, .department,
.documentation_url

Returns a url to view this cops documentation online.

.exclude_from_registry

Call for abstract Cop classes.

.inherited,
.joining_forces

Override and return the Force class(es) you need to join.

.match?

Returns true if the cop name or the cop namespace matches any of the given names.

.new,
.requires_gem

Register a version requirement for the given gem name.

.restrict_on_send

Reserved for Commissioner.

::RuboCop::ExcludeLimit - Extended

exclude_limit

Sets up a configuration option to have an exclude limit tracked.

transform

Instance Attribute Summary

Instance Method Summary

::RuboCop::Cop::CommentsHelp - Included

#comments_contain_disables?, #comments_in_range, #contains_comments?, #source_range_with_comment, #begin_pos_with_comment, #buffer, #end_position_for,
#find_end_line

Returns the end line of a node, which might be a comment and not part of the AST End line is considered either the line at which another node starts, or the line at which the parent node ends.

#start_line_position

::RuboCop::Cop::VisibilityHelp - Included

::RuboCop::Cop::Base - Inherited

#add_global_offense

Adds an offense that has no particular location.

#add_offense

Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense.

#begin_investigation

Called before any investigation.

#callbacks_needed,
#cop_config

Configuration Helpers.

#cop_name, #excluded_file?,
#external_dependency_checksum

This method should be overridden when a cop’s behavior depends on state that lives outside of these locations:

#inspect,
#message

Gets called if no message is specified when calling add_offense or add_global_offense Cops are discouraged to override this; instead pass your message directly.

#name

Alias for Base#cop_name.

#offenses,
#on_investigation_end

Called after all on_…​

#on_new_investigation

Called before all on_…​

#on_other_file

Called instead of all on_…​

#parse

There should be very limited reasons for a Cop to do it’s own parsing.

#parser_engine,
#ready

Called between investigations.

#relevant_file?,
#target_gem_version

Returns a gems locked versions (i.e.

#target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #covering_disabled_range, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?,
#enabled_lines?

A multi-line offense is suppressed by a directive on any line of its range, not only its first line, matching the intuition that the directive is attached to the offending code.

#file_name_matches_any?, #find_message, #find_severity, #matches_absolute_include_pattern?, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#suppression_reason

The -- reason on the directive that suppresses offenses on this range, or nil when the directive carries none.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

This class inherits a constructor from RuboCop::Cop::Base

Instance Method Details

#autocorrect(corrector, node) (private)

Autocorrect by moving the node, together with the contiguous group of same-category elements that follows it, to its expected position.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 231

def autocorrect(corrector, node)
  return if dynamic_constant?(node)

  anchor = insertion_anchor(node)
  return unless anchor

  anchor_range = source_range_with_comment(anchor)
  # Reversed for the same reason offenses are registered in reverse source order:
  # the last insertion at a position comes first.
  movable_group(node).reverse_each do |group_node|
    current_range = source_range_with_comment(group_node)
    corrector.insert_before(anchor_range, current_range.source)
    corrector.remove(current_range)
  end
end

#barrier?(node, sibling) ⇒ Boolean (private)

A dynamic constant blocks every element: the cop does not move such constants, and letting other elements jump over one would change execution order just the same. A bare visibility modifier blocks only the elements whose meaning depends on the visibility section they appear in.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 390

def barrier?(node, sibling)
  dynamic_constant?(sibling) || (visibility_dependent?(node) && visibility_block?(sibling))
end

#begin_pos_with_comment(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 433

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 (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 456

def buffer
  processed_source.buffer
end

#categories (private)

Setting categories hash allow you to group methods in group to match in the #expected_order.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 468

def categories
  cop_config['Categories']
end

#class_elements(class_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 301

def class_elements(class_node)
  class_def = class_node.body

  return [] unless class_def

  # Only a multi-statement body (`begin`/`kwbegin`) wraps several elements; any
  # single statement (`def`, `send`, `csend`, `if`, ...) is itself the sole element.
  # Exploding such a node into its children would yield non-node values (e.g. a
  # method-name `Symbol` from a `csend`) and crash later checks.
  if class_def.type?(:begin, :kwbegin)
    flatten_class_elements(class_def)
  else
    [class_def]
  end
end

#classify(node) ⇒ Object (private)

Classifies a node to match with something in the #expected_order

Parameters:

  • node

    to be analysed

Returns:

  • String when the node type is a :block then classify recursively with the first children

  • String when the node type is a :send then #find_send_node_category by method name

  • String otherwise trying to #humanize_node of the current node

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 254

def classify(node)
  return node.to_s unless node.respond_to?(:type)

  case node.type
  when :block
    classify(node.send_node)
  when :send
    find_send_node_category(node)
  else
    name = humanize_node(node)
    find_category(name) || name
  end.to_s
end

#dynamic_constant?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 339

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) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 423

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 (private)

Load expected order from ExpectedOrder config. Define new terms in the expected order by adding new #categories.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 462

def expected_order
  cop_config['ExpectedOrder']
end

#find_category(name) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 286

def find_category(name)
  name = name.to_s
  category, = categories.find { |_, names| names.include?(name) }
  category
end

#find_heredoc(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 452

def find_heredoc(node)
  node.each_node(:any_str).find(&:heredoc?)
end

#find_send_node_category(node) ⇒ String (private)

Categorize a node according to the #expected_order Try to match #categories values against the node’s method_name given also its visibility.

Parameters:

  • node

    to be analysed.

Returns:

  • (String)

    with the key category or the method_name as string

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 273

def find_send_node_category(node)
  name = node.method_name.to_s
  category = find_category(name)
  key = category || name
  visibility_key =
    if node.def_modifier?
      name.end_with?('_class_method') ? "#{name}s" : "#{name}_methods"
    else
      "#{node_visibility(node)}_#{key}"
    end
  expected_order.include?(visibility_key) ? visibility_key : key
end

#flatten_class_elements(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 324

def flatten_class_elements(node)
  node.children.compact.flat_map do |child|
    child.kwbegin_type? ? flatten_class_elements(child) : [child]
  end
end

#humanize_node(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 330

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) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 317

def ignore?(node, classification)
  classification.nil? ||
    classification.to_s.end_with?('=') ||
    expected_order.index(classification).nil? ||
    private_constant?(node)
end

#insertion_anchor(node) (private)

The expected position of the node: the first left sibling within its movable span whose category is expected to appear after the node’s. Requiring a strictly later category keeps the order of elements sharing a category stable.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 350

def insertion_anchor(node)
  index = expected_order.index(classify(node))

  movable_span(node).find do |sibling|
    classification = classify(sibling)

    !ignore?(sibling, classification) && expected_order.index(classification) > index
  end
end

#marked_as_private_constant?(node, name) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 417

def marked_as_private_constant?(node, name)
  return false unless node.method?(:private_constant)

  node.arguments.any? { |arg| arg.type?(:sym, :str) && arg.value == name }
end

#movable_group(node) (private)

The node together with the contiguous same-category right siblings, so that the whole group moves in a single pass while the offense is reported only on its first element.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 363

def movable_group(node)
  classification = classify(node)
  group = [node]

  node.right_siblings.each do |sibling|
    break unless classify(sibling) == classification
    break if ignore?(sibling, classification) || dynamic_constant?(sibling)

    group << sibling
  end

  group
end

#movable_span(node) (private)

Left siblings the node may be reordered with: those after the last barrier. Ignored elements within the span are simply jumped over.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 379

def movable_span(node)
  left_siblings = node.left_siblings
  barrier_index = left_siblings.rindex { |sibling| barrier?(node, sibling) }

  barrier_index ? left_siblings[(barrier_index + 1)..] : left_siblings
end

#on_class(class_node) Also known as: #on_sclass

Validates code style on class declaration. Add offense when find a node out of expected order. A node is out of order when its category is expected earlier than the highest-priority category seen so far, so that a low-priority element (even an unmovable one) cannot mask disorder among the elements that follow it. Consecutive elements of the same category are reported only once, on the first element of the group.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 198

def on_class(class_node)
  # Corrections are registered in reverse source order because an insertion at
  # a given position lands before any insertion already made there;
  # this keeps the source order of nodes moved before the same anchor.
  out_of_order_elements(class_node).reverse_each do |node, category, previous|
    message = format(MSG, category: category, previous: previous)

    add_offense(node, message: message) do |corrector|
      autocorrect(corrector, node)
    end
  end
end

#on_sclass(class_node)

Alias for #on_class.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 210

alias on_sclass on_class

#out_of_order_elements(class_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 214

def out_of_order_elements(class_node)
  out_of_order = []
  max_index = -1
  previous_category = nil
  walk_over_nested_class_definition(class_node) do |node, category|
    index = expected_order.index(category)
    if index < max_index && category != previous_category
      out_of_order << [node, category, expected_order[max_index]]
    end
    max_index = index if index > max_index
    previous_category = category
  end
  out_of_order
end

#private_constant?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 407

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) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 448

def start_line_position(node)
  buffer.line_range(node.loc.line).begin_pos - 1
end

#visibility_dependent?(node) ⇒ Boolean (private)

Whether moving the node across a bare visibility modifier would change its meaning. This is the case for def nodes and for macros whose category is classified by visibility (e.g. attr_accessor when the expected order lists private_attribute_macros). Inline visibility (private def foo, def self.foo) travels with the node.

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 398

def visibility_dependent?(node)
  return true if node.def_type?
  return false if !node.send_type? || node.def_modifier?

  key = find_category(node.method_name) || node.method_name.to_s

  VISIBILITY_SCOPES.any? { |visibility| expected_order.include?("#{visibility}_#{key}") }
end

#walk_over_nested_class_definition(class_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 292

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) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/layout/class_structure.rb', line 444

def whole_line_comment_at_line?(line)
  /\A\s*#/.match?(processed_source.lines[line - 1])
end