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)
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)
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)
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)
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)
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)
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)
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