From 8a6053bad0df595a2f3d55ceac8513a06a6a85f4 Mon Sep 17 00:00:00 2001 From: Alexander Momchilov Date: Fri, 28 Jul 2023 10:30:05 -0400 Subject: [PATCH] Add hook for customizing the debug representation of objects --- lib/debug/variable_inspector.rb | 14 +++++++++++ test/debug/variable_inspector_test.rb | 35 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/lib/debug/variable_inspector.rb b/lib/debug/variable_inspector.rb index f74e170db..4d4db0b34 100644 --- a/lib/debug/variable_inspector.rb +++ b/lib/debug/variable_inspector.rb @@ -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]) } @@ -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 diff --git a/test/debug/variable_inspector_test.rb b/test/debug/variable_inspector_test.rb index 33c833685..05f6c2b8a 100644 --- a/test/debug/variable_inspector_test.rb +++ b/test/debug/variable_inspector_test.rb @@ -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) @@ -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