class RuboCop::Cop::InternalAffairs::NodePatternGroups
Use node groups (‘any_block`, `argument`, `boolean`, `call`, `numeric`, `range`) in node patterns instead of a union (`{ … }`) of the member types of the group.
@example
# bad def_node_matcher :my_matcher, <<~PATTERN {send csend} PATTERN # good def_node_matcher :my_matcher, <<~PATTERN call PATTERN
rubocop:disable InternalAffairs/RedundantSourceRange – node here is a ‘NodePattern::Node`
Constants
- MSG
- NODE_GROUPS
- RESTRICT_ON_SEND
Public Instance Methods
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 80 def after_send(_) @walker.reset! end
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 40 def on_new_investigation @walker = ASTWalker.new end
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 66 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
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.
Private Instance Methods
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 212 def acceptable_heredoc?(node) node.type?(:str, :dstr) && node.heredoc? && node.each_child_node(:begin).none? end
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.
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 176 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
rubocop:disable Metrics/AbcSize 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.
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 86 def node_groups @walker.node_groups end
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 194 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
rubocop:enable Metrics/AbcSize
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 225 def pattern_value(pattern_node) pattern_node.heredoc? ? pattern_node.loc.heredoc_body.source : pattern_node.value end
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 216 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
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 141 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
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.
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 202 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
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 154 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
Collect a preceding pipe and any whitespace left of the pipe
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 91 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
rubocop:disable InternalAffairs/RedundantSourceRange – ‘node` here is a NodePatternNode
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 127 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
When there are other elements in the union, remove the node types that can be replaced.
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 169 def replace_union(corrector, group, replacement) corrector.replace(group.ranges.first, replacement) end
When there are no other elements, the entire union can be replaced
Source
# File lib/rubocop/cop/internal_affairs/node_pattern_groups.rb, line 113 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