Skip to content

Commit

Permalink
Add hook for customizing the debug representation of objects
Browse files Browse the repository at this point in the history
  • Loading branch information
amomchilov committed Nov 14, 2024
1 parent 48d487d commit 8a6053b
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 0 deletions.
14 changes: 14 additions & 0 deletions lib/debug/variable_inspector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ def indexed_members_of(obj, start:, count:)
def named_members_of(obj)
return [] if NaiveString === obj

if M_RESPOND_TO.bind_call(obj, :debug_representation)
debug_representation = obj.debug_representation
members = named_members_of(debug_representation)

# Discard the "#class" member of the debug representation, if any.
members.delete_if { |m| m.name == '#class' }

# Add the real "#class" of the object being inspected.
members.unshift Variable.internal(name: '#class', value: M_CLASS.bind_call(obj))

return members
end

members = case obj
when Hash then obj.map { |k, v| Variable.new(name: value_inspect(k), value: v) }
when Struct then obj.members.map { |name| Variable.new(name: name, value: obj[name]) }
Expand Down Expand Up @@ -69,6 +82,7 @@ def self.value_inspect(obj, short: true)

# TODO: Replace with Reflection helpers once they are merged
# https://github.com/ruby/debug/pull/1002
M_RESPOND_TO = method(:respond_to?).unbind
M_INSTANCE_VARIABLES = method(:instance_variables).unbind
M_INSTANCE_VARIABLE_GET = method(:instance_variable_get).unbind
M_CLASS = method(:class).unbind
Expand Down
35 changes: 35 additions & 0 deletions test/debug/variable_inspector_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,31 @@ def test_named_members_of_other_objects
assert_equal expected, @inspector.named_members_of(point)
end

def test_debug_representation_hook
object_with_simple_repr = ClassWithCustomDebugRepresentation.new({ a: 1, b: 2 })

expected = [
# We should always show the `#class` when using this hook, even if the
# debug_representation is a simple value.
Variable.internal(name: '#class', value: ClassWithCustomDebugRepresentation),
Variable.new(name: ':a', value: 1),
Variable.new(name: ':b', value: 2),
]

assert_equal expected, @inspector.named_members_of(object_with_simple_repr)

object_with_complex_repr = ClassWithCustomDebugRepresentation.new(Point.new(x: 1, y: 2))

expected = [
# Make sure we don't add the '#class' twice for non-simple debug representations
Variable.internal(name: '#class', value: ClassWithCustomDebugRepresentation),
Variable.new(name: :@x, value: 1),
Variable.new(name: :@y, value: 2),
]

assert_equal expected, @inspector.named_members_of(object_with_complex_repr)
end

private

class PointStruct < Struct.new(:x, :y, keyword_init: true)
Expand All @@ -211,5 +236,15 @@ def initialize(x:, y:)
@y = y
end
end

class ClassWithCustomDebugRepresentation
def initialize(debug_representation)
@debug_representation = debug_representation
end

def debug_representation
@debug_representation
end
end
end
end

0 comments on commit 8a6053b

Please sign in to comment.