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
Constant Summary
-
HUMANIZED_NODE_TYPE =
# File 'lib/rubocop/cop/layout/class_structure.rb', line 182{ casgn: :constants, defs: :public_class_methods, def: :public_methods, sclass: :class_singleton }.freeze -
MSG =
# File 'lib/rubocop/cop/layout/class_structure.rb', line 189'`%<category>s` is supposed to appear before `%<previous>s`.'
::RuboCop::Cop::Base - Inherited
EMPTY_OFFENSES, RESTRICT_ON_SEND
::RuboCop::Cop::VisibilityHelp - Included
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 |
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
::RuboCop::Cop::Base - Inherited
::RuboCop::Cop::AutocorrectLogic - Included
| #autocorrect?, #autocorrect_enabled?, #autocorrect_requested?, #autocorrect_with_disable_uncorrectable?, #correctable?, #disable_uncorrectable?, #safe_autocorrect?, | |
| #skipped_unsafe_correction_with_disable_uncorrectable? | Whether a correction exists but is skipped because it is unsafe in a safe autocorrect run, while |
Instance Method Summary
-
#on_class(class_node)
(also: #on_sclass)
Validates code style on class declaration.
-
#on_sclass(class_node)
Alias for #on_class.
-
#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.
-
#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.
- #begin_pos_with_comment(node) private
- #buffer private
-
#categories
private
Setting categories hash allow you to group methods in group to match in the #expected_order.
- #class_elements(class_node) private
-
#classify(node) ⇒ Object
private
Classifies a node to match with something in the #expected_order
- #dynamic_constant?(node) ⇒ Boolean private
- #end_position_for(node) private
-
#expected_order
private
Load expected order from
ExpectedOrderconfig. - #find_category(name) private
- #find_heredoc(node) private
-
#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.
- #flatten_class_elements(node) private
- #humanize_node(node) private
- #ignore?(node, classification) ⇒ Boolean private
-
#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.
- #marked_as_private_constant?(node, name) ⇒ Boolean private
-
#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.
-
#movable_span(node)
private
Left siblings the node may be reordered with: those after the last barrier.
- #out_of_order_elements(class_node) private
- #private_constant?(node) ⇒ Boolean private
- #start_line_position(node) private
-
#visibility_dependent?(node) ⇒ Boolean
private
Whether moving the node across a bare visibility modifier would change its meaning.
- #walk_over_nested_class_definition(class_node) private
- #whole_line_comment_at_line?(line) ⇒ Boolean private
::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 |
| #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 |
| #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.
# 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.
# File 'lib/rubocop/cop/layout/class_structure.rb', line 390
def (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.
# 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
# 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)
# 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.
# 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.
# 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)
# 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.
# 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)
# 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.
# 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.
# File 'lib/rubocop/cop/layout/class_structure.rb', line 379
def movable_span(node) left_siblings = node.left_siblings = left_siblings.rindex { |sibling| (node, sibling) } ? left_siblings[( + 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.
# 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| = format(MSG, category: category, previous: previous) add_offense(node, message: ) do |corrector| autocorrect(corrector, node) end end end
#on_sclass(class_node)
Alias for #on_class.
# 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)
# 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.
# 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)
# 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