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

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, 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

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.

.builtin?

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

Cops (other than builtin) are encouraged to implement this.

.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

::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_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #range_for_original, #range_from_node_or_range, #reset_investigation, #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 swapping between two nodes autocorrecting them

[ GitHub ]

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

[ GitHub ]

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

[ GitHub ]

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

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 340

def categories
  cop_config['Categories']
end

#class_elements(class_node) (private)

[ GitHub ]

  
# 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) ⇒ 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_category by method name

  • String otherwise trying to #humanize_node of the current node

[ GitHub ]

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

[ GitHub ]

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

[ GitHub ]

  
# 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 (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 334

def expected_order
  cop_config['ExpectedOrder']
end

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

[ GitHub ]

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

[ GitHub ]

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

[ GitHub ]

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

[ GitHub ]

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

[ GitHub ]

  
# 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

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

[ GitHub ]

  
# 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

#on_sclass(class_node)

Alias for #on_class.

[ GitHub ]

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

alias on_sclass on_class

#private_constant?(node) ⇒ Boolean (private)

[ GitHub ]

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

[ GitHub ]

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

[ GitHub ]

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

[ GitHub ]

  
# 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