Skip to content

Commit

Permalink
Share variable inspection logic between CDP and DAP
Browse files Browse the repository at this point in the history
  • Loading branch information
amomchilov committed Jul 27, 2023
1 parent 4ec9d7a commit 1295842
Show file tree
Hide file tree
Showing 6 changed files with 412 additions and 130 deletions.
52 changes: 52 additions & 0 deletions lib/debug/limited_pp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require "pp"

module DEBUGGER__
class LimitedPP
SHORT_INSPECT_LENGTH = 40

def self.pp(obj, max = 80)
out = self.new(max)
catch out do
::PP.singleline_pp(obj, out)
end
out.buf
end

attr_reader :buf

def initialize max
@max = max
@cnt = 0
@buf = String.new
end

def <<(other)
@buf << other

if @buf.size >= @max
@buf = @buf[0..@max] + '...'
throw self
end
end

def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
if short
LimitedPP.pp(obj, max_length)
else
obj.inspect
end
rescue NoMethodError => e
klass, oid = M_CLASS.bind_call(obj), M_OBJECT_ID.bind_call(obj)
if obj == (r = e.receiver)
"<\##{klass.name}#{oid} does not have \#inspect>"
else
rklass, roid = M_CLASS.bind_call(r), M_OBJECT_ID.bind_call(r)
"<\##{klass.name}:#{roid} contains <\##{rklass}:#{roid} and it does not have #inspect>"
end
# rescue Exception => e
# "<#inspect raises #{e.inspect}>"
end
end
end
54 changes: 19 additions & 35 deletions lib/debug/server_cdp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require 'tmpdir'
require 'tempfile'
require 'timeout'
require_relative 'variable_inspector'

module DEBUGGER__
module UI_CDP
Expand Down Expand Up @@ -1112,46 +1113,29 @@ def process_cdp args
event! :protocol_result, :scope, req, vars
when :properties
oid = args.shift
result = []
prop = []

if obj = @obj_map[oid]
case obj
when Array
result = obj.map.with_index{|o, i|
variable i.to_s, o
}
when Hash
result = obj.map{|k, v|
variable(k, v)
}
when Struct
result = obj.members.map{|m|
variable(m, obj[m])
}
when String
prop = [
internalProperty('#length', obj.length),
internalProperty('#encoding', obj.encoding)
]
when Class, Module
result = obj.instance_variables.map{|iv|
variable(iv, obj.instance_variable_get(iv))
}
prop = [internalProperty('%ancestors', obj.ancestors[1..])]
when Range
prop = [
internalProperty('#begin', obj.begin),
internalProperty('#end', obj.end),
]
members = if obj.is_a?(Array)
VariableInspector.new.indexed_members_of(obj, start: 0, count: obj.size)
else
VariableInspector.new.named_members_of(obj)
end

result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
}
prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
result = members.filter_map do |member|
next if member.internal?
variable(member.name, member.value)
end

internal_properties = members.filter_map do |member|
next unless member.internal?
internalProperty(member.name, member.value)
end
else
result = []
internal_properties = []
end
event! :protocol_result, :properties, req, result: result, internalProperties: prop

event! :protocol_result, :properties, req, result: result, internalProperties: internal_properties
when :exception
oid = args.shift
exc = nil
Expand Down
60 changes: 12 additions & 48 deletions lib/debug/server_dap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'irb/completion'
require 'tmpdir'
require 'fileutils'
require_relative 'variable_inspector'

module DEBUGGER__
module UI_DAP
Expand Down Expand Up @@ -765,18 +766,11 @@ def register_vars vars, tid
end
end

class NaiveString
attr_reader :str
def initialize str
@str = str
end
end

class ThreadClient
MAX_LENGTH = 180

def value_inspect obj, short: true
# TODO: max length should be configuarable?
# TODO: max length should be configurable?
str = DEBUGGER__.safe_inspect obj, short: short, max_length: MAX_LENGTH

if str.encoding == Encoding::UTF_8
Expand Down Expand Up @@ -875,48 +869,18 @@ def process_dap args
vid = args.shift
obj = @var_map[vid]
if obj
case req.dig('arguments', 'filter')
members = case req.dig('arguments', 'filter')
when 'indexed'
start = req.dig('arguments', 'start') || 0
count = req.dig('arguments', 'count') || obj.size
vars = (start ... (start + count)).map{|i|
variable(i.to_s, obj[i])
}
VariableInspector.new.indexed_members_of(
obj,
start: req.dig('arguments', 'start') || 0,
count: req.dig('arguments', 'count') || obj.size,
)
else
vars = []

case obj
when Hash
vars = obj.map{|k, v|
variable(value_inspect(k), v,)
}
when Struct
vars = obj.members.map{|m|
variable(m, obj[m])
}
when String
vars = [
variable('#length', obj.length),
variable('#encoding', obj.encoding),
]
printed_str = value_inspect(obj)
vars << variable('#dump', NaiveString.new(obj)) if printed_str.end_with?('...')
when Class, Module
vars << variable('%ancestors', obj.ancestors[1..])
when Range
vars = [
variable('#begin', obj.begin),
variable('#end', obj.end),
]
end

