diff options
author | Jeremy Evans <code@jeremyevans.net> | 2019-04-06 00:02:11 -0700 |
---|---|---|
committer | Jeremy Evans <code@jeremyevans.net> | 2019-05-26 11:09:21 -0700 |
commit | 39eadca76b48fc7841da688f6745e40897ec37ff (patch) | |
tree | 5c29107b29f5384eedb390af636186e7877369f3 | |
parent | 897901283c79e5f5f33656abdd453dc272268748 (diff) |
Add FrozenError#receiver
Similar to NameError#receiver, this returns the object on which
the modification was attempted. This is useful as it can pinpoint
exactly what is frozen. In many cases when a FrozenError is
raised, you cannot determine from the context which object is
frozen that you attempted to modify.
Users of the current rb_error_frozen C function will have to switch
to using rb_error_frozen_object or the new rb_frozen_error_raise
in order to set the receiver of the FrozenError.
To allow the receiver to be set from Ruby, support an optional
second argument to FrozenError#initialize.
Implements [Feature #15751]
-rw-r--r-- | NEWS | 9 | ||||
-rw-r--r-- | error.c | 51 | ||||
-rw-r--r-- | eval.c | 2 | ||||
-rw-r--r-- | include/ruby/intern.h | 1 | ||||
-rw-r--r-- | spec/ruby/core/exception/frozen_error_spec.rb | 34 | ||||
-rw-r--r-- | test/ruby/test_exception.rb | 27 | ||||
-rw-r--r-- | thread.c | 4 |
7 files changed, 121 insertions, 7 deletions
@@ -66,6 +66,15 @@ Enumerator:: can be directly passed to another method as a block argument. [Feature #15618] +FrozenError:: + + New method:: + + * Added FrozenError#receiver to return the frozen object that + modification was attempted on. To set this object when raising + FrozenError in Ruby code, pass it as the second argument to + FrozenError.new. + Integer:: Modified method:: @@ -1396,6 +1396,32 @@ exit_success_p(VALUE exc) return Qfalse; } +/* + * call-seq: + * FrozenError.new(msg=nil, receiver=nil) -> name_error + * + * Construct a new FrozenError exception. If given the <i>receiver</i> + * parameter may subsequently be examined using the FrozenError#receiver + * method. + * + * a = [].freeze + * raise FrozenError.new("can't modify frozen array", a) + */ + +static VALUE +frozen_err_initialize(int argc, VALUE *argv, VALUE self) +{ + VALUE mesg, recv; + + argc = rb_scan_args(argc, argv, "02", &mesg, &recv); + if (argc > 1) { + argc--; + rb_ivar_set(self, id_recv, recv); + } + rb_call_super(argc, argv); + return self; +} + void rb_name_error(ID id, const char *fmt, ...) { @@ -2483,6 +2509,8 @@ Init_Exception(void) rb_eRuntimeError = rb_define_class("RuntimeError", rb_eStandardError); rb_eFrozenError = rb_define_class("FrozenError", rb_eRuntimeError); + rb_define_method(rb_eFrozenError, "initialize", frozen_err_initialize, -1); + rb_define_method(rb_eFrozenError, "receiver", name_err_receiver, 0); rb_eSecurityError = rb_define_class("SecurityError", rb_eException); rb_eNoMemError = rb_define_class("NoMemoryError", rb_eException); rb_eEncodingError = rb_define_class("EncodingError", rb_eStandardError); @@ -2846,6 +2874,20 @@ rb_error_frozen(const char *what) } void +rb_frozen_error_raise(VALUE frozen_obj, const char *fmt, ...) +{ + va_list args; + VALUE exc, mesg; + + va_start(args, fmt); + mesg = rb_vsprintf(fmt, args); + va_end(args); + exc = rb_exc_new3(rb_eFrozenError, mesg); + rb_ivar_set(exc, id_recv, frozen_obj); + rb_exc_raise(exc); +} + +void rb_error_frozen_object(VALUE frozen_obj) { VALUE debug_info; @@ -2855,12 +2897,13 @@ rb_error_frozen_object(VALUE frozen_obj) VALUE path = rb_ary_entry(debug_info, 0); VALUE line = rb_ary_entry(debug_info, 1); - rb_raise(rb_eFrozenError, "can't modify frozen %"PRIsVALUE", created at %"PRIsVALUE":%"PRIsVALUE, - CLASS_OF(frozen_obj), path, line); + rb_frozen_error_raise(frozen_obj, + "can't modify frozen %"PRIsVALUE", created at %"PRIsVALUE":%"PRIsVALUE, + CLASS_OF(frozen_obj), path, line); } else { - rb_raise(rb_eFrozenError, "can't modify frozen %"PRIsVALUE, - CLASS_OF(frozen_obj)); + rb_frozen_error_raise(frozen_obj, "can't modify frozen %"PRIsVALUE, + CLASS_OF(frozen_obj)); } } @@ -454,7 +454,7 @@ rb_class_modify_check(VALUE klass) goto noclass; } } - rb_error_frozen(desc); + rb_frozen_error_raise(klass, "can't modify frozen %s", desc); } } diff --git a/include/ruby/intern.h b/include/ruby/intern.h index bcbf8424e4..01944fb0e8 100644 --- a/include/ruby/intern.h +++ b/include/ruby/intern.h @@ -278,6 +278,7 @@ PRINTF_ARGS(NORETURN(void rb_loaderror(const char*, ...)), 1, 2); PRINTF_ARGS(NORETURN(void rb_loaderror_with_path(VALUE path, const char*, ...)), 2, 3); PRINTF_ARGS(NORETURN(void rb_name_error(ID, const char*, ...)), 2, 3); PRINTF_ARGS(NORETURN(void rb_name_error_str(VALUE, const char*, ...)), 2, 3); +PRINTF_ARGS(NORETURN(void rb_frozen_error_raise(VALUE, const char*, ...)), 2, 3); NORETURN(void rb_invalid_str(const char*, const char*)); NORETURN(void rb_error_frozen(const char*)); NORETURN(void rb_error_frozen_object(VALUE)); diff --git a/spec/ruby/core/exception/frozen_error_spec.rb b/spec/ruby/core/exception/frozen_error_spec.rb new file mode 100644 index 0000000000..7b356253c4 --- /dev/null +++ b/spec/ruby/core/exception/frozen_error_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' + +describe "FrozenError" do + ruby_version_is "2.5" do + it "is a subclass of RuntimeError" do + RuntimeError.should be_ancestor_of(FrozenError) + end + end +end + +describe "FrozenError.new" do + ruby_version_is "2.7" do + it "should take optional receiver argument" do + o = Object.new + FrozenError.new("msg", o).receiver.should equal(o) + end + end +end + +describe "FrozenError#receiver" do + ruby_version_is "2.7" do + it "should return frozen object that modification was attempted on" do + o = Object.new.freeze + begin + def o.x; end + rescue => e + e.should be_kind_of(FrozenError) + e.receiver.should equal(o) + else + raise + end + end + end +end diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb index cabd20c0e8..9a0de5c430 100644 --- a/test/ruby/test_exception.rb +++ b/test/ruby/test_exception.rb @@ -853,6 +853,33 @@ end.join alias inspect pretty_inspect end + def test_frozen_error_receiver + obj = Object.new.freeze + (obj.foo = 1) rescue (e = $!) + assert_same(obj, e.receiver) + obj.singleton_class.const_set(:A, 2) rescue (e = $!) + assert_same(obj.singleton_class, e.receiver) + end + + def test_frozen_error_initialize + obj = Object.new + exc = FrozenError.new("bar", obj) + assert_equal("bar", exc.message) + assert_same(obj, exc.receiver) + + exc = FrozenError.new("bar") + assert_equal("bar", exc.message) + assert_raise_with_message(ArgumentError, "no receiver is available") { + exc.receiver + } + + exc = FrozenError.new + assert_equal("FrozenError", exc.message) + assert_raise_with_message(ArgumentError, "no receiver is available") { + exc.receiver + } + end + def test_name_error_new_default error = NameError.new assert_equal("NameError", error.message) @@ -3325,7 +3325,7 @@ VALUE rb_thread_local_aset(VALUE thread, ID id, VALUE val) { if (OBJ_FROZEN(thread)) { - rb_error_frozen("thread locals"); + rb_frozen_error_raise(thread, "can't modify frozen thread locals"); } return threadptr_local_aset(rb_thread_ptr(thread), id, val); @@ -3402,7 +3402,7 @@ rb_thread_variable_set(VALUE thread, VALUE id, VALUE val) VALUE locals; if (OBJ_FROZEN(thread)) { - rb_error_frozen("thread locals"); + rb_frozen_error_raise(thread, "can't modify frozen thread locals"); } locals = rb_ivar_get(thread, id_locals); |