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: Allow full expressions as function arguments #63

Merged
merged 1 commit into from Jul 13, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@

* [#50](https://github.com/rubocop-hq/rubocop-ast/pull/50): Support find pattern matching for Ruby 2.8 (3.0) parser. ([@koic][])
* [#55](https://github.com/rubocop-hq/rubocop-ast/pull/55): Add `ProcessedSource#line_with_comment?`. ([@marcandre][])
* [#63](https://github.com/rubocop-hq/rubocop-ast/pull/63): NodePattern now supports patterns as arguments to predicate and functions. ([@marcandre][])

### Bug fixes

Expand Down
28 changes: 26 additions & 2 deletions docs/modules/ROOT/pages/node_pattern.adoc
Expand Up @@ -292,7 +292,7 @@ You can also use it at the node level, asking for each child:
* `(int odd?)` will match only with odd numbers, asking it to the current
number.

== `#` to call external methods
== `#` to call functions

Sometimes, we want to add extra logic. Let's imagine we're searching for
prime numbers, so we have a method to detect it:
Expand All @@ -310,12 +310,36 @@ def prime?(n)
end
----

We can use the `#prime?` method directly in the expression:
We can use the `#prime?` function directly in the expression:

----
(int #prime?)
----

== Arguments for predicate and function calls

Arguments can be passed to predicates and function calls, like literals, parameters:

[source,ruby]
----
def divisible_by?(value, divisor)
value % divisor == 0
end
----

Example patterns using this function:
----
(int #divisible_by?(42))
(send (int _value) :+ (int #divisible_by?(_value))
----

The arguments can be pattern themselves, in which case a matcher responding to `===` will be passed. This makes patterns composable:

```ruby
def_node_pattern :global_const?, '(const {nil? cbase} %1)'
def_node_pattern :class_creator, '(send #global_const?({:Class :Module}) :new ...)'
```

== Using node matcher macros

The RuboCop base includes two useful methods to use the node pattern with Ruby in a
Expand Down
23 changes: 22 additions & 1 deletion lib/rubocop/ast/node_pattern.rb
Expand Up @@ -96,6 +96,9 @@ module AST
# # if that returns a truthy value, the match succeeds
# 'equal?(%1)' # predicates can be given 1 or more extra args
# '#method(%0, 1)' # funcalls can also be given 1 or more extra args
# # These arguments can be patterns themselves, in
# # which case a matcher responding to === will be
# # passed.
#
# You can nest arbitrarily deep:
#
Expand Down Expand Up @@ -626,6 +629,13 @@ def atom_to_expr(atom)
"#{atom} === #{CUR_ELEMENT}"
end

def expr_to_atom(expr)
with_temp_variables do |compare|
in_context = with_context(expr, compare, use_temp_node: false)
"::RuboCop::AST::NodePattern::Matcher.new{|#{compare}| #{in_context}}"
end
end

# @return compiled atom (e.g. ":literal" or "SOME_CONST")
# or nil if not a simple atom (unknown wildcard, other tokens)
def compile_atom(token)
Expand All @@ -642,7 +652,7 @@ def compile_atom(token)

def compile_arg
token = tokens.shift
compile_atom(token) || fail_due_to("invalid in arglist: #{token.inspect}")
compile_atom(token) || expr_to_atom(compile_expr(token))
end

def next_capture
Expand Down Expand Up @@ -911,6 +921,17 @@ def self.descend(element, &block)

nil
end

# @api private
class Matcher
def initialize(&block)
@block = block
end

def ===(compare)
@block.call(compare)
end
end
end
end
end
Expand Down
22 changes: 22 additions & 0 deletions spec/rubocop/ast/node_pattern_spec.rb
Expand Up @@ -1181,6 +1181,28 @@
end
end

context 'with an expression argument' do
before do
def instance.some_function(node, arg)
arg === node # rubocop:disable Style/CaseEquality
end
end

let(:pattern) { '(send (int _value) :+ #some_function( {(int _value) (float _value)} ) )' }

context 'for which the predicate is true' do
let(:ruby) { '2 + 2.0' }

it_behaves_like 'matching'
end

context 'for which the predicate is false' do
let(:ruby) { '2 + 3.0' }

it_behaves_like 'nonmatching'
end
end

context 'with multiple arguments' do
let(:pattern) { '(str between?(%1, %2))' }
let(:ruby) { '"c"' }
Expand Down