123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::InternalAffairs::NodePatternGroups

Relationships & Source Files
Namespace Children
Classes:
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/internal_affairs/node_pattern_groups.rb,
lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_processor.rb,
lib/rubocop/cop/internal_affairs/node_pattern_groups/ast_walker.rb

Overview

Use node groups (any_block, argument, boolean, call, numeric, range) in node patterns instead of a union ({ …​ }) of the member types of the group.

Examples:

# bad
def_node_matcher :my_matcher, <<~PATTERN
  {send csend}
PATTERN

# good
def_node_matcher :my_matcher, <<~PATTERN
  call
PATTERN

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

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

#acceptable_heredoc?(node) ⇒ Boolean (private)

A heredoc can be a dstr without interpolation, but if there is interpolation there’ll be a begin node, in which case, we cannot evaluate the pattern.

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 210

def acceptable_heredoc?(node)
  node.type?(:str, :dstr) && node.heredoc? && node.each_child_node(:begin).none?
end

#after_send(_)

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 78

def after_send(_)
  @walker.reset!
end

#apply_range_offsets(pattern_node) (private)

Calculate the ranges for each node within the pattern string that will be replaced or removed. Takes the offset of the string node into account.

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 174

def apply_range_offsets(pattern_node)
  range, offset = range_with_offset(pattern_node)

  node_groups.each do |node_group|
    node_group.ranges ||= []
    node_group.offense_range = pattern_range(range, node_group.union, offset)

    if node_group.other_elements?
      node_group.node_types.each do |node_type|
        node_group.ranges << pattern_range(range, node_type, offset)
      end
    else
      node_group.ranges << node_group.offense_range
    end
  end
end

#node_groups (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 84

def node_groups
  @walker.node_groups
end

#on_new_investigation

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 38

def on_new_investigation
  @walker = ASTWalker.new
end

#on_send(node)

When a Node Pattern matcher is defined, investigate the pattern string to search for node types that can be replaced with a node group (ie. {send csend} can be replaced with call).

In order to deal with node patterns in an efficient and non-brittle way, we will parse the Node Pattern string given to this send node using RuboCop::AST::NodePattern::Parser::WithMeta. WithMeta is important! We need location information so that we can calculate the exact locations within the pattern to report and correct.

The resulting AST is processed by NodePatternGroups::ASTProccessor which rewrites the AST slightly to handle node sequences (ie. (send _ :foo …​)). See the documentation of that class for more details.

Then the processed AST is walked, and metadata is collected for node types that can be replaced with a node group.

Finally, the metadata is used to register offenses and make corrections, using the location data captured earlier. The ranges captured while parsing the Node Pattern are offset using the string argument to this send node to ensure that offenses are registered at the correct location.

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 64

def on_send(node)
  pattern_node = node.arguments[1]
  return unless acceptable_heredoc?(pattern_node) || pattern_node.str_type?

  process_pattern(pattern_node)
  return if node_groups.nil?

  apply_range_offsets(pattern_node)

  node_groups.each_with_index do |group, index|
    register_offense(group, index)
  end
end

#pattern_range(range, node, offset) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 192

def pattern_range(range, node, offset)
  begin_pos = node.source_range.begin_pos
  end_pos = node.source_range.end_pos
  size = end_pos - begin_pos

  range.adjust(begin_pos: begin_pos + offset).resize(size)
end

#pattern_value(pattern_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 223

def pattern_value(pattern_node)
  pattern_node.heredoc? ? pattern_node.loc.heredoc_body.source : pattern_node.value
end

#process_pattern(pattern_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 214

def process_pattern(pattern_node)
  parser = RuboCop::AST::NodePattern::Parser::WithMeta.new
  ast = parser.parse(pattern_value(pattern_node))
  ast = ASTProcessor.new.process(ast)
  @walker.walk(ast)
rescue RuboCop::AST::NodePattern::Invalid
  # if the pattern is invalid, no offenses will be registered
end

#range_for_full_union_element(range, index, pipe) (private)

If the union contains pipes, remove the pipe character as well. Unfortunately we don’t get the location of the pipe in loc object, so we have to find it.

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 139

def range_for_full_union_element(range, index, pipe)
  if index.positive?
    range = if pipe
              range_with_preceding_pipe(range)
            else
              range_with_surrounding_space(range: range, side: :left, newlines: true)
            end
  end

  range
end

#range_with_offset(pattern_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 200

def range_with_offset(pattern_node)
  if pattern_node.heredoc?
    [pattern_node.loc.heredoc_body, 0]
  else
    [pattern_node.source_range, pattern_node.loc.begin.size]
  end
end

#range_with_preceding_pipe(range) (private)

Collect a preceding pipe and any whitespace left of the pipe

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 152

def range_with_preceding_pipe(range)
  pos = range.begin_pos - 1

  while pos
    unless processed_source.buffer.source[pos].match?(/[\s|]/)
      return range.with(begin_pos: pos + 1)
    end

    pos -= 1
  end

  range
end

#register_offense(group, index) (private)

 — node here is a NodePatternNode

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 89

def register_offense(group, index)
  replacement = replacement(group)
  message = format(
    MSG,
    names: group.node_types.map { |node| node.source_range.source }.join('`, `'),
    replacement: replacement
  )

  add_offense(group.offense_range, message: message) do |corrector|
    # Only correct one group at a time to avoid clobbering.
    # Other offenses will be corrected in the subsequent iterations of the
    # correction loop.
    next if index.positive?

    if group.other_elements?
      replace_types_with_node_group(corrector, group, replacement)
    else
      replace_union(corrector, group, replacement)
    end
  end
end

#replace_types_with_node_group(corrector, group, replacement) (private)

When there are other elements in the union, remove the node types that can be replaced.

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 125

def replace_types_with_node_group(corrector, group, replacement)
  ranges = group.ranges.map.with_index do |range, index|
    # Collect whitespace and pipes preceding each element
    range_for_full_union_element(range, index, group.pipe)
  end

  ranges.each { |range| corrector.remove(range) }

  corrector.insert_before(ranges.first, replacement)
end

#replace_union(corrector, group, replacement) (private)

When there are no other elements, the entire union can be replaced

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 167

def replace_union(corrector, group, replacement)
  corrector.replace(group.ranges.first, replacement)
end

#replacement(group) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/internal_affairs/node_pattern_groups.rb', line 111

def replacement(group)
  if group.sequence?
    # If the original nodes were in a sequence (ie. wrapped in parentheses),
    # use it to generate the resulting NodePattern syntax.
    first_node_type = group.node_types.first
    template = first_node_type.source_range.source
    template.sub(first_node_type.child.to_s, group.name.to_s)
  else
    group.name
  end
end