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