lib/debug/tracer.rb



# frozen_string_literal: true

module DEBUGGER__
  class Tracer
    include SkipPathHelper
    include Color

    def colorize(str, color)
      # don't colorize trace sent into a file
      if @into
        str
      else
        super
      end
    end

    attr_reader :type, :key

    def initialize ui, pattern: nil, into: nil
      if /\ADEBUGGER__::(([A-Z][a-z]+?)[A-Z][a-z]+)/ =~ self.class.name
        @name = $1
        @type = $2.downcase
      end

      setup

      if pattern
        @pattern = Regexp.compile(pattern)
      else
        @pattern = nil
      end

      if @into = into
        @output = File.open(into, 'w')
        @output.puts "PID:#{Process.pid} #{self}"
      else
        @output = ui
      end

      @key = [@type, @pattern, @into].freeze

      enable
    end

    def header depth
      "DEBUGGER (trace/#{@type}) \#th:#{Thread.current.instance_variable_get(:@__thread_client_id)} \#depth:#{'%-2d'%depth}"
    end

    def enable
      @tracer.enable
    end

    def disable
      @tracer.disable
    end

    def enabled?
      @tracer.enabled?
    end

    def description
      nil
    end

    def to_s
      s = "#{@name}#{description} (#{@tracer.enabled? ? 'enabled' : 'disabled'})"
      s += " with pattern #{@pattern.inspect}" if @pattern
      s += " into: #{@into}" if @into
      s
    end

    def skip? tp
      ThreadClient.current.management? || skip_path?(tp.path) || skip_with_pattern?(tp)
    end

    def skip_with_pattern?(tp)
      @pattern && !tp.path.match?(@pattern)
    end

    def out tp, msg = nil, depth = caller.size - 1
      location_str = colorize("#{FrameInfo.pretty_path(tp.path)}:#{tp.lineno}", [:GREEN])
      buff = "#{header(depth)}#{msg} at #{location_str}"

      if false # TODO: Ractor.main?
        ThreadClient.current.on_trace self.object_id, buff
      else
        @output.puts buff
        @output.flush
      end
    end

    def minfo tp
      return "block{}" if tp.event == :b_call

      klass = tp.defined_class

      if klass.singleton_class?
        "#{tp.self}.#{tp.method_id}"
      else
        "#{klass}\##{tp.method_id}"
      end
    end
  end

  class LineTracer < Tracer
    def setup
      @tracer = TracePoint.new(:line){|tp|
        next if skip?(tp)
        # pp tp.object_id, caller(0)
        out tp
      }
    end
  end

  class CallTracer < Tracer
    def setup
      @tracer = TracePoint.new(:a_call, :a_return){|tp|
        next if skip?(tp)

        depth = caller.size

        call_identifier_str =
          if tp.defined_class
            minfo(tp)
          else
            "block"
          end

        call_identifier_str = colorize_blue(call_identifier_str)

        case tp.event
        when :call, :c_call, :b_call
          depth += 1 if tp.event == :c_call
          sp = ' ' * depth
          out tp, ">#{sp}#{call_identifier_str}", depth
        when :return, :c_return, :b_return
          depth += 1 if tp.event == :c_return
          sp = ' ' * depth
          return_str = colorize_magenta(DEBUGGER__.safe_inspect(tp.return_value, short: true))
          out tp, "<#{sp}#{call_identifier_str} #=> #{return_str}", depth
        end
      }
    end

    def skip_with_pattern?(tp)
      super && !tp.method_id&.match?(@pattern)
    end
  end

  class ExceptionTracer < Tracer
    def setup
      @tracer = TracePoint.new(:raise) do |tp|
        next if skip?(tp)

        exc = tp.raised_exception

        out tp, " #{colorize_magenta(exc.inspect)}"
      rescue Exception => e
        p e
      end
    end

    def skip_with_pattern?(tp)
      super && !tp.raised_exception.inspect.match?(@pattern)
    end
  end

  class ObjectTracer < Tracer
    def initialize ui, obj_id, obj_inspect, **kw
      @obj_id = obj_id
      @obj_inspect = obj_inspect
      super(ui, **kw)
      @key = [@type, @obj_id, @pattern, @into].freeze
    end

    def description
      " for #{@obj_inspect}"
    end

    def colorized_obj_inspect
      colorize_magenta(@obj_inspect)
    end

    def setup
      @tracer = TracePoint.new(:a_call){|tp|
        next if skip?(tp)

        if M_OBJECT_ID.bind_call(tp.self) == @obj_id
          klass = tp.defined_class
          method = tp.method_id
          method_info =
            if klass.singleton_class?
              if tp.self.is_a?(Class)
                ".#{method} (#{klass}.#{method})"
              else
                ".#{method}"
              end
            else
              "##{method} (#{klass}##{method})"
            end

          out tp, " #{colorized_obj_inspect} receives #{colorize_blue(method_info)}"
        elsif !tp.parameters.empty?
          b = tp.binding
          method_info = colorize_blue(minfo(tp))

          tp.parameters.each{|type, name|
            next unless name

            colorized_name = colorize_cyan(name)

            case type
            when :req, :opt, :key, :keyreq
              if b.local_variable_get(name).object_id == @obj_id
                out tp, " #{colorized_obj_inspect} is used as a parameter #{colorized_name} of #{method_info}"
              end
            when :rest
              next if name == :"*"

              ary = b.local_variable_get(name)
              ary.each{|e|
                if e.object_id == @obj_id
                  out tp, " #{colorized_obj_inspect} is used as a parameter in #{colorized_name} of #{method_info}"
                end
              }
            when :keyrest
              next if name == :'**'
              h = b.local_variable_get(name)
              h.each{|k, e|
                if e.object_id == @obj_id
                  out tp, " #{colorized_obj_inspect} is used as a parameter in #{colorized_name} of #{method_info}"
                end
              }
            end
          }
        end
      }
    end
  end
end