diff --git a/CHANGELOG.md b/CHANGELOG.md index e711a21fe..a2bd660df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/modules/ROOT/pages/node_pattern.adoc b/docs/modules/ROOT/pages/node_pattern.adoc index 401f60573..f57fa3f67 100644 --- a/docs/modules/ROOT/pages/node_pattern.adoc +++ b/docs/modules/ROOT/pages/node_pattern.adoc @@ -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: @@ -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 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..5b8095883 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 # 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"' }