lib/ruby_parser/bm_sexp.rb



#Sexp changes from ruby_parser
#and some changes for caching hash value and tracking 'original' line number
#of a Sexp.
class Sexp
  attr_accessor :original_line, :or_depth
  ASSIGNMENT_BOOL = [:gasgn, :iasgn, :lasgn, :cvdecl, :cvasgn, :cdecl, :or, :and, :colon2, :op_asgn_or]
  CALLS = [:call, :attrasgn, :safe_call, :safe_attrasgn]

  alias_method :method_missing, :method_missing # silence redefined method warning
  def method_missing name, *args
    #Brakeman does not use this functionality,
    #so overriding it to raise a NoMethodError.
    #
    #The original functionality calls find_node and optionally
    #deletes the node if found.
    #
    #Defining a method named "return" seems like a bad idea, so we have to
    #check for it here instead
    if name == :return
      find_node name, *args
    else
      raise NoMethodError.new("No method '#{name}' for Sexp", name, args)
    end
  end

  #Create clone of Sexp and nested Sexps but not their non-Sexp contents.
  #If a line number is provided, also sets line/original_line on all Sexps.
  def deep_clone line = nil
    s = Sexp.new

    self.each do |e|
      if e.is_a? Sexp
        s << e.deep_clone(line)
      else
        s << e
      end
    end

    if line
      s.original_line = self.original_line || self.line
      s.line(line)
    else
      s.original_line = self.original_line
      s.line(self.line) if self.line
    end

    s
  end

  alias_method :paren, :paren # silence redefined method warning
  def paren
    @paren ||= false
  end

  alias_method :value, :value # silence redefined method warning
  def value
    raise WrongSexpError, "Sexp#value called on multi-item Sexp: `#{self.inspect}`" if size > 2
    self[1]
  end

  def value= exp
    raise WrongSexpError, "Sexp#value= called on multi-item Sexp: `#{self.inspect}`" if size > 2
    @my_hash_value = nil
    self[1] = exp
  end

  def second
    self[1]
  end

  def to_sym
    self.value.to_sym
  end

  def node_type= type
    @my_hash_value = nil
    self[0] = type
  end

  #Join self and exp into an :or Sexp.
  #Sets or_depth.
  #Used for combining "branched" values in AliasProcessor.
  def combine exp, line = nil
    combined = Sexp.new(:or, self, exp).line(line || -2)

    combined.or_depth = [self.or_depth, exp.or_depth].compact.reduce(0, :+) + 1

    combined
  end

  alias :node_type :sexp_type
  alias :values :sexp_body # TODO: retire

  alias :old_push :<<
  alias :old_compact :compact
  alias :old_fara :find_and_replace_all
  alias :old_find_node :find_node

  def << arg
    @my_hash_value = nil
    old_push arg
  end

  alias_method :hash, :hash # silence redefined method warning
  def hash
    #There still seems to be some instances in which the hash of the
    #Sexp changes, but I have not found what method call is doing it.
    #Of course, Sexp is subclasses from Array, so who knows what might
    #be going on.
    @my_hash_value ||= super
  end

  def compact
    @my_hash_value = nil
    old_compact
  end

  def find_and_replace_all *args
    @my_hash_value = nil
    old_fara(*args)
  end

  def find_node *args
    @my_hash_value = nil
    old_find_node(*args)
  end

  #Raise a WrongSexpError if the nodes type does not match one of the expected
  #types.
  def expect *types
    unless types.include? self.node_type
      raise WrongSexpError, "Expected #{types.join ' or '} but given #{self.inspect}", caller[1..-1]
    end
  end

  #Returns target of a method call:
  #
  #s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1)))
  #         ^-----------target-----------^
  def target
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    self[1]
  end

  #Sets the target of a method call:
  def target= exp
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    @my_hash_value = nil
    self[1] = exp
  end

  #Returns method of a method call:
  #
  #s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1)))
  #                        ^- method
  def method
    expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper, :result

    case self.node_type
    when :call, :attrasgn, :safe_call, :safe_attrasgn
      self[2]
    when :super, :zsuper
      :super
    when :result
      self.last
    end
  end

  def method= name
    expect :call, :safe_call

    self[2] = name
  end

  #Sets the arglist in a method call.
  def arglist= exp
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    @my_hash_value = nil
    start_index = 3

    if exp.is_a? Sexp and exp.node_type == :arglist
      exp = exp.sexp_body
    end

    exp.each_with_index do |e, i|
      self[start_index + i] = e
    end
  end

  def set_args *exp
    self.arglist = exp
  end

  #Returns arglist for method call. This differs from Sexp#args, as Sexp#args
  #does not return a 'real' Sexp (it does not have a node type) but
  #Sexp#arglist returns a s(:arglist, ...)
  #
  #    s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1), s(:lit, 2)))
  #                                                 ^------------ arglist ------------^
  def arglist
    expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper

    case self.node_type
    when :call, :attrasgn, :safe_call, :safe_attrasgn
      self.sexp_body(3).unshift :arglist
    when :super, :zsuper
      if self[1]
        self.sexp_body.unshift :arglist
      else
        Sexp.new(:arglist)
      end
    end
  end

  #Returns arguments of a method call. This will be an 'untyped' Sexp.
  #
  #    s(:call, s(:call, nil, :x, s(:arglist)), :y, s(:arglist, s(:lit, 1), s(:lit, 2)))
  #                                                             ^--------args--------^
  def args
    expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper

    case self.node_type
    when :call, :attrasgn, :safe_call, :safe_attrasgn
      if self[3]
        self.sexp_body(3)
      else
        Sexp.new
      end
    when :super, :zsuper
      if self[1]
        self.sexp_body
      else
        Sexp.new
      end
    end
  end

  def each_arg replace = false
    expect :call, :attrasgn, :safe_call, :safe_attrasgn, :super, :zsuper
    range = nil

    case self.node_type
    when :call, :attrasgn, :safe_call, :safe_attrasgn
      if self[3]
        range = (3...self.length)
      end
    when :super, :zsuper
      if self[1]
        range = (1...self.length)
      end
    end

    if range
      range.each do |i|
        res = yield self[i]
        self[i] = res if replace
      end
    end

    self
  end

  def each_arg! &block
    @my_hash_value = nil
    self.each_arg true, &block
  end

  #Returns first argument of a method call.
  def first_arg
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    self[3]
  end

  #Sets first argument of a method call.
  def first_arg= exp
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    @my_hash_value = nil
    self[3] = exp
  end

  #Returns second argument of a method call.
  def second_arg
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    self[4]
  end

  #Sets second argument of a method call.
  def second_arg= exp
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    @my_hash_value = nil
    self[4] = exp
  end

  def third_arg
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    self[5]
  end

  def third_arg= exp
    expect :call, :attrasgn, :safe_call, :safe_attrasgn
    @my_hash_value = nil
    self[5] = exp
  end

  def last_arg
    expect :call, :attrasgn, :safe_call, :safe_attrasgn

    if self[3]
      self[-1]
    else
      nil
    end
  end

  def call_chain
    expect :call, :attrasgn, :safe_call, :safe_attrasgn

    chain = []
    call = self

    while call.class == Sexp and CALLS.include? call.first 
      chain << call.method
      call = call.target
    end

    chain.reverse!
    chain
  end

  #Returns condition of an if expression:
  #
  #    s(:if,
  #     s(:lvar, :condition), <-- condition
  #     s(:lvar, :then_val),
  #     s(:lvar, :else_val)))
  def condition
    expect :if
    self[1]
  end

  def condition= exp
    expect :if
    self[1] = exp
  end


  #Returns 'then' clause of an if expression:
  #
  #    s(:if,
  #     s(:lvar, :condition),
  #     s(:lvar, :then_val), <-- then clause
  #     s(:lvar, :else_val)))
  def then_clause
    expect :if
    self[2]
  end

  #Returns 'else' clause of an if expression:
  #
  #    s(:if,
  #     s(:lvar, :condition),
  #     s(:lvar, :then_val),
  #     s(:lvar, :else_val)))
  #     ^---else caluse---^
  def else_clause
    expect :if
    self[3]
  end

  #Method call associated with a block:
  #
  #    s(:iter,
  #     s(:call, nil, :x, s(:arglist)), <- block_call
  #      s(:lasgn, :y),
  #       s(:block, s(:lvar, :y), s(:call, nil, :z, s(:arglist))))
  def block_call
    expect :iter

    if self[1].node_type == :lambda
      s(:call, nil, :lambda).line(self.line)
    else
      self[1]
    end
  end

  #Returns block of a call with a block.
  #Could be a single expression or a block:
  #
  #    s(:iter,
  #     s(:call, nil, :x, s(:arglist)),
  #      s(:lasgn, :y),
  #       s(:block, s(:lvar, :y), s(:call, nil, :z, s(:arglist))))
  #       ^-------------------- block --------------------------^
  def block delete = nil
    unless delete.nil? #this is from RubyParser
      return find_node :block, delete
    end

    expect :iter, :scope, :resbody

    case self.node_type
    when :iter
      self[3]
    when :scope
      self[1]
    when :resbody
      #This is for Ruby2Ruby ONLY
      find_node :block
    end
  end

  #Returns parameters for a block
  #
  #    s(:iter,
  #     s(:call, nil, :x, s(:arglist)),
  #      s(:lasgn, :y), <- block_args
  #       s(:call, nil, :p, s(:arglist, s(:lvar, :y))))
  def block_args
    expect :iter
    if self[2] == 0 # ?! See https://github.com/presidentbeef/brakeman/issues/331
      return Sexp.new(:args)
    else
      self[2]
    end
  end

  def first_param
    expect :args
    self[1]
  end

  #Returns the left hand side of assignment or boolean:
  #
  #    s(:lasgn, :x, s(:lit, 1))
  #               ^--lhs
  def lhs
    expect(*ASSIGNMENT_BOOL)
    self[1]
  end

  #Sets the left hand side of assignment or boolean.
  def lhs= exp
    expect(*ASSIGNMENT_BOOL)
    @my_hash_value = nil
    self[1] = exp
  end

  #Returns right side (value) of assignment or boolean:
  #
  #    s(:lasgn, :x, s(:lit, 1))
  #                  ^--rhs---^
  def rhs
    expect :attrasgn, :safe_attrasgn, *ASSIGNMENT_BOOL

    if self.node_type == :attrasgn or self.node_type == :safe_attrasgn
      if self[2] == :[]=
        self[4]
      else
        self[3]
      end
    else
      self[2]
    end
  end

  #Sets the right hand side of assignment or boolean.
  def rhs= exp
    expect :attrasgn, :safe_attrasgn, *ASSIGNMENT_BOOL
    @my_hash_value = nil

    if self.node_type == :attrasgn or self.node_type == :safe_attrasgn
      self[3] = exp
    else
      self[2] = exp
    end
  end

  #Returns name of method being defined in a method definition.
  def method_name
    expect :defn, :defs

    case self.node_type
    when :defn
      self[1]
    when :defs
      self[2]
    end
  end

  def formal_args
    expect :defn, :defs

    case self.node_type
    when :defn
      self[2]
    when :defs
      self[3]
    end
  end

  #Sets body, which is now a complicated process because the body is no longer
  #a separate Sexp, but just a list of Sexps.
  def body= exp
    expect :defn, :defs, :class, :module
    @my_hash_value = nil

    case self.node_type
    when :defn, :class
      index = 3
    when :defs
      index = 4
    when :module
      index = 2
    end

    self.slice!(index..-1) #Remove old body

    if exp.first == :rlist
      exp = exp.sexp_body
    end

    #Insert new body
    exp.each do |e|
      self[index] = e
      index += 1
    end
  end

  #Returns body of a method definition, class, or module.
  #This will be an untyped Sexp containing a list of Sexps from the body.
  def body
    expect :defn, :defs, :class, :module

    case self.node_type
    when :defn, :class
      self.sexp_body(3)
    when :defs
      self.sexp_body(4)
    when :module
      self.sexp_body(2)
    end
  end

  #Like Sexp#body, except the returned Sexp is of type :rlist
  #instead of untyped.
  def body_list
    self.body.unshift :rlist
  end

  # Number of "statements" in a method.
  # This is more efficient than `Sexp#body.length`
  # because `Sexp#body` creates a new Sexp.
  def method_length
    expect :defn, :defs

    case self.node_type
    when :defn
      self.length - 3
    when :defs
      self.length - 4
    end
  end

  def render_type
    expect :render
    self[1]
  end

  def class_name
    expect :class, :module
    self[1]
  end

  alias module_name class_name

  def parent_name
    expect :class
    self[2]
  end

  #Returns the call Sexp in a result returned from FindCall
  def call
    expect :result

    self.last
  end

  #Returns the module the call is inside
  def module
    expect :result

    self[1]
  end

  #Return the class the call is inside
  def result_class
    expect :result

    self[2]
  end

  require 'set'
  def inspect seen = Set.new
    if seen.include? self.object_id
      's(...)'
    else
      seen << self.object_id
      sexp_str = self.map do |x|
        if x.is_a? Sexp
          x.inspect seen
        else
          x.inspect
        end
      end.join(', ')

      "s(#{sexp_str})"
    end
  end
end

#Invalidate hash cache if the Sexp changes
[:[]=, :clear, :collect!, :compact!, :concat, :delete, :delete_at,
  :delete_if, :drop, :drop_while, :fill, :flatten!, :insert,
  :keep_if, :map!, :pop, :push, :reject!, :replace, :reverse!, :rotate!,
  :select!, :shift, :shuffle!, :slice!, :sort!, :sort_by!, :transpose,
  :uniq!, :unshift].each do |method|

  Sexp.class_eval <<-RUBY
    def #{method} *args
      @my_hash_value = nil
      super
    end
    RUBY
end

#Methods used by RubyParser which would normally go through method_missing but
#we don't want that to happen because it hides Brakeman errors
[:resbody, :lasgn, :iasgn, :splat].each do |method|
  Sexp.class_eval <<-RUBY
    def #{method} delete = false
      if delete
        @my_hash_value = false
      end
      find_node :#{method}, delete
    end
  RUBY
end

class String
  ##
  # This is a hack used by the lexer to sneak in line numbers at the
  # identifier level. This should be MUCH smaller than making
  # process_token return [value, lineno] and modifying EVERYTHING that
  # reduces tIDENTIFIER.

  attr_accessor :lineno
end

class WrongSexpError < RuntimeError; end