Skip to content

Commit eb90348

Browse files
committed
Add named params to NodePattern
1 parent 3c17d59 commit eb90348

File tree

2 files changed

+83
-8
lines changed

2 files changed

+83
-8
lines changed

lib/rubocop/ast/node_pattern.rb

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,11 @@ class Compiler
125125
STRING = /".+?"/.freeze
126126
METHOD_NAME = /\#?#{IDENTIFIER}[!?]?\(?/.freeze
127127
PARAM_CONST = /%[A-Z:][a-zA-Z_:]+/.freeze
128+
KEYWORD_NAME = /%[a-z_]+/.freeze
128129
PARAM_NUMBER = /%\d*/.freeze
129130

130131
SEPARATORS = /\s+/.freeze
131-
TOKENS = Regexp.union(META, PARAM_CONST, PARAM_NUMBER, NUMBER,
132+
TOKENS = Regexp.union(META, PARAM_CONST, KEYWORD_NAME, PARAM_NUMBER, NUMBER,
132133
METHOD_NAME, SYMBOL, STRING)
133134

134135
TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze
@@ -141,6 +142,7 @@ class Compiler
141142
LITERAL = /\A(?:#{SYMBOL}|#{NUMBER}|#{STRING})\Z/.freeze
142143
PARAM = /\A#{PARAM_NUMBER}\Z/.freeze
143144
CONST = /\A#{PARAM_CONST}\Z/.freeze
145+
KEYWORD = /\A#{KEYWORD_NAME}\Z/.freeze
144146
CLOSING = /\A(?:\)|\}|\])\Z/.freeze
145147

146148
REST = '...'
@@ -199,6 +201,7 @@ def initialize(str, node_var = 'node0')
199201
@captures = 0 # number of captures seen
200202
@unify = {} # named wildcard -> temp variable
201203
@params = 0 # highest % (param) number seen
204+
@keywords = Set[] # keyword parameters seen
202205
run(node_var)
203206
end
204207

@@ -238,6 +241,7 @@ def compile_expr(token = tokens.shift)
238241
when LITERAL then compile_literal(token)
239242
when PREDICATE then compile_predicate(token)
240243
when NODE then compile_nodetype(token)
244+
when KEYWORD then compile_keyword(token[1..-1])
241245
when CONST then compile_const(token[1..-1])
242246
when PARAM then compile_param(token[1..-1])
243247
when CLOSING then fail_due_to("#{token} in invalid position")
@@ -626,6 +630,10 @@ def compile_const(const)
626630
"#{const} === #{CUR_ELEMENT}"
627631
end
628632

633+
def compile_keyword(keyword)
634+
"#{get_keyword(keyword)} === #{CUR_ELEMENT}"
635+
end
636+
629637
def compile_args(tokens)
630638
index = tokens.find_index { |token| token == ')' }
631639

@@ -661,6 +669,11 @@ def get_param(number)
661669
number.zero? ? @root : "param#{number}"
662670
end
663671

672+
def get_keyword(name)
673+
@keywords << name
674+
name
675+
end
676+
664677
def emit_yield_capture(when_no_capture = '')
665678
yield_val = if @captures.zero?
666679
when_no_capture
@@ -686,9 +699,15 @@ def emit_param_list
686699
(1..@params).map { |n| "param#{n}" }.join(',')
687700
end
688701

689-
def emit_trailing_params
702+
def emit_keyword_list(forwarding: false)
703+
pattern = "%<keyword>s: #{'%<keyword>s' if forwarding}"
704+
@keywords.map { |k| format(pattern, keyword: k) }.join(',')
705+
end
706+
707+
def emit_trailing_params(forwarding: false)
690708
params = emit_param_list
691-
params.empty? ? '' : ",#{params}"
709+
keywords = emit_keyword_list(forwarding: forwarding)
710+
[params, keywords].reject(&:empty?).map { |p| ", #{p}" }.join
692711
end
693712

