Skip to content

Commit

Permalink
NodePattern: Allow full expressions as function arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
marcandre committed Jul 11, 2020
1 parent 77b50ac commit bd3102c
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
### New features

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

## 0.1.0 (2020-06-26)

Expand Down
24 changes: 24 additions & 0 deletions docs/modules/ROOT/pages/node_pattern.adoc
Expand Up @@ -316,6 +316,30 @@ We can use the `#prime?` method directly in the expression:
(int #prime?)
----

== Arguments for predicate and external methods

Arguments can be passed to predicates and external methods, 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 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

0 comments on commit bd3102c

Please sign in to comment.