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
DEPARTMENT_SEVERITIES, 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
-
.autocorrect_incompatible_with
A moved element is inserted before the line break that precedes its anchor.
::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
| #active_support_extensions_enabled?, #always_autocorrect?, #config, #config_to_allow_offenses, #config_to_allow_offenses=, #contextual_autocorrect?, #disabled_autocorrect?, | |
| #preview? | Whether the user opted in to unstable behavior, with |
| #processed_source, #project_index, #string_literals_frozen_by_default?, #target_satisfies_all_gem_version_requirements? | |
::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 |
| #contextual_autocorrect_held_back? | When LSP is enabled or the |
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
-
#class_method_category(node)
private
Only
private_class_methodreaches a singleton method; a bareprivatedoes not. - #class_method_visibility_modifier?(node, method_name:) 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_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
- #heredocs(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
| #find_visibility_end | Navigate to find the last protected method. |
| #find_visibility_start, #node_visibility, #node_visibility_from_visibility_block, #node_visibility_from_visibility_inline, #node_visibility_from_visibility_inline_on_def, #node_visibility_from_visibility_inline_on_method_name, #visibility_block?, #visibility_inline_on_def?, | |
| #visibility_inline_on_method_name? | A modifier applies to every symbol it is given, not only to a sole argument. |
::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 | — grouped under the Commissioner heading above. |
| #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 the version of a gem in the target, by default its locked version (i.e. from Gemfile.lock or gems.locked). |
| #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?, #new_offense, #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
Class Method Details
.autocorrect_incompatible_with
A moved element is inserted before the line break that precedes its anchor.
When the anchor follows a blank line, Layout/EmptyLines,
Layout/EmptyLinesAroundAccessModifier or Layout/EmptyLinesAroundClassBody
may remove that line break in the same pass.
# File 'lib/rubocop/cop/layout/class_structure.rb', line 195
def self.autocorrect_incompatible_with [Layout::EmptyLines, Layout::EmptyLinesAroundAccessModifier, Layout::EmptyLinesAroundClassBody] end
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 239
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 415
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 455
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 478
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 490
def categories cop_config['Categories'] end
#class_elements(class_node) (private)
[ GitHub ]# File 'lib/rubocop/cop/layout/class_structure.rb', line 309
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
#class_method_category(node) (private)
Only private_class_method reaches a singleton method; a bare private does not.
Such a call follows the definition it names, so only right siblings are searched.
# File 'lib/rubocop/cop/layout/class_structure.rb', line 351
def class_method_category(node) modifier = node.right_siblings.reverse.find do |sibling| class_method_visibility_modifier?(sibling, method_name: node.method_name) end modifier ? :"#{modifier.method_name}s" : HUMANIZED_NODE_TYPE[node.type] end
#class_method_visibility_modifier?(node, method_name:) (private)
[ GitHub ]# File 'lib/rubocop/cop/layout/class_structure.rb', line 360
def_node_matcher :class_method_visibility_modifier?, <<~PATTERN (send nil? {:private_class_method :public_class_method} <(sym %method_name) ...>) PATTERN
#classify(node) ⇒ Object (private)
Classifies a node to match with something in the #expected_order
# File 'lib/rubocop/cop/layout/class_structure.rb', line 262
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 364
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 ]#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 484
def expected_order cop_config['ExpectedOrder'] end
#find_category(name) (private)
[ GitHub ]# File 'lib/rubocop/cop/layout/class_structure.rb', line 294
def find_category(name) name = name.to_s category, = categories.find { |_, names| names.include?(name) } category 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 281
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 332
def flatten_class_elements(node) node.children.compact.flat_map do |child| child.kwbegin_type? ? flatten_class_elements(child) : [child] end end
#heredocs(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/layout/class_structure.rb', line 474
def heredocs(node) node.each_node(:any_str).select(&:heredoc?) end
#humanize_node(node) (private)
[ GitHub ]# File 'lib/rubocop/cop/layout/class_structure.rb', line 338
def humanize_node(node) if node.def_type? return :initializer if node.method?(:initialize) return "#{node_visibility(node)}_methods" end return class_method_category(node) if node.defs_type? HUMANIZED_NODE_TYPE[node.type] || node.type end
#ignore?(node, classification) ⇒ Boolean (private)
# File 'lib/rubocop/cop/layout/class_structure.rb', line 325
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 375
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 442
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 388
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 404
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 206
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 218
alias on_sclass on_class
#out_of_order_elements(class_node) (private)
[ GitHub ]# File 'lib/rubocop/cop/layout/class_structure.rb', line 222
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 432
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 470
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 423
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 300
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 466
def whole_line_comment_at_line?(line) /\A\s*#/.match?(processed_source.lines[line - 1]) end