aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2019-04-06 00:02:11 -0700
committerJeremy Evans <code@jeremyevans.net>2019-05-26 11:09:21 -0700
commit39eadca76b48fc7841da688f6745e40897ec37ff (patch)
tree5c29107b29f5384eedb390af636186e7877369f3
parent897901283c79e5f5f33656abdd453dc272268748 (diff)
downloadruby-39eadca76b48fc7841da688f6745e40897ec37ff.tar.gz
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--NEWS9
-rw-r--r--error.c51
-rw-r--r--eval.c2
-rw-r--r--include/ruby/intern.h1
-rw-r--r--spec/ruby/core/exception/frozen_error_spec.rb34
-rw-r--r--test/ruby/test_exception.rb27
-rw-r--r--thread.c4
7 files changed, 121 insertions, 7 deletions
diff --git a/NEWS b/NEWS
index 7e9450b9dd..89ed64ce4b 100644
--- a/NEWS
+++ b/NEWS
@@ -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::
diff --git a/error.c b/error.c
index 403516a683..039999cf6a 100644
--- a/error.c
+++ b/error.c
@@ -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));
}
}
diff --git a/eval.c b/eval.c
index 48473c79e2..e778d074dd 100644
--- a/eval.c
+++ b/eval.c
@@ -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)
diff --git a/thread.c b/thread.c
index e88ed87306..d4f33a2e42 100644
--- a/thread.c
+++ b/thread.c
@@ -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);