Skip to content

Commit

Permalink
NodePattern: Add ` for descendant search [#7693]
Browse files Browse the repository at this point in the history
  • Loading branch information
marcandre authored and bbatsov committed Feb 10, 2020
1 parent e672f90 commit 4ac09e6
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 9 deletions.
34 changes: 33 additions & 1 deletion lib/rubocop/node_pattern.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -551,6 +554,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'
Expand Down Expand Up @@ -850,6 +866,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
28 changes: 28 additions & 0 deletions manual/node_pattern.md
Expand Up @@ -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
Expand Down
10 changes: 2 additions & 8 deletions 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
Expand Down Expand Up @@ -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 }

Expand Down Expand Up @@ -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)' }

Expand Down Expand Up @@ -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',
Expand Down
56 changes: 56 additions & 0 deletions spec/rubocop/node_pattern_spec.rb
Expand Up @@ -1638,6 +1638,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) { '()' }
Expand Down Expand Up @@ -1711,4 +1751,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

0 comments on commit 4ac09e6

Please sign in to comment.