From 0213f329410d623b1b19c7fed44777280ed59436 Mon Sep 17 00:00:00 2001 From: Alexander Momchilov Date: Thu, 27 Jul 2023 14:01:04 -0400 Subject: [PATCH] Fix incorrect bind_call backport and extract Reflection methods --- lib/debug/reflection.rb | 72 +++++++++++++++++++++ lib/debug/server_cdp.rb | 6 +- lib/debug/server_dap.rb | 12 ++-- lib/debug/session.rb | 13 +--- lib/debug/thread_client.rb | 30 +++------ lib/debug/tracer.rb | 2 +- test/debug/reflection_test.rb | 117 ++++++++++++++++++++++++++++++++++ 7 files changed, 211 insertions(+), 41 deletions(-) create mode 100644 lib/debug/reflection.rb create mode 100644 test/debug/reflection_test.rb diff --git a/lib/debug/reflection.rb b/lib/debug/reflection.rb new file mode 100644 index 000000000..9bb086889 --- /dev/null +++ b/lib/debug/reflection.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module DEBUGGER__ + module Reflection + module_function def instance_variables_of(o) + M_INSTANCE_VARIABLES.bind_call(o) + end + + module_function def instance_variable_get_from(o, name) + M_INSTANCE_VARIABLE_GET.bind_call(o, name) + end + + module_function def class_of(o) + M_CLASS.bind_call(o) + end + + module_function def singleton_class_of(o) + M_SINGLETON_CLASS.bind_call(o) + end + + module_function def is_kind_of?(object, type) + M_KIND_OF_P.bind_call(object, type) + end + + module_function def responds_to?(object, message, include_all: false) + M_RESPOND_TO_P.bind_call(object, message, include_all) + end + + module_function def method_of(type, method_name) + M_METHOD.bind_call(type, method_name) + end + + module_function def object_id_of(o) + M_OBJECT_ID.bind_call(o) + end + + module_function def name_of(type) + M_NAME.bind_call(type) + end + + M_INSTANCE_VARIABLES = Kernel.instance_method(:instance_variables) + M_INSTANCE_VARIABLE_GET = Kernel.instance_method(:instance_variable_get) + M_CLASS = Kernel.instance_method(:class) + M_SINGLETON_CLASS = Kernel.instance_method(:singleton_class) + M_KIND_OF_P = Kernel.instance_method(:kind_of?) + M_RESPOND_TO_P = Kernel.instance_method(:respond_to?) + M_METHOD = Kernel.instance_method(:method) + M_OBJECT_ID = Kernel.instance_method(:object_id) + M_NAME = Module.instance_method(:name) + + private_constant( + :M_INSTANCE_VARIABLES, + :M_INSTANCE_VARIABLE_GET, + :M_CLASS, + :M_SINGLETON_CLASS, + :M_KIND_OF_P, + :M_RESPOND_TO_P, + :M_METHOD, + :M_OBJECT_ID, + :M_NAME, + ) + end +end + +# for Ruby 2.6 compatibility +unless UnboundMethod.method_defined?(:bind_call) + class UnboundMethod + def bind_call(receiver, *args, &block) + bind(receiver).call(*args, &block) + end + end +end diff --git a/lib/debug/server_cdp.rb b/lib/debug/server_cdp.rb index ef556febd..b33f8659f 100644 --- a/lib/debug/server_cdp.rb +++ b/lib/debug/server_cdp.rb @@ -1059,8 +1059,8 @@ def process_cdp args result = b.local_variable_get(expr) rescue NameError # try to check method - if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true) - result = M_METHOD.bind_call(b.receiver, expr) + if Reflection.responds_to?(b.receiver, expr, include_all: true) + result = Reflection.method_of(b.receiver, expr) else message = "Error: Can not evaluate: #{expr.inspect}" end @@ -1246,7 +1246,7 @@ def propertyDescriptor_ name, obj, type, description: nil, subtype: nil v = prop[:value] v.delete :value v[:subtype] = subtype if subtype - v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s + v[:className] = (klass = Reflection.class_of(obj)).name || klass.to_s end prop end diff --git a/lib/debug/server_dap.rb b/lib/debug/server_dap.rb index 8dbefda8a..511eb95c3 100644 --- a/lib/debug/server_dap.rb +++ b/lib/debug/server_dap.rb @@ -937,7 +937,7 @@ def process_dap args case expr when /\A\@\S/ begin - result = M_INSTANCE_VARIABLE_GET.bind_call(b.receiver, expr) + result = Reflection.instance_variable_get_from(b.receiver, expr) rescue NameError message = "Error: Not defined instance variable: #{expr.inspect}" end @@ -959,8 +959,8 @@ def process_dap args result = b.local_variable_get(expr) rescue NameError # try to check method - if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true) - result = M_METHOD.bind_call(b.receiver, expr) + if Reflection.responds_to?(b.receiver, expr, include_all: true) + result = Reflection.method_of(b.receiver, expr) else message = "Error: Can not evaluate: #{expr.inspect}" end @@ -1040,10 +1040,10 @@ def evaluate_result r end def type_name obj - klass = M_CLASS.bind_call(obj) + klass = Reflection.class_of(obj) begin - M_NAME.bind_call(klass) || klass.to_s + Reflection.name_of(klass) || klass.to_s rescue Exception => e "" end @@ -1057,7 +1057,7 @@ def variable_ name, obj, indexedVariables: 0, namedVariables: 0 vid = 0 end - namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size + namedVariables += Reflection.instance_variables_of(obj).size if NaiveString === obj str = obj.str.dump diff --git a/lib/debug/session.rb b/lib/debug/session.rb index e9d95775c..3f79988bd 100644 --- a/lib/debug/session.rb +++ b/lib/debug/session.rb @@ -2340,11 +2340,11 @@ def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false obj.inspect end rescue NoMethodError => e - klass, oid = M_CLASS.bind_call(obj), M_OBJECT_ID.bind_call(obj) + klass, oid = Reflection.class_of(obj), Reflection.object_id_of(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) + rklass, roid = Reflection.class_of(r), Reflection.object_id_of(r) "<\##{klass.name}:#{roid} contains <\##{rklass}:#{roid} and it does not have #inspect>" end rescue Exception => e @@ -2623,12 +2623,3 @@ class Binding alias break debugger alias b debugger end - -# for Ruby 2.6 compatibility -unless method(:p).unbind.respond_to? :bind_call - class UnboundMethod - def bind_call(obj, *args) - self.bind(obj).call(*args) - end - end -end diff --git a/lib/debug/thread_client.rb b/lib/debug/thread_client.rb index 57a802615..9b846a704 100644 --- a/lib/debug/thread_client.rb +++ b/lib/debug/thread_client.rb @@ -6,16 +6,6 @@ require_relative 'color' module DEBUGGER__ - M_INSTANCE_VARIABLES = method(:instance_variables).unbind - M_INSTANCE_VARIABLE_GET = method(:instance_variable_get).unbind - M_CLASS = method(:class).unbind - M_SINGLETON_CLASS = method(:singleton_class).unbind - M_KIND_OF_P = method(:kind_of?).unbind - M_RESPOND_TO_P = method(:respond_to?).unbind - M_METHOD = method(:method).unbind - M_OBJECT_ID = method(:object_id).unbind - M_NAME = method(:name).unbind - module SkipPathHelper def skip_path?(path) !path || @@ -590,8 +580,8 @@ def show_ivars pat, expr = nil end if _self - M_INSTANCE_VARIABLES.bind_call(_self).sort.each{|iv| - value = M_INSTANCE_VARIABLE_GET.bind_call(_self, iv) + Reflection.instance_variables_of(_self).sort.each{|iv| + value = Reflection.instance_variable_get_from(_self, iv) puts_variable_info iv, value, pat } end @@ -617,7 +607,7 @@ def get_consts expr = nil, only_self: false, &block rescue Exception => e # ignore else - if M_KIND_OF_P.bind_call(_self, Module) + if Reflection.is_kind_of(_self, Module) iter_consts _self, &block return else @@ -626,10 +616,10 @@ def get_consts expr = nil, only_self: false, &block end elsif _self = current_frame&.self cs = {} - if M_KIND_OF_P.bind_call(_self, Module) + if Reflection.is_kind_of(_self, Module) cs[_self] = :self else - _self = M_CLASS.bind_call(_self) + _self = Reflection.class_of(_self) cs[_self] = :self unless only_self end @@ -769,12 +759,12 @@ def show_outline expr locals = current_frame&.local_variables - klass = M_CLASS.bind_call(obj) + klass = Reflection.class_of(obj) klass = obj if Class == klass || Module == klass - o.dump("constants", obj.constants) if M_RESPOND_TO_P.bind_call(obj, :constants) + o.dump("constants", obj.constants) if Reflection.responds_to?(obj, :constants) outline_method(o, klass, obj) - o.dump("instance variables", M_INSTANCE_VARIABLES.bind_call(obj)) + o.dump("instance variables", Reflection.instance_variables_of(obj)) o.dump("class variables", klass.class_variables) o.dump("locals", locals.keys) if locals end @@ -782,7 +772,7 @@ def show_outline expr def outline_method(o, klass, obj) begin - singleton_class = M_SINGLETON_CLASS.bind_call(obj) + singleton_class = Reflection.singleton_class_of(obj) rescue TypeError singleton_class = nil end @@ -1192,7 +1182,7 @@ def wait_next_action_ obj_inspect = truncate(obj_inspect, width: width) end - event! :result, :trace_pass, M_OBJECT_ID.bind_call(obj), obj_inspect, opt + event! :result, :trace_pass, Reflection.object_id_of(obj), obj_inspect, opt rescue => e puts e.message event! :result, nil diff --git a/lib/debug/tracer.rb b/lib/debug/tracer.rb index 49c6923c1..30b573feb 100644 --- a/lib/debug/tracer.rb +++ b/lib/debug/tracer.rb @@ -185,7 +185,7 @@ def setup @tracer = TracePoint.new(:a_call){|tp| next if skip?(tp) - if M_OBJECT_ID.bind_call(tp.self) == @obj_id + if Reflection.object_id_of(tp.self) == @obj_id klass = tp.defined_class method = tp.method_id method_info = diff --git a/test/debug/reflection_test.rb b/test/debug/reflection_test.rb new file mode 100644 index 000000000..b7045590b --- /dev/null +++ b/test/debug/reflection_test.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'test/unit' +require_relative '../../lib/debug/reflection' + +module DEBUGGER__ + class ReflectionTest < Test::Unit::TestCase + def setup + @sample_object = SampleClass.new(1, 2) + end + + def test_instance_variables_of + assert_equal [:@a, :@b], Reflection.instance_variables_of(@sample_object) + end + + def test_instance_variables_get + assert_equal 1, Reflection.instance_variable_get_from(@sample_object, :@a) + assert_equal 2, Reflection.instance_variable_get_from(@sample_object, :@b) + end + + def test_class_of + assert_same SampleClass, Reflection.class_of(@sample_object) + end + + def test_singleton_class_of + expected = class << SampleClass + self + end + + assert_same expected, Reflection.singleton_class_of(SampleClass) + end + + def test_is_kind_of?() + assert_true Reflection.is_kind_of?(@sample_object, SampleClass) + assert_false Reflection.is_kind_of?(@sample_object, Object) + end + + def test_responds_to? + assert_true Reflection.responds_to?(@sample_object, :a) + assert_false Reflection.responds_to?(@sample_object, :doesnt_exist) + + assert_false Reflection.responds_to?(@sample_object, :sample_private_method) + assert_false Reflection.responds_to?(@sample_object, :sample_private_method, include_all: false) + assert_true Reflection.responds_to?(@sample_object, :sample_private_method, include_all: true) + end + + def test_method_of + assert_equal 1, Reflection.method_of(@sample_object, :a).call + end + + def test_object_id_of + assert_equal @sample_object.__id__, Reflection.object_id_of(@sample_object) + end + + def test_name_of + assert_equal "DEBUGGER__::ReflectionTest::SampleClass", Reflection.name_of(SampleClass) + end + + def test_bind_call_backport + omit_if( + UnboundMethod.instance_method(:bind_call).source_location.nil?, + "This Ruby version (#{RUBY_VERSION}) has a native #bind_call implementation, so it doesn't need the backport.", + ) + + puts caller_locations + original_object = SampleTarget.new("original") + new_target = SampleTarget.new("new") + + m = original_object.method(:sample_method).unbind + + rest_args = ["a1", "a2"] + kwargs = { k1: 1, k2: 2 } + proc = Proc.new { |x| x } + + result = m.bind_call(new_target, "parg1", "parg2", *rest_args, **kwargs, &proc) + + assert_equal "new", result.fetch(:self).id + assert_equal "parg1", result.fetch(:parg1) + assert_equal "parg2", result.fetch(:parg2) + assert_equal rest_args, result.fetch(:rest_args) + assert_equal kwargs, result.fetch(:kwargs) + assert_same proc, result.fetch(:block) + end + + private + + # A class for testing reflection, which doesn't implement all the usual reflection methods being tested. + class SampleClass < BasicObject + attr_reader :a, :b + + def initialize(a, b) + @a = a + @b = b + end + + private + + def sample_private_method; end + + class << self + undef_method :method + end + end + + class SampleTarget + attr_reader :id + + def initialize(id) + @id = id + end + + def sample_method(parg1, parg2, *rest_args, **kwargs, &block) + { self: self, parg1: parg1, parg2: parg2, rest_args: rest_args, kwargs: kwargs, block: block } + end + end + end +end