class RuboCop::NodePattern::Compiler

@private Builds Ruby code which implements a pattern

Constants

CLOSING
FUNCALL
ID_CHAR
LITERAL
META
NODE
NUMBER
PARAM
PREDICATE
RSYM
TOKEN
WILDCARD

Attributes

match_code[R]

Public Class Methods

new(str, node_var = 'node0') click to toggle source
# File lib/rubocop/node_pattern.rb, line 117
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

Public Instance Methods

compile_arg(token) click to toggle source
# File lib/rubocop/node_pattern.rb, line 350
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 343
def compile_args(tokens)
  args = []
  args << compile_arg(tokens.shift) until tokens.first == ')'
  tokens.shift # drop the )
  args
end
compile_ascend(tokens, cur_node, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 289
def compile_ascend(tokens, cur_node, seq_head)
  "(#{cur_node}.parent && "            "#{compile_expr(tokens, "#{cur_node}.parent", seq_head)})"
end
compile_capt_ellip(tokens, cur_node, terms, index) click to toggle source
# File lib/rubocop/node_pattern.rb, line 213
def compile_capt_ellip(tokens, cur_node, terms, index)
  capture = next_capture
  if (term = compile_seq_tail(tokens, "#{cur_node}.children.last"))
    terms << "(#{cur_node}.children.size > #{index})"
    terms << term
    terms << "(#{capture} = #{cur_node}.children[#{index}..-2])"
  else
    terms << "(#{cur_node}.children.size >= #{index})" if index > 0
    terms << "(#{capture} = #{cur_node}.children[#{index}..-1])"
  end
  terms
end
compile_capture(tokens, cur_node, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 280
def compile_capture(tokens, cur_node, seq_head)
  "(#{next_capture} = #{cur_node}#{'.type' if seq_head}; "            "#{compile_expr(tokens, cur_node, seq_head)})"
end
compile_ellipsis(tokens, cur_node, terms, index) click to toggle source
# File lib/rubocop/node_pattern.rb, line 203
def compile_ellipsis(tokens, cur_node, terms, index)
  if (term = compile_seq_tail(tokens, "#{cur_node}.children.last"))
    terms << "(#{cur_node}.children.size > #{index})"
    terms << term
  elsif index > 0
    terms << "(#{cur_node}.children.size >= #{index})"
  end
  terms
end
compile_expr(tokens, cur_node, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 136
def compile_expr(tokens, cur_node, seq_head)
  # read a single pattern-matching expression from the token stream,
  # return Ruby code which performs the corresponding matching operation
  # on 'cur_node' (which is Ruby code which evaluates to an AST node)
  #
  # the 'pattern-matching' expression may be a composite which
  # contains an arbitrary number of sub-expressions
  token = tokens.shift
  case token
  when '('       then compile_seq(tokens, cur_node, seq_head)
  when '{'       then compile_union(tokens, cur_node, seq_head)
  when '['       then compile_intersect(tokens, cur_node, seq_head)
  when '!'       then compile_negation(tokens, cur_node, seq_head)
  when '$'       then compile_capture(tokens, cur_node, seq_head)
  when '^'       then compile_ascend(tokens, cur_node, seq_head)
  when WILDCARD  then compile_wildcard(cur_node, token[1..-1], seq_head)
  when FUNCALL   then compile_funcall(tokens, cur_node, token, seq_head)
  when LITERAL   then compile_literal(cur_node, token, seq_head)
  when PREDICATE then compile_predicate(tokens, cur_node, token, seq_head)
  when NODE      then compile_nodetype(cur_node, token)
  when PARAM     then compile_param(cur_node, token[1..-1], seq_head)
  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(tokens, cur_node, method, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 322
def compile_funcall(tokens, cur_node, method, seq_head)
  # 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_node}#{'.type' if seq_head}),#{args.join(',')})"
  else
    "(#{method}(#{cur_node}#{'.type' if seq_head}))"
  end
end
compile_intersect(tokens, cur_node, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 265
def compile_intersect(tokens, cur_node, seq_head)
  fail_due_to('empty intersection') if tokens.first == ']'

  init = "temp#{@temps += 1} = #{cur_node}"
  cur_node = "temp#{@temps}"

  terms = []
  until tokens.first == ']'
    terms << compile_expr(tokens, cur_node, seq_head)
  end
  tokens.shift

  join_terms(init, terms, ' && ')
end
compile_literal(cur_node, literal, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 308
def compile_literal(cur_node, literal, seq_head)
  "(#{cur_node}#{'.type' if seq_head} == #{literal})"
end
compile_negation(tokens, cur_node, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 285
def compile_negation(tokens, cur_node, seq_head)
  "(!#{compile_expr(tokens, cur_node, seq_head)})"
end
compile_nodetype(cur_node, type) click to toggle source
# File lib/rubocop/node_pattern.rb, line 335
def compile_nodetype(cur_node, type)
  "(#{cur_node} && #{cur_node}.#{type}_type?)"
end
compile_param(cur_node, number, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 339
def compile_param(cur_node, number, seq_head)
  "(#{cur_node}#{'.type' if seq_head} == #{get_param(number)})"
end
compile_predicate(tokens, cur_node, predicate, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 312
def compile_predicate(tokens, cur_node, predicate, seq_head)
  if predicate.end_with?('(') # is there an arglist?
    args = compile_args(tokens)
    predicate = predicate[0..-2] # drop the trailing (
    "(#{cur_node}#{'.type' if seq_head}.#{predicate}(#{args.join(',')}))"
  else
    "(#{cur_node}#{'.type' if seq_head}.#{predicate})"
  end
end
compile_seq(tokens, cur_node, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 163
def compile_seq(tokens, cur_node, seq_head)
  fail_due_to('empty parentheses') if tokens.first == ')'
  fail_due_to('parentheses at sequence head') if seq_head

  # 'cur_node' is a Ruby expression which evaluates to an AST node,
  # but we don't know how expensive it is
  # to be safe, cache the node in a temp variable and then use the
  # temp variable as 'cur_node'
  init = "temp#{@temps += 1} = #{cur_node}"
  cur_node = "temp#{@temps}"
  terms = compile_seq_terms(tokens, cur_node)

  join_terms(init, terms, ' && ')
end
compile_seq_tail(tokens, cur_node) click to toggle source
# File lib/rubocop/node_pattern.rb, line 226
def compile_seq_tail(tokens, cur_node)
  tokens.shift
  if tokens.first == ')'
    tokens.shift
    nil
  else
    expr = compile_expr(tokens, cur_node, false)
    fail_due_to('missing )') unless tokens.shift == ')'
    expr
  end
end
compile_seq_terms(tokens, cur_node) click to toggle source
# File lib/rubocop/node_pattern.rb, line 178
def compile_seq_terms(tokens, cur_node)
  terms = []
  index = nil
  until tokens.first == ')'
    if tokens.first == '...'
      return compile_ellipsis(tokens, cur_node, terms, index || 0)
    elsif tokens.first == '$...'
      return compile_capt_ellip(tokens, cur_node, terms, index || 0)
    elsif index.nil?
      # in 'sequence head' position; some expressions are compiled
      # differently at 'sequence head' (notably 'node type' expressions)
      # grep for seq_head to see where it makes a difference
      terms << compile_expr(tokens, cur_node, true)
      index = 0
    else
      child_node = "#{cur_node}.children[#{index}]"
      terms << compile_expr(tokens, child_node, false)
      index += 1
    end
  end
  terms << "(#{cur_node}.children.size == #{index})"
  tokens.shift # drop concluding )
  terms
end
compile_union(tokens, cur_node, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 238
def compile_union(tokens, cur_node, seq_head)
  fail_due_to('empty union') if tokens.first == '}'

  init = "temp#{@temps += 1} = #{cur_node}"
  cur_node = "temp#{@temps}"

  terms = []
  # 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)
  captures_before = @captures
  terms << compile_expr(tokens, cur_node, seq_head)
  captures_after = @captures

  until tokens.first == '}'
    @captures = captures_before
    terms << compile_expr(tokens, cur_node, seq_head)
    if @captures != captures_after
      fail_due_to('each branch of {} must have same # of captures')
    end
  end
  tokens.shift

  join_terms(init, terms, ' || ')
end
compile_wildcard(cur_node, name, seq_head) click to toggle source
# File lib/rubocop/node_pattern.rb, line 294
def compile_wildcard(cur_node, name, seq_head)
  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_node}#{'.type' if seq_head} == temp#{@unify[name]})"
  else
    n = @unify[name] = (@temps += 1)
    "(temp#{n} = #{cur_node}#{'.type' if seq_head}; true)"
  end
end
emit_capture_list() click to toggle source
# File lib/rubocop/node_pattern.rb, line 378
def emit_capture_list
  (1..@captures).map { |n| "capture#{n}" }.join(',')
end
emit_method_code() click to toggle source
# File lib/rubocop/node_pattern.rb, line 401
def emit_method_code
  <<-CODE
    return nil unless #{@match_code}
    block_given? ? yield(#{emit_capture_list}) : (return #{emit_retval})
  CODE
end
emit_param_list() click to toggle source
# File lib/rubocop/node_pattern.rb, line 392
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 382
def emit_retval
  if @captures.zero?
    'true'
  elsif @captures == 1
    'capture1'
  else
    "[#{emit_capture_list}]"
  end
end
emit_trailing_params() click to toggle source
# File lib/rubocop/node_pattern.rb, line 396
def emit_trailing_params
  params = emit_param_list
  params.empty? ? '' : ",#{params}"
end
fail_due_to(message) click to toggle source
# File lib/rubocop/node_pattern.rb, line 408
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 368
def get_param(number)
  number = number.empty? ? 1 : Integer(number)
  @params = number if number > @params
  number.zero? ? @root : "param#{number}"
end
join_terms(init, terms, operator) click to toggle source
# File lib/rubocop/node_pattern.rb, line 374
def join_terms(init, terms, operator)
  "(#{init};#{terms.join(operator)})"
end
next_capture() click to toggle source
# File lib/rubocop/node_pattern.rb, line 364
def next_capture
  "capture#{@captures += 1}"
end
run(node_var) click to toggle source
# File lib/rubocop/node_pattern.rb, line 129
def run(node_var)
  tokens = @string.scan(TOKEN)
  tokens.reject! { |token| token =~ /\A[\s,]+\Z/ } # drop whitespace
  @match_code = compile_expr(tokens, node_var, false)
  fail_due_to('unbalanced pattern') unless tokens.empty?
end