From cf4009ea829e06f0cef1527d2db750ca15d51ee6 Mon Sep 17 00:00:00 2001 From: Marc-Andre Lafortune Date: Thu, 9 Jul 2020 01:11:12 -0400 Subject: [PATCH] NodePattern: Allow full expressions as function arguments --- CHANGELOG.md | 1 + docs/modules/ROOT/pages/node_pattern.adoc | 24 +++++++++++++++++++++++ lib/rubocop/ast/node_pattern.rb | 23 +++++++++++++++++++++- spec/rubocop/ast/node_pattern_spec.rb | 22 +++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8bdb0527..371ad9f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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][]) +* [#x](https://github.com/rubocop-hq/rubocop-ast/pull/x): NodePattern now supports patterns as arguments to predicate and functions. ([@marcandre][]) ## 0.1.0 (2020-06-26) diff --git a/docs/modules/ROOT/pages/node_pattern.adoc b/docs/modules/ROOT/pages/node_pattern.adoc index 401f60573..d9bd2ae22 100644 --- a/docs/modules/ROOT/pages/node_pattern.adoc +++ b/docs/modules/ROOT/pages/node_pattern.adoc @@ -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 diff --git a/lib/rubocop/ast/node_pattern.rb b/lib/rubocop/ast/node_pattern.rb index e3cc108e7..0b6db3da2 100644 --- a/lib/rubocop/ast/node_pattern.rb +++ b/lib/rubocop/ast/node_pattern.rb @@ -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: # @@ -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) @@ -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 @@ -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 diff --git a/spec/rubocop/ast/node_pattern_spec.rb b/spec/rubocop/ast/node_pattern_spec.rb index f01afcdfd..77ac2f623 100644 --- a/spec/rubocop/ast/node_pattern_spec.rb +++ b/spec/rubocop/ast/node_pattern_spec.rb @@ -1181,6 +1181,28 @@ end end + context 'with an expression argument' do + before do + def instance.some_function(node, arg) + arg === node + 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"' }