Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NodePattern: Add ` for descendant search #7693

Merged
merged 1 commit into from Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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'
Expand Down Expand Up @@ -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
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 @@ -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) { '()' }
Expand Down Expand Up @@ -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