diff --git a/CHANGELOG.md b/CHANGELOG.md index 167f33341..b12c660b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [#11](https://github.com/rubocop-hq/rubocop-ast/issues/11): Add `argument_type?` method to make it easy to recognize argument nodes. ([@tejasbubane][]) * [#31](https://github.com/rubocop-hq/rubocop-ast/pull/31): NodePattern now uses `param === node` to match params, which allows Regexp, Proc, Set in addition to Nodes and literals. ([@marcandre][]) * [#41](https://github.com/rubocop-hq/rubocop-ast/pull/41): Add `delimiters` and related predicates for `RegexpNode`. ([@owst][]) +* [#35](https://github.com/rubocop-hq/rubocop-ast/pull/35): NodePattern now accepts `%named_param` and `%CONST`. The macros `def_node_pattern` and `def_node_search` accept default named parameters. ([@marcandre][]) ## 0.0.3 (2020-05-15) diff --git a/docs/modules/ROOT/pages/node_pattern.adoc b/docs/modules/ROOT/pages/node_pattern.adoc index 5023cfed7..318a05e46 100644 --- a/docs/modules/ROOT/pages/node_pattern.adoc +++ b/docs/modules/ROOT/pages/node_pattern.adoc @@ -436,6 +436,23 @@ interesting_call?(node, method: /^transform/) # match anything starting with 'tr Named parameters as arguments to custom methods are also supported. +== `%CONST` for constants + +Constants can be included in patterns. They will be matched using `===`, so ++Regexp+ / +Set+ / +Proc+ can be used in addition to literals and +Nodes+: + +[source,ruby] +---- +SOME_CALLS = Set[:transform_values, :transform_keys, + :transform_values!, :transform_keys!, + :to_h].freeze + +def_node_matcher :interesting_call?, '(send _ %SOME_CALLS ...)' + +---- + +Constants as arguments to custom methods are also supported. + == `nil` or `nil?` Take a special attention to nil behavior: diff --git a/lib/rubocop/ast/node_pattern.rb b/lib/rubocop/ast/node_pattern.rb index 0347a89f8..e76350e08 100644 --- a/lib/rubocop/ast/node_pattern.rb +++ b/lib/rubocop/ast/node_pattern.rb @@ -86,6 +86,7 @@ module AST # # parameters (see `%1`) # # Note that the macros `def_node_pattern` and # # `def_node_search` accept default values for these. + # '(send _ %CONST)' # the named constant will act like `%1` and `%named`. # '^^send' # each ^ ascends one level in the AST # # so this matches against the grandparent node # '`send' # descends any number of level in the AST @@ -129,11 +130,12 @@ class Compiler NUMBER = /-?\d+(?:\.\d+)?/.freeze STRING = /".+?"/.freeze METHOD_NAME = /\#?#{IDENTIFIER}[!?]?\(?/.freeze + PARAM_CONST = /%[A-Z:][a-zA-Z_:]+/.freeze KEYWORD_NAME = /%[a-z_]+/.freeze PARAM_NUMBER = /%\d*/.freeze SEPARATORS = /\s+/.freeze - TOKENS = Regexp.union(META, KEYWORD_NAME, PARAM_NUMBER, NUMBER, + TOKENS = Regexp.union(META, PARAM_CONST, KEYWORD_NAME, PARAM_NUMBER, NUMBER, METHOD_NAME, SYMBOL, STRING) TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze @@ -145,6 +147,7 @@ class Compiler FUNCALL = /\A\##{METHOD_NAME}/.freeze LITERAL = /\A(?:#{SYMBOL}|#{NUMBER}|#{STRING})\Z/.freeze PARAM = /\A#{PARAM_NUMBER}\Z/.freeze + CONST = /\A#{PARAM_CONST}\Z/.freeze KEYWORD = /\A#{KEYWORD_NAME}\Z/.freeze CLOSING = /\A(?:\)|\}|\])\Z/.freeze @@ -245,6 +248,7 @@ def compile_expr(token = tokens.shift) when PREDICATE then compile_predicate(token) when NODE then compile_nodetype(token) when KEYWORD then compile_keyword(token[1..-1]) + when CONST then compile_const(token[1..-1]) when PARAM then compile_param(token[1..-1]) when CLOSING then fail_due_to("#{token} in invalid position") when nil then fail_due_to('pattern ended prematurely') @@ -628,6 +632,10 @@ def compile_param(number) "#{get_param(number)} === #{CUR_ELEMENT}" end + def compile_const(const) + "#{get_const(const)} === #{CUR_ELEMENT}" + end + def compile_keyword(keyword) "#{get_keyword(keyword)} === #{CUR_ELEMENT}" end @@ -649,6 +657,7 @@ def compile_arg(token) access_unify(name) || fail_due_to('invalid in arglist: ' + token) when LITERAL then token when KEYWORD then get_keyword(name) + when CONST then get_const(name) when PARAM then get_param(name) when CLOSING then fail_due_to("#{token} in invalid position") when nil then fail_due_to('pattern ended prematurely') @@ -673,6 +682,10 @@ def get_keyword(name) name end + def get_const(const) + const # Output the constant exactly as given + end + def emit_yield_capture(when_no_capture = '') yield_val = if @captures.zero? when_no_capture diff --git a/spec/rubocop/ast/node_pattern_spec.rb b/spec/rubocop/ast/node_pattern_spec.rb index 9b15e3d26..c4901b1db 100644 --- a/spec/rubocop/ast/node_pattern_spec.rb +++ b/spec/rubocop/ast/node_pattern_spec.rb @@ -1162,6 +1162,25 @@ end end + context 'with a constant argument' do + let(:pattern) { '(send (int equal?(%CONST)) ...)' } + let(:ruby) { '1 + 2' } + + before { stub_const 'CONST', const_value } + + context 'for which the predicate is true' do + let(:const_value) { 1 } + + it_behaves_like 'matching' + end + + context 'for which the predicate is false' do + let(:const_value) { 2 } + + it_behaves_like 'nonmatching' + end + end + context 'with multiple arguments' do let(:pattern) { '(str between?(%1, %2))' } let(:ruby) { '"c"' } @@ -2103,5 +2122,20 @@ def withargs(foo, bar, qux) end end end + + context 'with a pattern with a constant' do + let(:pattern) { '(sym %TEST)' } + let(:helper_name) { :def_node_matcher } + + before { defined_class::TEST = hello_matcher } + + it_behaves_like 'matching' + + context 'when the value is not in the set' do + let(:ruby) { ':world' } + + it_behaves_like 'nonmatching' + end + end end end