class RuboCop::NodePattern::Compiler
@private Builds Ruby code which implements a pattern
Constants
- ANY_ORDER_TEMPLATE
- CAPTURED_REST
- CLOSING
- CUR_ELEMENT
- CUR_NODE
- CUR_PLACEHOLDER
Placeholders while compiling, see with_…_context methods
- FUNCALL
- IDENTIFIER
- LITERAL
- META
- METHOD_NAME
- NODE
- NUMBER
- PARAM
- PARAM_NUMBER
- PREDICATE
- REPEATED_TEMPLATE
- REST
- SEPARATORS
- SEQ_HEAD_GUARD
- SEQ_HEAD_INDEX
- STRING
- SYMBOL
- TOKEN
- TOKENS
- WILDCARD
Attributes
captures[R]
match_code[R]
tokens[R]
Public Class Methods
new(str, node_var = 'node0')
click to toggle source
# File lib/rubocop/node_pattern.rb, line 185 def initialize(str, node_var = 'node0') @string = str @root = node_var @temps = 0 # avoid name clashes between temp variables @captures = 0 # number of captures seen @unify = {} # named wildcard -> temp variable number @params = 0 # highest % (param) number seen run(node_var) end
tokens(pattern)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 686 def self.tokens(pattern) pattern.scan(TOKEN).reject { |token| token =~ /\A#{SEPARATORS}\Z/ } end
Public Instance Methods
auto_use_temp_node?(code)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 645 def auto_use_temp_node?(code) code.scan(CUR_PLACEHOLDER).count > 1 end
compile_any_order(capture_all = nil)
click to toggle source
rubocop:disable Metrics/MethodLength
# File lib/rubocop/node_pattern.rb, line 433 def compile_any_order(capture_all = nil) rest = capture_rest = nil patterns = [] with_temp_variables do |child, matched| tokens_until('>', 'any child').each do fail_due_to 'ellipsis must be at the end of <>' if rest token = tokens.shift case token when CAPTURED_REST then rest = capture_rest = next_capture when REST then rest = true else patterns << compile_expr(token) end end [rest ? patterns.size..Float::INFINITY : patterns.size, ->(range) { ANY_ORDER_TEMPLATE.result(binding) }] end end
compile_arg(token)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 558 def compile_arg(token) case token when WILDCARD then name = token[1..-1] number = @unify[name] || fail_due_to('invalid in arglist: ' + token) "temp#{number}" when LITERAL then token when PARAM then get_param(token[1..-1]) when CLOSING then fail_due_to("#{token} in invalid position") when nil then fail_due_to('pattern ended prematurely') else fail_due_to("invalid token in arglist: #{token.inspect}") end end
compile_args(tokens)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 548 def compile_args(tokens) index = tokens.find_index { |token| token == ')' } tokens.slice!(0..index).each_with_object([]) do |token, args| next if [')', ','].include?(token) args << compile_arg(token) end end
compile_ascend()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 493 def compile_ascend with_context("#{CUR_NODE} && #{compile_expr}", "#{CUR_NODE}.parent") end
compile_capture()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 485 def compile_capture "(#{next_capture} = #{CUR_ELEMENT}; #{compile_expr})" end
compile_captured_ellipsis()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 418 def compile_captured_ellipsis capture = next_capture block = lambda { |range| # Consider ($...) like (_ $...): range = 0..range.end if range.begin == SEQ_HEAD_INDEX "(#{capture} = #{CUR_NODE}.children[#{range}])" } [0..Float::INFINITY, block] end
compile_ellipsis()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 428 def compile_ellipsis [0..Float::INFINITY, 'true'] end
compile_expr(token = tokens.shift)
click to toggle source
rubocop:disable Metrics/MethodLength, Metrics/AbcSize
# File lib/rubocop/node_pattern.rb, line 207 def compile_expr(token = tokens.shift) # read a single pattern-matching expression from the token stream, # return Ruby code which performs the corresponding matching operation # # the 'pattern-matching' expression may be a composite which # contains an arbitrary number of sub-expressions, but that composite # must all have precedence higher or equal to that of `&&` # # Expressions may use placeholders like: # CUR_NODE: Ruby code that evaluates to an AST node # CUR_ELEMENT: Either the node or the type if in first element of # a sequence (aka seq_head, e.g. "(seq_head first_node_arg ...") case token when '(' then compile_seq when '{' then compile_union when '[' then compile_intersect when '!' then compile_negation when '$' then compile_capture when '^' then compile_ascend when WILDCARD then compile_wildcard(token[1..-1]) when FUNCALL then compile_funcall(token) when LITERAL then compile_literal(token) when PREDICATE then compile_predicate(token) when NODE then compile_nodetype(token) 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') else fail_due_to("invalid token #{token.inspect}") end end
compile_funcall(method)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 527 def compile_funcall(method) # call a method in the context which this pattern-matching # code is used in. pass target value as an argument method = method[1..-1] # drop the leading # if method.end_with?('(') # is there an arglist? args = compile_args(tokens) method = method[0..-2] # drop the trailing ( "#{method}(#{CUR_ELEMENT},#{args.join(',')})" else "#{method}(#{CUR_ELEMENT})" end end
compile_guard_clause()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 252 def compile_guard_clause "#{CUR_NODE}.is_a?(RuboCop::AST::Node)" end
compile_intersect()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 479 def compile_intersect tokens_until(']', 'intersection') .map { compile_expr } .join(' && ') end
compile_literal(literal)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 513 def compile_literal(literal) "#{CUR_ELEMENT} == #{literal}" end
compile_negation()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 489 def compile_negation "!(#{compile_expr})" end
compile_nodetype(type)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 540 def compile_nodetype(type) "#{compile_guard_clause} && #{CUR_NODE}.#{type.tr('-', '_')}_type?" end
compile_param(number)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 544 def compile_param(number) "#{CUR_ELEMENT} == #{get_param(number)}" end
compile_predicate(predicate)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 517 def compile_predicate(predicate) if predicate.end_with?('(') # is there an arglist? args = compile_args(tokens) predicate = predicate[0..-2] # drop the trailing ( "#{CUR_ELEMENT}.#{predicate}(#{args.join(',')})" else "#{CUR_ELEMENT}.#{predicate}" end end
compile_repeated_expr(token)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 267 def compile_repeated_expr(token) before = @captures expr = compile_expr(token) min, max = parse_repetition_token return [1, expr] if min.nil? if @captures != before captured = "captures[#{before}...#{@captures}]" accumulate = next_temp_variable(:accumulate) end arity = min..max || Float::INFINITY [arity, repeated_generator(expr, captured, accumulate)] end
compile_seq()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 247 def compile_seq terms = tokens_until(')', 'sequence').map { variadic_seq_term } Sequence.new(self, *terms).compile end
compile_union()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 467 def compile_union # we need to ensure that each branch of the {} contains the same # number of captures (since only one branch of the {} can actually # match, the same variables are used to hold the captures for each # branch) enum = tokens_until('}', 'union') terms = insure_same_captures(enum, 'branch of {}') .map { compile_expr } "(#{terms.join(' || ')})" end
compile_wildcard(name)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 497 def compile_wildcard(name) if name.empty? 'true' elsif @unify.key?(name) # we have already seen a wildcard with this name before # so the value it matched the first time will already be stored # in a temp. check if this value matches the one stored in the temp "#{CUR_ELEMENT} == temp#{@unify[name]}" else n = @unify[name] = next_temp_value # double assign to temp#{n} to avoid "assigned but unused variable" "(temp#{n} = #{CUR_ELEMENT}; " \ "temp#{n} = temp#{n}; true)" end end
emit_method_code()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 614 def emit_method_code <<-RUBY return unless #{@match_code} block_given? ? #{emit_yield_capture} : (return #{emit_retval}) RUBY end
emit_param_list()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 605 def emit_param_list (1..@params).map { |n| "param#{n}" }.join(',') end
emit_retval()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 595 def emit_retval if @captures.zero? 'true' elsif @captures == 1 'captures[0]' else 'captures' end end
emit_trailing_params()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 609 def emit_trailing_params params = emit_param_list params.empty? ? '' : ",#{params}" end
emit_yield_capture(when_no_capture = '')
click to toggle source
# File lib/rubocop/node_pattern.rb, line 584 def emit_yield_capture(when_no_capture = '') yield_val = if @captures.zero? when_no_capture elsif @captures == 1 'captures[0]' # Circumvent https://github.com/jruby/jruby/issues/5710 else '*captures' end "yield(#{yield_val})" end
fail_due_to(message)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 621 def fail_due_to(message) raise Invalid, "Couldn't compile due to #{message}. Pattern: #{@string}" end
get_param(number)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 578 def get_param(number) number = number.empty? ? 1 : Integer(number) @params = number if number > @params number.zero? ? @root : "param#{number}" end
insure_same_captures(enum, what) { || ... }
click to toggle source
rubocop:enable Metrics/MethodLength
# File lib/rubocop/node_pattern.rb, line 452 def insure_same_captures(enum, what) return to_enum __method__, enum, what unless block_given? captures_before = captures_after = nil enum.each do captures_before ||= @captures @captures = captures_before yield captures_after ||= @captures if captures_after != @captures fail_due_to("each #{what} must have same # of captures") end end end
next_capture()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 572 def next_capture index = @captures @captures += 1 "captures[#{index}]" end
next_temp_value()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 641 def next_temp_value @temps += 1 end
next_temp_variable(name)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 637 def next_temp_variable(name) "#{name}#{next_temp_value}" end
parse_repetition_token()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 293 def parse_repetition_token case tokens.first when '*' then min = 0 when '+' then min = 1 when '?' then min = 0 max = 1 else return end tokens.shift [min, max] end
repeated_generator(expr, captured, accumulate)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 282 def repeated_generator(expr, captured, accumulate) with_temp_variables do |child| lambda do |range| if range.begin == SEQ_HEAD_INDEX fail_due_to 'repeated pattern at beginning of sequence' end REPEATED_TEMPLATE.result(binding) end end end
run(node_var)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 196 def run(node_var) @tokens = Compiler.tokens(@string) @match_code = with_context(compile_expr, node_var, use_temp_node: false) @match_code.prepend("(captures = Array.new(#{@captures})) && ") \ if @captures.positive? fail_due_to('unbalanced pattern') unless tokens.empty? end
substitute_cur_node(code, cur_node, first_cur_node: cur_node)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 675 def substitute_cur_node(code, cur_node, first_cur_node: cur_node) iter = 0 code .gsub(CUR_ELEMENT, CUR_NODE) .gsub(CUR_NODE) do iter += 1 iter == 1 ? first_cur_node : cur_node end .gsub(SEQ_HEAD_GUARD, '') end
tokens_until(stop, what) { |until first == stop| ... }
click to toggle source
rubocop:enable Metrics/MethodLength, Metrics/AbcSize
# File lib/rubocop/node_pattern.rb, line 239 def tokens_until(stop, what) return to_enum __method__, stop, what unless block_given? fail_due_to("empty #{what}") if tokens.first == stop && what yield until tokens.first == stop tokens.shift end
variadic_seq_term()
click to toggle source
# File lib/rubocop/node_pattern.rb, line 256 def variadic_seq_term token = tokens.shift case token when CAPTURED_REST then compile_captured_ellipsis when REST then compile_ellipsis when '$<' then compile_any_order(next_capture) when '<' then compile_any_order else compile_repeated_expr(token) end end
with_child_context(code, child_index)
click to toggle source
with_<…>_context methods are used whenever the context, i.e the current node or the current element can be determined.
# File lib/rubocop/node_pattern.rb, line 652 def with_child_context(code, child_index) with_context(code, "#{CUR_NODE}.children[#{child_index}]") end
with_context(code, cur_node, use_temp_node: auto_use_temp_node?(code))
click to toggle source
# File lib/rubocop/node_pattern.rb, line 656 def with_context(code, cur_node, use_temp_node: auto_use_temp_node?(code)) if use_temp_node with_temp_node(cur_node) do |init, temp_var| substitute_cur_node(code, temp_var, first_cur_node: init) end else substitute_cur_node(code, cur_node) end end
with_seq_head_context(code)
click to toggle source
# File lib/rubocop/node_pattern.rb, line 667 def with_seq_head_context(code) if code.include?(SEQ_HEAD_GUARD) fail_due_to('parentheses at sequence head') end code.gsub CUR_ELEMENT, "#{CUR_NODE}.type" end
with_temp_node(cur_node) { |"(#{node} = #{cur_node})", node| ... }
click to toggle source
# File lib/rubocop/node_pattern.rb, line 625 def with_temp_node(cur_node) with_temp_variables do |node| yield "(#{node} = #{cur_node})", node end .gsub("\n", "\n ") # Nicer indent for debugging end
with_temp_variables() { |*names| ... }
click to toggle source
# File lib/rubocop/node_pattern.rb, line 632 def with_temp_variables(&block) names = block.parameters.map { |_, name| next_temp_variable(name) } yield(*names) end