From 3cffeaf06b7b05e2c4a13804f64dcec9372e02f5 Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@jeremyevans.net>
Date: Sat, 6 Apr 2019 00:02:11 -0700
Subject: [PATCH] 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.

I'm not sure that using name_err_receiver as the C function for
FrozenError#receiver is acceptable, but it doesn't appear to cause
problems.

Users of the current rb_error_frozen C function will have to switch
to using rb_error_frozen_object or the new rb_raise_frozen_error
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.
---
 error.c                     | 51 ++++++++++++++++++++++++++++++++++---
 eval.c                      |  2 +-
 include/ruby/intern.h       |  1 +
 test/ruby/test_exception.rb | 27 ++++++++++++++++++++
 thread.c                    |  4 +--
 5 files changed, 78 insertions(+), 7 deletions(-)

diff --git a/error.c b/error.c
index e61a3844cf..4edc10a599 100644
--- a/error.c
+++ b/error.c
@@ -1395,6 +1395,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, ...)
 {
@@ -2482,6 +2508,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);
@@ -2843,6 +2871,20 @@ rb_error_frozen(const char *what)
     rb_raise(rb_eFrozenError, "can't modify frozen %s", 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)
 {
@@ -2853,12 +2895,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 59b3b30293..32ff15a206 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 17aafd7f8e..6a27d42ab3 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/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 @@ def pretty_inspect; "`obj'"; end
       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 213a3e5fb7..c6caf2cc5b 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);
-- 
2.21.0

