Skip to content

Commit

Permalink
Add keyword parameters defaults to def_node_search and `def_node_pa…
Browse files Browse the repository at this point in the history
…ttern`
  • Loading branch information
marcandre committed Jun 14, 2020
1 parent 6f22198 commit 397b3bb
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 14 deletions.
20 changes: 20 additions & 0 deletions docs/modules/ROOT/pages/node_pattern.adoc
Expand Up @@ -409,6 +409,26 @@ has_user_data?(node, /^pass(word)?$/i)
has_user_data?(node, ->(key) { # return true or false depending on key })
----

== `%keyword` for named parameters

Arguments can be passed as named parameters. They will be matched using `===`.
Defaults values can be passed to `def_node_matcher` and `def_node_search`.

[source,ruby]
----
def_node_matcher :interesting_call?, '(send _ %method ...)',
method: Set[:transform_values, :transform_keys,
:transform_values!, :transform_keys!,
:to_h].freeze
# Usage:
interesting_call?(node) # use the default methods
interesting_call?(node, method: /^transform/) # match anything starting with 'transform'
----

Named parameters as arguments to custom methods are not supported.

== `%CONST` for constants

Constants can be included in patterns. They will be matched using `===`, so
Expand Down
45 changes: 32 additions & 13 deletions lib/rubocop/ast/node_pattern.rb
Expand Up @@ -78,6 +78,12 @@ module AST
# # for consistency, %0 is the 'root node' which is
# # passed as the 1st argument to #match, where the
# # matching process starts
# '(send _ %keyword)' # arguments can also be passed as keyword
# # parameters; they will also be compared with
# # the AST using #=== so you can pass Procs, Regexp
# # in addition to Nodes or literals.
# # Note that the macros `def_node_pattern` and
# # `def_node_search` accept default values for these.
# '(send _ %CONST)' # the named constant will be compared with
# # the AST using #=== so you can pass Procs, Regexp
# # in addition to Nodes or literals.
Expand Down Expand Up @@ -784,21 +790,32 @@ def self.tokens(pattern)
pattern.scan(TOKEN).reject { |token| token =~ /\A#{SEPARATORS}\Z/ }
end

def def_helper(base, src)
def def_helper(base, method_name, **defaults)
location = caller_locations(3, 1).first
unless defaults.empty?
base.send :define_method, method_name do |*args, **values|
send method_name, *args, **defaults, **values
end
method_name = :"without_defaults_#{method_name}"
end
src = yield method_name
base.class_eval(src, location.path, location.lineno)
end

def def_node_matcher(base, method_name)
def_helper(base, <<~RUBY)
def #{method_name}(node = self#{emit_trailing_params})
#{emit_method_code}
end
RUBY
def def_node_matcher(base, method_name, **defaults)
def_helper(base, method_name, **defaults) do |name|
<<~RUBY
def #{name}(node = self#{emit_trailing_params})
#{emit_method_code}
end
RUBY
end
end

def def_node_search(base, method_name)
def_helper(base, emit_node_search(method_name))
def def_node_search(base, method_name, **defaults)
def_helper(base, method_name, **defaults) do |name|
emit_node_search(name)
end
end

def emit_node_search(method_name)
Expand Down Expand Up @@ -839,8 +856,9 @@ module Macros
# yield to the block (passing any captures as block arguments).
# If the node matches, and no block is provided, the new method will
# return the captures, or `true` if there were none.
def def_node_matcher(method_name, pattern_str)
Compiler.new(pattern_str, 'node').def_node_matcher(self, method_name)
def def_node_matcher(method_name, pattern_str, **keyword_defaults)
Compiler.new(pattern_str, 'node')
.def_node_matcher(self, method_name, **keyword_defaults)
end

# Define a method which recurses over the descendants of an AST node,
Expand All @@ -849,8 +867,9 @@ def def_node_matcher(method_name, pattern_str)
# If the method name ends with '?', the new method will return `true`
# as soon as it finds a descendant which matches. Otherwise, it will
# yield all descendants which match.
def def_node_search(method_name, pattern_str)
Compiler.new(pattern_str, 'node').def_node_search(self, method_name)
def def_node_search(method_name, pattern_str, **keyword_defaults)
Compiler.new(pattern_str, 'node')
.def_node_search(self, method_name, **keyword_defaults)
end
end

Expand Down
31 changes: 30 additions & 1 deletion spec/rubocop/ast/node_pattern_spec.rb
Expand Up @@ -1827,10 +1827,11 @@ def withargs(foo, bar, qux)
end)
end

let(:keyword_defaults) { {} }
let(:method_name) { :my_matcher }
let(:line_no) { __LINE__ + 2 }
let(:defined_class) do
MyClass.public_send helper_name, method_name, pattern
MyClass.public_send helper_name, method_name, pattern, **keyword_defaults
MyClass
end
let(:ruby) { ':hello' }
Expand Down Expand Up @@ -1989,6 +1990,34 @@ def withargs(foo, bar, qux)
expect(result.is_a?(Enumerator)).to be(true)
expect(result.to_a).to match_array %i[hello]
end

# rubocop:disable RSpec/NestedGroups
context 'when helper is called with default keyword_params' do
let(:keyword_defaults) { { foo: :world } }

it 'is overriden when calling the matcher' do
expect(result.is_a?(Enumerator)).to be(true)
expect(result.to_a).to match_array %i[hello]
end

context 'and no value is given to the matcher' do
let(:keyword_params) { {} }

it 'uses the defaults' do
expect(result.is_a?(Enumerator)).to be(true)
expect(result.to_a).to match_array %i[world]
end
end

context 'some defaults are not params' do
let(:keyword_defaults) { { bar: :world } }

it 'raises an error' do
expect { result }.to raise_error(ArgumentError)
end
end
end
# rubocop:enable RSpec/NestedGroups
end
end

Expand Down

0 comments on commit 397b3bb

Please sign in to comment.