unless NaiveString === obj
vars += M_INSTANCE_VARIABLES.bind_call(obj).sort.map{|iv|
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
}
vars.unshift variable('#class', M_CLASS.bind_call(obj))
end
VariableInspector.new.named_members_of(obj)
end

vars = members.map { |member| variable(member.name, member.value) }
end
event! :protocol_result, :variable, req, variables: (vars || []), tid: self.id

Expand Down Expand Up @@ -1059,7 +1023,7 @@ def variable_ name, obj, indexedVariables: 0, namedVariables: 0

namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size

if NaiveString === obj
if VariableInspector::NaiveString === obj
str = obj.str.dump
vid = indexedVariables = namedVariables = 0
else
Expand Down
50 changes: 3 additions & 47 deletions lib/debug/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
require_relative 'source_repository'
require_relative 'breakpoint'
require_relative 'tracer'
require_relative 'limited_pp'

# To prevent loading old lib/debug.rb in Ruby 2.6 to 3.0
$LOADED_FEATURES << 'debug.rb'
Expand Down Expand Up @@ -2302,53 +2303,8 @@ def self.load_rc
end
end

# Inspector

SHORT_INSPECT_LENGTH = 40

class LimitedPP
def self.pp(obj, max=80)
out = self.new(max)
catch out do
PP.singleline_pp(obj, out)
end
out.buf
end

attr_reader :buf

def initialize max
@max = max
@cnt = 0
@buf = String.new
end

def <<(other)
@buf << other

if @buf.size >= @max
@buf = @buf[0..@max] + '...'
throw self
end
end
end

def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
if short
LimitedPP.pp(obj, max_length)
else
obj.inspect
end
rescue NoMethodError => e
klass, oid = M_CLASS.bind_call(obj), M_OBJECT_ID.bind_call(obj)
if obj == (r = e.receiver)
"<\##{klass.name}#{oid} does not have \#inspect>"
else
rklass, roid = M_CLASS.bind_call(r), M_OBJECT_ID.bind_call(r)
"<\##{klass.name}:#{roid} contains <\##{rklass}:#{roid} and it does not have #inspect>"
end
rescue Exception => e
"<#inspect raises #{e.inspect}>"
def self.safe_inspect obj, max_length: LimitedPP::SHORT_INSPECT_LENGTH, short: false
LimitedPP.safe_inspect(obj, max_length: max_length, short: short)
end

def self.warn msg
Expand Down
110 changes: 110 additions & 0 deletions lib/debug/variable_inspector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# frozen_string_literal: true

require_relative 'limited_pp'

module DEBUGGER__
class VariableInspector
class Member
attr_reader :name, :value

def initialize(name:, value:, internal: false)
@name = name
@value = value
@is_internal = internal
end

def internal?
@is_internal
end

def self.internal name:, value:
new(name:, value:, internal: true)
end

def ==(other)
other.instance_of?(self.class) &&
@name == other.name &&
@value == other.value &&
@is_internal == other.internal?
end

def inspect
"#<Member name=#{@name.inspect} value=#{@value.inspect}#{@is_internal ? " internal" : ""}>"
end
end

def indexed_members_of obj, start:, count:
return [] if start > (obj.length - 1)

capped_count = [count, obj.length - start].min

(start...(start + capped_count)).map do |i|
Member.new(name: i.to_s, value: obj[i])
end
end

def named_members_of obj
members = case obj
when Hash then obj.map { |k, v| Member.new(name: value_inspect(k), value: v) }
when Struct then obj.members.map { |name| Member.new(name:, value: obj[name]) }
when String
members = [
Member.internal(name: '#length', value: obj.length),
Member.internal(name: '#encoding', value: obj.encoding),
]

printed_str = value_inspect(obj)
members << Member.internal(name: "#dump", value: NaiveString.new(obj)) if printed_str.end_with?('...')

members
when Class, Module then [Member.internal(name: "%ancestors", value: obj.ancestors[1..])]
when Range then [
Member.internal(name: "#begin", value: obj.begin),
Member.internal(name: "#end", value: obj.end),
]
else []
end

unless NaiveString === obj
members += M_INSTANCE_VARIABLES.bind_call(obj).sort.map{|iv|
Member.new(name: iv, value: M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
}
members.unshift Member.internal(name: '#class', value: M_CLASS.bind_call(obj))
end

members
end

private

MAX_LENGTH = 180

def value_inspect obj, short: true
# TODO: max length should be configurable?
str = LimitedPP.safe_inspect obj, short: short, max_length: MAX_LENGTH

if str.encoding == Encoding::UTF_8
str.scrub
else
str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
end
end

# TODO: Replace with Reflection helpers once they are merged
# https://github.com/ruby/debug/pull/1002
M_INSTANCE_VARIABLES = method(:instance_variables).unbind
M_INSTANCE_VARIABLE_GET = method(:instance_variable_get).unbind
M_CLASS = method(:class).unbind

class NaiveString
attr_reader :str
def initialize str
@str = str
end

def == other
other.instance_of?(self.class) && @str == other.str
end
end
end
end
Loading

0 comments on commit 1295842

Please sign in to comment.