class RuboCop::Cop::InternalAffairs::NodePatternGroups


PATTERN
call
def_node_matcher :my_matcher, <<~PATTERN
# good
PATTERN
{send csend}
def_node_matcher :my_matcher, <<~PATTERN
# bad
@example
in node patterns instead of a union (‘{ … }`) of the member types of the group.
Use node groups (`any_block`, `argument`, `boolean`, `call`, `numeric`, `range`)

def acceptable_heredoc?(node)

there'll be a `begin` node, in which case, we cannot evaluate the pattern.
A heredoc can be a `dstr` without interpolation, but if there is interpolation
def acceptable_heredoc?(node)
  node.type?(:str, :dstr) && node.heredoc? && node.each_child_node(:begin).none?
end

def after_send(_)

def after_send(_)
  @walker.reset!
end

def apply_range_offsets(pattern_node)

be replaced or removed. Takes the offset of the string node into account.
Calculate the ranges for each node within the pattern string that will
rubocop:disable Metrics/AbcSize
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

def node_groups

def node_groups
  @walker.node_groups
end

def on_new_investigation

def on_new_investigation
  @walker = ASTWalker.new
end

def on_send(node)


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

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

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

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

replaced with `call`).
for node types that can be replaced with a node group (ie. `{send csend}` can be
When a Node Pattern matcher is defined, investigate the pattern string to search
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

def pattern_range(range, node, offset)

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

def pattern_value(pattern_node)

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

def process_pattern(pattern_node)

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

def range_for_full_union_element(range, index, pipe)

to find it.
Unfortunately we don't get the location of the pipe in `loc` object, so we have
If the union contains pipes, remove the pipe character as well.
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

def range_with_offset(pattern_node)

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

def range_with_preceding_pipe(range)

Collect a preceding pipe and any whitespace left of the pipe
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

def register_offense(group, index)

rubocop:disable InternalAffairs/RedundantSourceRange -- `node` here is a NodePatternNode
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

def replace_types_with_node_group(corrector, group, replacement)

When there are other elements in the union, remove the node types that can be replaced.
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

def replace_union(corrector, group, replacement)

When there are no other elements, the entire union can be replaced
def replace_union(corrector, group, replacement)
  corrector.replace(group.ranges.first, replacement)
end

def replacement(group)

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