694713
def emit_method_code
@@ -788,7 +807,7 @@ def emit_node_search(method_name)
788807
else
789808
prelude = <<~RUBY
790809
return enum_for(:#{method_name},
791-
node0#{emit_trailing_params}) unless block_given?
810+
node0#{emit_trailing_params(forwarding: true)}) unless block_given?
792811
RUBY
793812
on_match = emit_yield_capture('node')
794813
end
@@ -845,11 +864,15 @@ def initialize(str)
845864
instance_eval(src, __FILE__, __LINE__ + 1)
846865
end
847866

848-
def match(*args)
867+
def match(*args, **rest)
849868
# If we're here, it's because the singleton method has not been defined,
850869
# either because we've been dup'ed or serialized through YAML
851870
initialize(pattern)
852-
match(*args)
871+
if rest.empty?
872+
match(*args)
873+
else
874+
match(*args, **rest)
875+
end
853876
end
854877

855878
def marshal_load(pattern)

spec/rubocop/ast/node_pattern_spec.rb

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616

1717
let(:node) { root_node }
1818
let(:params) { [] }
19+
let(:keyword_params) { {} }
1920
let(:instance) { described_class.new(pattern) }
20-
let(:result) { instance.match(node, *params) }
21+
let(:result) do
22+
if keyword_params.empty? # Avoid bug in Ruby < 2.6
23+
instance.match(node, *params)
24+
else
25+
instance.match(node, *params, **keyword_params)
26+
end
27+
end
2128

2229
shared_examples 'matching' do
2330
include RuboCop::AST::Sexp
@@ -1160,6 +1167,35 @@
11601167
end
11611168
end
11621169

1170+
context 'as named parameters' do
1171+
let(:pattern) { '%foo' }
1172+
let(:matcher) { Object.new }
1173+
let(:keyword_params) { { foo: matcher } }
1174+
let(:ruby) { '10' }
1175+
1176+
context 'when provided as argument to match' do
1177+
before { expect(matcher).to receive(:===).with(s(:int, 10)).and_return true } # rubocop:todo RSpec/ExpectInHook
1178+
1179+
it_behaves_like 'matching'
1180+
end
1181+
1182+
context 'when extra are provided' do
1183+
let(:keyword_params) { { foo: matcher, bar: matcher } }
1184+
1185+
it 'raises an ArgumentError' do
1186+
expect { result }.to raise_error(ArgumentError)
1187+
end
1188+
end
1189+
1190+
context 'when not provided' do
1191+
let(:keyword_params) { {} }
1192+
1193+
it 'raises an ArgumentError' do
1194+
expect { result }.to raise_error(ArgumentError)
1195+
end
1196+
end
1197+
end
1198+
11631199
context 'in a nested sequence' do
11641200
let(:pattern) { '(send (send _ %2) %1)' }
11651201
let(:params) { %i[inc dec] }
@@ -1798,7 +1834,13 @@ def withargs(foo, bar, qux)
17981834
MyClass
17991835
end
18001836
let(:ruby) { ':hello' }
1801-
let(:result) { defined_class.new.send(method_name, node, *params) }
1837+
let(:result) do
1838+
if keyword_params.empty? # Avoid bug in Ruby < 2.7
1839+
defined_class.new.send(method_name, node, *params)
1840+
else
1841+
defined_class.new.send(method_name, node, *params, **keyword_params)
1842+
end
1843+
end
18021844

18031845
if Set[1] === 1 # rubocop:disable Style/CaseEquality
18041846
let(:hello_matcher) { Set[:hello, :foo] }
@@ -1938,6 +1980,16 @@ def withargs(foo, bar, qux)
19381980
expect(result.is_a?(Enumerator)).to be(true)
19391981
expect(result.to_a).to match_array %i[hello world]
19401982
end
1983+
1984+
context 'when the pattern contains keyword_params' do
1985+
let(:pattern) { '(sym $%foo)' }
1986+
let(:keyword_params) { { foo: hello_matcher } }
1987+
1988+
it 'returns an enumerator yielding the captures' do
1989+
expect(result.is_a?(Enumerator)).to be(true)
1990+
expect(result.to_a).to match_array %i[hello]
1991+
end
1992+
end
19411993
end
19421994

19431995
context 'when called on non-matching code' do

0 commit comments

Comments
 (0)