class PowerAssert::Parser

Constants

AND_OR_OPS
DUMMY
Ident
MID2SRCTXT

Attributes

binding[R]
line[R]
lineno[R]
path[R]

Public Class Methods

new(line, path, lineno, binding, assertion_method_name = nil, assertion_proc = nil) click to toggle source
# File lib/power_assert/parser.rb, line 9
def initialize(line, path, lineno, binding, assertion_method_name = nil, assertion_proc = nil)
  @line = line
  @line_for_parsing = (valid_syntax?(line) ? line : slice_expression(line)).b
  @path = path
  @lineno = lineno
  @binding = binding
  @proc_local_variables = binding.eval('local_variables').map(&:to_s)
  @assertion_method_name = assertion_method_name
  @assertion_proc = assertion_proc
end

Public Instance Methods

call_paths() click to toggle source
# File lib/power_assert/parser.rb, line 24
def call_paths
  collect_paths(idents).uniq
end
idents() click to toggle source
# File lib/power_assert/parser.rb, line 20
def idents
  @idents ||= extract_idents(Ripper.sexp(@line_for_parsing))
end
method_id_set() click to toggle source
# File lib/power_assert/parser.rb, line 28
def method_id_set
  methods = idents.flatten.find_all {|i| i.type == :method }
  @method_id_set ||= methods.map(&:name).map(&:to_sym).each_with_object({}) {|i, h| h[i] = true }
end

Private Instance Methods

collect_paths(idents, prefixes = [[]], index = 0) click to toggle source
# File lib/power_assert/parser.rb, line 221
def collect_paths(idents, prefixes = [[]], index = 0)
  if index < idents.length
    node = idents[index]
    if node.kind_of?(Branch)
      prefixes = node.flat_map {|n| collect_paths(n, prefixes, 0) }
    else
      prefixes = prefixes.map {|prefix| prefix + [node] }
    end
    collect_paths(idents, prefixes, index + 1)
  else
    prefixes
  end
end
extract_idents(sexp) click to toggle source

Returns idents as graph structure.

                                                +--c--b--+
extract_idents(Ripper.sexp('a&.b(c).d')) #=> a--+        +--d
                                                +--------+
# File lib/power_assert/parser.rb, line 70
def extract_idents(sexp)
  tag, * = sexp
  case tag
  when :arg_paren, :assoc_splat, :fcall, :hash, :method_add_block, :string_literal, :return
    extract_idents(sexp[1])
  when :assign, :massign
    extract_idents(sexp[2])
  when :opassign
    _, _, (_, op_name, (_, op_column)), s0 = sexp
    extract_idents(s0) + [Ident[:method, op_name.sub(/=\z/, ''), op_column]]
  when :dyna_symbol
    if sexp[1][0].kind_of?(Symbol)
      # sexp[1] can be [:string_content, [..]] while parsing { "a": 1 }
      extract_idents(sexp[1])
    else
      sexp[1].flat_map {|s| extract_idents(s) }
    end
  when :assoclist_from_args, :bare_assoc_hash, :paren, :string_embexpr,
    :regexp_literal, :xstring_literal
    sexp[1].flat_map {|s| extract_idents(s) }
  when :command
    [sexp[2], sexp[1]].flat_map {|s| extract_idents(s) }
  when :assoc_new, :dot2, :dot3, :string_content
    sexp[1..-1].flat_map {|s| extract_idents(s) }
  when :unary
    handle_columnless_ident([], sexp[1], extract_idents(sexp[2]))
  when :binary
    op = sexp[2]
    if AND_OR_OPS.include?(op)
      extract_idents(sexp[1]) + [Branch[extract_idents(sexp[3]), []]]
    else
      handle_columnless_ident(extract_idents(sexp[1]), op, extract_idents(sexp[3]))
    end
  when :call
    _, recv, (op_sym, op_name, _), method = sexp
    with_safe_op = ((op_sym == :@op and op_name == '&.') or op_sym == :"&.")
    if method == :call
      handle_columnless_ident(extract_idents(recv), :call, [], with_safe_op)
    else
      extract_idents(recv) + (with_safe_op ? [Branch[extract_idents(method), []]] : extract_idents(method))
    end
  when :array
    sexp[1] ? sexp[1].flat_map {|s| extract_idents(s) } : []
  when :command_call
    [sexp[1], sexp[4], sexp[3]].flat_map {|s| extract_idents(s) }
  when :aref
    handle_columnless_ident(extract_idents(sexp[1]), :[], extract_idents(sexp[2]))
  when :method_add_arg
    idents = extract_idents(sexp[1])
    if idents.empty?
      # idents may be empty(e.g. ->{}.())
      extract_idents(sexp[2])
    else
      if idents[-1].kind_of?(Branch) and idents[-1][1].empty?
        # Safe navigation operator is used. See :call clause also.
        idents[0..-2] + [Branch[extract_idents(sexp[2]) + idents[-1][0], []]]
      else
        idents[0..-2] + extract_idents(sexp[2]) + [idents[-1]]
      end
    end
  when :args_add_block
    _, (tag, ss0, *ss1), _ = sexp
    if tag == :args_add_star
      (ss0 + ss1).flat_map {|s| extract_idents(s) }
    else
      sexp[1].flat_map {|s| extract_idents(s) }
    end
  when :vcall
    _, (tag, name, (_, column)) = sexp
    if tag == :@ident
      [Ident[@proc_local_variables.include?(name) ? :ref : :method, name, column]]
    else
      []
    end
  when :program
    _, ((tag0, (tag1, (tag2, (tag3, mname, _)), _), (tag4, _, ss))) = sexp
    if tag0 == :method_add_block and tag1 == :method_add_arg and tag2 == :fcall and
        (tag3 == :@ident or tag3 == :@const) and mname == @assertion_method_name and (tag4 == :brace_block or tag4 == :do_block)
      ss.flat_map {|s| extract_idents(s) }
    else
      _, (s0, *) = sexp
      extract_idents(s0)
    end
  when :ifop
    _, s0, s1, s2 = sexp
    [*extract_idents(s0), Branch[extract_idents(s1), extract_idents(s2)]]
  when :if, :unless
    _, s0, ss0, (_, ss1) = sexp
    [*extract_idents(s0), Branch[ss0.flat_map {|s| extract_idents(s) }, ss1 ? ss1.flat_map {|s| extract_idents(s) } : []]]
  when :if_mod, :unless_mod
    _, s0, s1 = sexp
    [*extract_idents(s0), Branch[extract_idents(s1), []]]
  when :var_ref, :var_field
    _, (tag, ref_name, (_, column)) = sexp
    case tag
    when :@kw
      if ref_name == 'self'
        [Ident[:ref, 'self', column]]
      else
        []
      end
    when :@ident, :@const, :@cvar, :@ivar, :@gvar
      [Ident[:ref, ref_name, column]]
    else
      []
    end
  when :@ident, :@const, :@op
    _, method_name, (_, column) = sexp
    [Ident[:method, method_name, column]]
  else
    []
  end
