123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Style::ClassAndModuleChildren

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/style/class_and_module_children.rb

Overview

Checks that namespaced classes and modules are defined with a consistent style.

With nested style, classes and modules should be defined separately (one constant on each line, without ::). With compact style, classes and modules should be defined with fully qualified names (using :: for namespaces).

Note
The style chosen will affect Module.nesting for the class or module. Using nested style will result in each level being added, whereas compact style will only include the fully qualified class or module name.

By default, EnforcedStyle applies to both classes and modules. If desired, separate styles can be defined for classes and modules by using EnforcedStyleForClasses and EnforcedStyleForModules respectively. If not set, or set to nil, the EnforcedStyle value will be used.

The compact style is only forced for classes/modules with one child.

Examples:

EnforcedStyle: nested (default)

# bad
class Foo::Bar
end

# good
class Foo
  class Bar
  end
end

EnforcedStyle: compact

# bad
class Foo
  class Bar
  end
end

# good
class Foo::Bar
end

Cop Safety Information:

  • Autocorrection is unsafe.

    Moving from compact to nested children requires knowledge of whether the outer parent is a module or a class. Moving from nested to compact requires verification that the outer parent is defined elsewhere. By default RuboCop does not have the knowledge to perform either operation safely and thus requires manual oversight.

    When AllCops/UseProjectIndex is enabled and the rubydex gem is installed, the project-wide index is consulted to resolve whether the outer parent is a class or a module, and compacting is skipped when the outer parent is not defined elsewhere.

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

::RuboCop::Cop::Alignment - Included

SPACE

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

SYMBOL_TO_STRING_CACHE

::RuboCop::Cop::ProjectIndexHelp - Included

BUILTIN_DOCUMENT_URI, FILE_URI_PREFIX, WINDOWS_DRIVE_PREFIX

::RuboCop::Cop::RangeHelp - Included

BYTE_ORDER_MARK, NOT_GIVEN

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

::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::RangeHelp - Included

#add_range,
#arguments_range

A range containing the first to the last argument of a method call or method definition.

#column_offset_between,
#contents_range

