diff --git a/lib/rubocop/node_pattern.rb b/lib/rubocop/node_pattern.rb index 14e58a05ccb..f38ffda31f5 100644 --- a/lib/rubocop/node_pattern.rb +++ b/lib/rubocop/node_pattern.rb @@ -78,6 +78,8 @@ module RuboCop # # matching process starts # '^^send' # each ^ ascends one level in the AST # # so this matches against the grandparent node + # '`send' # descends any number of level in the AST + # # so this matches against any descendant node # '#method' # we call this a 'funcall'; it calls a method in the # # context where a pattern-matching method is defined # # if that returns a truthy value, the match succeeds @@ -112,7 +114,7 @@ class Compiler SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze META = Regexp.union( - %w"( ) { } [ ] $< < > $... $ ! ^ ... + * ?" + %w"( ) { } [ ] $< < > $... $ ! ^ ` ... + * ?" ).freeze NUMBER = /-?\d+(?:\.\d+)?/.freeze STRING = /".+?"/.freeze @@ -223,6 +225,7 @@ def compile_expr(token = tokens.shift) when '!' then compile_negation when '$' then compile_capture when '^' then compile_ascend + when '`' then compile_descend when WILDCARD then compile_wildcard(token[1..-1]) when FUNCALL then compile_funcall(token) when LITERAL then compile_literal(token) @@ -496,6 +499,19 @@ def compile_ascend with_context("#{CUR_NODE} && #{compile_expr}", "#{CUR_NODE}.parent") end + def compile_descend + with_temp_variables do |descendant| + pattern = with_context(compile_expr, descendant, + use_temp_node: false) + [ + "RuboCop::NodePattern.descend(#{CUR_ELEMENT}).", + "any? do |#{descendant}|", + " #{pattern}", + 'end' + ].join("\n") + end + end + def compile_wildcard(name) if name.empty? 'true' @@ -796,6 +812,22 @@ def ==(other) def to_s "#<#{self.class} #{pattern}>" end + + # Yields its argument and any descendants, depth-first. + # + def self.descend(element, &block) + return to_enum(__method__, element) unless block_given? + + yield element + + if element.is_a?(::RuboCop::AST::Node) + element.children.each do |child| + descend(child, &block) + end + end + + nil + end end end # rubocop:enable Metrics/ClassLength, Metrics/CyclomaticComplexity diff --git a/manual/node_pattern.md b/manual/node_pattern.md index 60aeaa8c66d..9c8f50128cc 100644 --- a/manual/node_pattern.md +++ b/manual/node_pattern.md @@ -230,6 +230,34 @@ For example, the previous example is basically the same as: ``` (pair ^^hash $_value) + +## `` ` `` for descendants + +The `` ` `` character can be used to search a node and all its descendants. +For example if looking for a `return` statement anywhere within a method definition, +we can write: + +``` +(def _method_name _args `return) +``` + +This would match both of these methods `foo` and `bar`, even though +these `return` for `foo` and `bar` are not at the same level. + +``` +def foo # (def :foo + return 42 # (args) +end # (return + # (int 42))) + +def bar # (def :bar + return 42 if foo # (args) + nil # (begin +end # (if + # (send nil :foo) + # (return + # (int 42)) nil) + # (nil))) ``` ## Predicate methods diff --git a/spec/rubocop/ast/node_spec.rb b/spec/rubocop/ast/node_spec.rb index 8c89804a862..d2071e246b2 100644 --- a/spec/rubocop/ast/node_spec.rb +++ b/spec/rubocop/ast/node_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true RSpec.describe RuboCop::AST::Node do - describe '#value_used?' do - let(:node) { RuboCop::ProcessedSource.new(src, ruby_version).ast } + let(:node) { RuboCop::ProcessedSource.new(src, ruby_version).ast } + describe '#value_used?' do before do module RuboCop module AST @@ -116,8 +116,6 @@ def used? end describe '#recursive_basic_literal?' do - let(:node) { RuboCop::ProcessedSource.new(src, ruby_version).ast } - shared_examples 'literal' do |source| let(:src) { source } @@ -164,8 +162,6 @@ def used? end describe '#pure?' do - let(:node) { RuboCop::ProcessedSource.new(src, ruby_version).ast } - context 'for a method call' do let(:src) { 'obj.method(arg1, arg2)' } @@ -337,8 +333,6 @@ def used? end describe '#sibling_index' do - let(:node) { RuboCop::ProcessedSource.new(src, ruby_version).ast } - let(:src) do [ 'def foo; end', diff --git a/spec/rubocop/node_pattern_spec.rb b/spec/rubocop/node_pattern_spec.rb index cd06147f8e3..a259b793e14 100644 --- a/spec/rubocop/node_pattern_spec.rb +++ b/spec/rubocop/node_pattern_spec.rb @@ -1534,6 +1534,46 @@ def withargs(foo, bar, qux) end end + describe 'descend' do + let(:ruby) { '[1, [[2, 3, [[5]]], 4]]' } + + context 'with an immediate match' do + let(:pattern) { '(array `$int _)' } + + let(:captured_val) { s(:int, 1) } + + it_behaves_like 'single capture' + end + + context 'with a match multiple levels, depth first' do + let(:pattern) { '(array (int 1) `$int)' } + + let(:captured_val) { s(:int, 2) } + + it_behaves_like 'single capture' + end + + context 'nested' do + let(:pattern) { '(array (int 1) `(array <`(array $int) ...>))' } + + let(:captured_val) { s(:int, 5) } + + it_behaves_like 'single capture' + end + + context 'with a literal match' do + let(:pattern) { '(array (int 1) `4)' } + + it_behaves_like 'matching' + end + + context 'without match' do + let(:pattern) { '(array `$str ...)' } + + it_behaves_like 'nonmatching' + end + end + describe 'bad syntax' do context 'with empty parentheses' do let(:pattern) { '()' } @@ -1607,4 +1647,20 @@ def withargs(foo, bar, qux) it_behaves_like 'invalid' end end + + describe '.descend' do + let(:ruby) { '[[1, 2], 3]' } + + it 'yields all children depth first' do + e = described_class.descend(node) + expect(e.instance_of?(Enumerator)).to be(true) + array, three = node.children + one, two = array.children + expect(e.to_a).to eq([node, array, one, 1, two, 2, three, 3]) + end + + it 'yields the given argument if it is not a Node' do + expect(described_class.descend(42).to_a).to eq([42]) + end + end end