end
handle_columnless_ident(left_idents, mid, right_idents, with_safe_op = false) click to toggle source
# File lib/power_assert/parser.rb, line 200
def handle_columnless_ident(left_idents, mid, right_idents, with_safe_op = false)
  left_max = left_idents.flatten.max_by(&:column)
  right_min = right_idents.flatten.min_by(&:column)
  bg = left_max ? left_max.column + left_max.name.length : 0
  ed = right_min ? right_min.column - 1 : @line_for_parsing.length - 1
  mname = mid.to_s
  srctxt = MID2SRCTXT[mid] || mname
  re = /
    #{'\b' if /\A\w/ =~ srctxt}
    #{Regexp.escape(srctxt)}
    #{'\b' if /\w\z/ =~ srctxt}
  /x
  indices = str_indices(@line_for_parsing, re, bg, ed)
  if indices.length == 1 or !(right_idents.empty? and left_idents.empty?)
    ident = Ident[:method, mname, right_idents.empty? ? indices.first : indices.last]
    left_idents + right_idents + (with_safe_op ? [Branch[[ident], []]] : [ident])
  else
    left_idents + right_idents
  end
end
slice_expression(str) click to toggle source
# File lib/power_assert/parser.rb, line 48
def slice_expression(str)
  str = str.chomp
  str.sub!(/\A\s*(?:if|unless|elsif|case|while|until) /) {|i| ' ' * i.length }
  str.sub!(/\A\s*(?:\}|\]|end)?\./) {|i| ' ' * i.length }
  str.sub!(/[\{\.\]\z/, '')
  str.sub!(/(?:&&|\|\|)\z/, '')
  str.sub!(/ (?:do|and|or)\z/, '')
  str
end
str_indices(str, re, offset, limit) click to toggle source
# File lib/power_assert/parser.rb, line 184
def str_indices(str, re, offset, limit)
  idx = str.index(re, offset)
  if idx and idx <= limit
    [idx, *str_indices(str, re, idx + 1, limit)]
  else
    []
  end
end
valid_syntax?(str) click to toggle source
# File lib/power_assert/parser.rb, line 35
def valid_syntax?(str)
  return true unless defined?(RubyVM)
  begin
    verbose, $VERBOSE = $VERBOSE, nil
    RubyVM::InstructionSequence.compile(str)
    true
  rescue SyntaxError
    false
  ensure
    $VERBOSE = verbose
  end
end