A range containing only the contents of a literal with delimiters (e.g.

#directions,
#effective_column

Returns the column attribute of the range, except if the range is on the first line and there’s a byte order mark at the beginning of that line, in which case 1 is subtracted from the column value.

#final_pos, #move_pos, #move_pos_str, #range_between, #range_by_whole_lines, #range_with_comments, #range_with_comments_and_lines, #range_with_surrounding_comma, #range_with_surrounding_space, #source_range

::RuboCop::Cop::ProjectIndexHelp - Included

#external_dependency_checksum, #compute_project_index_signature,
#definitions_in_other_files

Returns the definitions among definitions that live in a file other than the one being inspected, ordered by path and line.

#indexed_singleton_member

A namespace without any singleton method has no singleton-class declaration of its own, so the lookup starts from the first ancestor that has one; its find_member covers the rest of the chain.

#indexed_singleton_of

The declaration of `declaration’s singleton class, or nil when no singleton method is defined on it anywhere in the project.

#inherited_index_member?

Whether an ancestor of scope other than scope itself defines member_name.

#lexical_nesting_of

The lexical nesting the node’s constants resolve through, outermost first.

#prior_definition_in_other_file, #project_index_signature,
#resolve_constant_in_index

Resolves a constant node the way Ruby does: the first segment through the lexical nesting and every following segment inside the previous one.

#same_file?

::RuboCop::Cop::ConfigurableEnforcedStyle - Included

::RuboCop::Cop::Alignment - 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, #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, #matches_absolute_include_pattern?, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

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

Instance Method Details

#add_trailing_end(corrector, node, padding) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 136

def add_trailing_end(corrector, node, padding)
  replacement = "#{padding}end\n#{leading_spaces(node)}end"
  corrector.replace(node.loc.end, replacement)
end

#autocorrect(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 276

def autocorrect(corrector, node)
  return if node.class_type? && node.parent_class && style_for_classes != :nested

  nest_or_compact(corrector, node)
end

#check_compact_style(node, body) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 265

def check_compact_style(node, body)
  parent = node.parent
  return if parent&.type?(:class, :module)

  return unless needs_compacting?(body)

  add_offense(node.loc.name, message: COMPACT_MSG) do |corrector|
    autocorrect(corrector, node)
  end
end

#check_nested_style(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 256

def check_nested_style(node)
  return unless compact_node_name?(node)
  return if node.parent&.type?(:class, :module)

  add_offense(node.loc.name, message: NESTED_MSG) do |corrector|
    autocorrect(corrector, node)
  end
end

#check_style(node, body, style) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 238

def check_style(node, body, style)
  return if node.identifier.namespace&.cbase_type?
  return unless const_namespace?(node.identifier.namespace)

  if style == :nested
    check_nested_style(node)
  else
    check_compact_style(node, body)
  end
end

#compact_definition(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 141

def compact_definition(corrector, node)
  # Compacting produces a definition whose type's style resolves to `nested`,
  # making autocorrection ping-pong between the two forms.
  return if style_for_kind(node.body.type) == :nested
  return unless compactible_namespace?(node)

  compact_node(corrector, node)
  remove_end(corrector, node.body)
  unindent(corrector, node)
end

#compact_identifier_name(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 192

def compact_identifier_name(node)
  "#{node.identifier.const_name}::" \
    "#{node.body.children.first.const_name}"
end

#compact_node(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 177

def compact_node(corrector, node)
  range = range_between(node.loc.keyword.begin_pos, node.body.loc.name.end_pos)
  corrector.replace(range, compact_replacement(node))
end

#compact_node_name?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 286

def compact_node_name?(node)
  node.identifier.source.include?('::')
end

#compact_replacement(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 182

def compact_replacement(node)
  replacement = "#{node.body.type} #{compact_identifier_name(node)}"

  body_comments = processed_source.ast_with_comments[node.body]
  unless body_comments.empty?
    replacement = body_comments.map(&:text).push(replacement).join("\n")
  end
  replacement
end

#compactible_namespace?(node) ⇒ Boolean (private)

Compacting removes this definition of the namespace, so the result raises NameError at load time unless the namespace is also defined somewhere else. With the project index this is verified before correcting; without it the correction is performed regardless (the cop’s autocorrection is unsafe).

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 156

def compactible_namespace?(node)
  return true unless project_index

  declaration = resolve_constant_in_index(node.identifier)
  return false unless declaration.is_a?(Rubydex::Namespace)

  declaration.definitions.any? { |definition| definition_elsewhere?(definition, node) }
end

#const_namespace?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 249

def const_namespace?(node)
  return true if node.nil? || node.cbase_type?
  return false unless node.const_type?

  const_namespace?(node.namespace)
end

#definition_elsewhere?(definition, node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 165

def definition_elsewhere?(definition, node)
  location = definition.location
  return true unless location.uri.start_with?(FILE_URI_PREFIX)

  !same_file?(location.to_file_path, processed_source.file_path) ||
    location.to_display.start_line != node.first_line
rescue StandardError
  # A path that cannot be converted or compared cannot prove the
  # namespace is defined elsewhere; err on not correcting.
  false
end

#heuristic_namespace_keyword(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 119

def heuristic_namespace_keyword(node)
  class_definition = node.left_sibling&.each_node(:class)&.find do |class_node|
    class_node.identifier == node.identifier.namespace
  end

  class_definition ? 'class' : 'module'
end

#indexed_namespace_keyword(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 108

def indexed_namespace_keyword(node)
  return nil unless project_index

  declaration = resolve_constant_in_index(node.identifier.namespace)

  case declaration
  when Rubydex::Class then 'class'
  when Rubydex::Module then 'module'
  end
end

#leading_spaces(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 225

def leading_spaces(node)
  node.source_range.source_line[/\A\s*/]
end

#namespace_keyword(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 104

def namespace_keyword(node)
  indexed_namespace_keyword(node) || heuristic_namespace_keyword(node)
end

#needs_compacting?(body) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 282

def needs_compacting?(body)
  body && %i[module class].include?(body.type)
end

#nest_definition(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 90

def nest_definition(corrector, node)
  keyword = namespace_keyword(node)
  # A namespace wrapper whose style resolves to `compact` would itself violate
  # that style, making autocorrection ping-pong between the two forms.
  return if style_for_kind(keyword.to_sym) == :compact

  padding = indentation(node) + leading_spaces(node)
  padding_for_trailing_end = padding.sub(' ' * node.loc.end.column, '')

  corrector.replace(node.loc.keyword, keyword)
  split_on_double_colon(corrector, node, padding)
  add_trailing_end(corrector, node, padding_for_trailing_end)
end

#nest_or_compact(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 80

def nest_or_compact(corrector, node)
  style = style_for_kind(node.type)

  if style == :nested
    nest_definition(corrector, node)
  else
    compact_definition(corrector, node)
  end
end

#on_class(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 68

def on_class(node)
  return if node.parent_class && style_for_classes != :nested

  check_style(node, node.body, style_for_classes)
end

#on_module(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 74

def on_module(node)
  check_style(node, node.body, style_for_modules)
end

#remove_end(corrector, body) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 198

def remove_end(corrector, body)
  remove_begin_pos = if same_line?(body.loc.name, body.loc.end)
                       body.loc.name.end_pos
                     else
                       body.loc.end.begin_pos - leading_spaces(body).size
                     end
  adjustment = processed_source.raw_source[remove_begin_pos] == ';' ? 0 : 1
  range = range_between(remove_begin_pos, body.loc.end.end_pos + adjustment)

  corrector.remove(range)
end

#spaces_size(spaces_string) (private)

A tab counts as configured_indentation_width columns, matching the width ::RuboCop::Cop::AlignmentCorrector uses to convert column_delta back into tabs. Using `Layout/IndentationStyle’s own IndentationWidth here would make the delta and the correction disagree.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 233

def spaces_size(spaces_string)
  mapping = { "\t" => configured_indentation_width }
  spaces_string.chars.sum { |character| mapping.fetch(character, 1) }
end

#split_on_double_colon(corrector, node, padding) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 127

def split_on_double_colon(corrector, node, padding)
  children_definition = node.children.first
  range = range_between(children_definition.loc.double_colon.begin_pos,
                        children_definition.loc.double_colon.end_pos)
  replacement = "\n#{padding}#{node.loc.keyword.source} "

  corrector.replace(range, replacement)
end

#style_for_classes (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 294

def style_for_classes
  cop_config['EnforcedStyleForClasses']&.to_sym || style
end

#style_for_kind(kind) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 290

def style_for_kind(kind)
  kind == :class ? style_for_classes : style_for_modules
end

#style_for_modules (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 298

def style_for_modules
  cop_config['EnforcedStyleForModules']&.to_sym || style
end

#unindent(corrector, node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/class_and_module_children.rb', line 211

def unindent(corrector, node)
  return unless node.body.children.last

  last_child_leading_spaces = leading_spaces(node.body.children.last)
  return if spaces_size(leading_spaces(node)) == spaces_size(last_child_leading_spaces)

  column_delta = configured_indentation_width - spaces_size(last_child_leading_spaces)
  return if column_delta.zero?

  AlignmentCorrector.correct(
    corrector, processed_source, node, column_delta, tab_indentation: true
  )
end