diff --git a/NEWS b/NEWS index d4ada857fa..717ae69b65 100644 --- a/NEWS +++ b/NEWS @@ -109,6 +109,11 @@ with all sufficient information, see the ChangeLog file or Redmine * Description set by Thread#name= is now visible on Windows 10. +* KeyError + + * KeyError#receiver [Feature #12063] + * KeyError#key [Feature #12063] + === Stdlib updates (outstanding ones only) * Bundler diff --git a/error.c b/error.c index a12c5d206d..ffdd1a4ef1 100644 --- a/error.c +++ b/error.c @@ -815,7 +815,7 @@ VALUE rb_mErrno; static VALUE rb_eNOERROR; static ID id_new, id_cause, id_message, id_backtrace; -static ID id_name, id_args, id_Errno, id_errno, id_i_path; +static ID id_name, id_key, id_args, id_Errno, id_errno, id_i_path; static ID id_receiver, id_iseq, id_local_variables; static ID id_private_call_p; extern ID ruby_static_id_status; @@ -1547,6 +1547,37 @@ rb_invalid_str(const char *str, const char *type) rb_raise(rb_eArgError, "invalid value for %s: %+"PRIsVALUE, type, s); } +static VALUE +key_err_receiver(VALUE self) +{ + VALUE recv; + + recv = rb_ivar_lookup(self, id_receiver, Qundef); + if (recv != Qundef) return recv; + rb_raise(rb_eArgError, "no receiver is available"); +} + +static VALUE +key_err_key(VALUE self) +{ + VALUE key; + + key = rb_ivar_lookup(self, id_key, Qundef); + if (key != Qundef) return key; + rb_raise(rb_eArgError, "no key is available"); +} + +VALUE +rb_key_err_new(VALUE mesg, VALUE recv, VALUE key) +{ + VALUE exc = rb_obj_alloc(rb_eKeyError); + rb_ivar_set(exc, id_mesg, mesg); + rb_ivar_set(exc, id_bt, Qnil); + rb_ivar_set(exc, id_key, key); + rb_ivar_set(exc, id_receiver, recv); + return exc; +} + /* * call-seq: * SyntaxError.new([msg]) -> syntax_error @@ -2161,6 +2192,8 @@ Init_Exception(void) rb_eArgError = rb_define_class("ArgumentError", rb_eStandardError); rb_eIndexError = rb_define_class("IndexError", rb_eStandardError); rb_eKeyError = rb_define_class("KeyError", rb_eIndexError); + rb_define_method(rb_eKeyError, "receiver", key_err_receiver, 0); + rb_define_method(rb_eKeyError, "key", key_err_key, 0); rb_eRangeError = rb_define_class("RangeError", rb_eStandardError); rb_eScriptError = rb_define_class("ScriptError", rb_eException); @@ -2216,6 +2249,7 @@ Init_Exception(void) id_message = rb_intern_const("message"); id_backtrace = rb_intern_const("backtrace"); id_name = rb_intern_const("name"); + id_key = rb_intern_const("key"); id_args = rb_intern_const("args"); id_receiver = rb_intern_const("receiver"); id_private_call_p = rb_intern_const("private_call?"); diff --git a/hash.c b/hash.c index bc02696b30..7198e8a9d8 100644 --- a/hash.c +++ b/hash.c @@ -912,7 +912,7 @@ rb_hash_fetch_m(int argc, VALUE *argv, VALUE hash) desc = rb_any_to_s(key); } desc = rb_str_ellipsize(desc, 65); - rb_raise(rb_eKeyError, "key not found: %"PRIsVALUE, desc); + rb_key_err_raise(rb_sprintf("key not found: %"PRIsVALUE, desc), hash, key); } return argv[1]; } @@ -3375,7 +3375,7 @@ env_fetch(int argc, VALUE *argv) if (!env) { if (block_given) return rb_yield(key); if (argc == 1) { - rb_raise(rb_eKeyError, "key not found: \"%"PRIsVALUE"\"", key); + rb_key_err_raise(rb_sprintf("key not found: \"%"PRIsVALUE"\"", key), envtbl, key); } return argv[1]; } diff --git a/internal.h b/internal.h index 3e6b30a235..3ff731c2de 100644 --- a/internal.h +++ b/internal.h @@ -1156,6 +1156,9 @@ VALUE rb_name_err_new(VALUE mesg, VALUE recv, VALUE method); rb_exc_raise(rb_name_err_new(mesg, recv, name)) #define rb_name_err_raise(mesg, recv, name) \ rb_name_err_raise_str(rb_fstring_cstr(mesg), (recv), (name)) +VALUE rb_key_err_new(VALUE mesg, VALUE recv, VALUE name); +#define rb_key_err_raise(mesg, recv, name) \ + rb_exc_raise(rb_key_err_new(mesg, recv, name)) NORETURN(void ruby_deprecated_internal_feature(const char *)); #define DEPRECATED_INTERNAL_FEATURE(func) \ (ruby_deprecated_internal_feature(func), UNREACHABLE) diff --git a/sprintf.c b/sprintf.c index 2bd4966a1b..a956381cd4 100644 --- a/sprintf.c +++ b/sprintf.c @@ -633,7 +633,7 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) } nextvalue = rb_hash_default_value(hash, sym); if (NIL_P(nextvalue)) { - rb_enc_raise(enc, rb_eKeyError, "key%.*s not found", len, start); + rb_key_err_raise(rb_enc_sprintf(enc, "key%.*s not found", len, start), hash, sym); } } if (term == '}') goto format_s; diff --git a/test/ruby/test_env.rb b/test/ruby/test_env.rb index 29c058339f..d136ba75fa 100644 --- a/test/ruby/test_env.rb +++ b/test/ruby/test_env.rb @@ -122,9 +122,11 @@ class TestEnv < Test::Unit::TestCase assert_equal("foo", ENV.fetch("test")) ENV.delete("test") feature8649 = '[ruby-core:56062] [Feature #8649]' - assert_raise_with_message(KeyError, 'key not found: "test"', feature8649) do + e = assert_raise_with_message(KeyError, 'key not found: "test"', feature8649) do ENV.fetch("test") end + assert_same(ENV, e.receiver) + assert_equal("test", e.key) assert_equal("foo", ENV.fetch("test", "foo")) assert_equal("bar", ENV.fetch("test") { "bar" }) assert_equal("bar", ENV.fetch("test", "foo") { "bar" }) diff --git a/test/ruby/test_hash.rb b/test/ruby/test_hash.rb index 8fb1c17003..ebc622a827 100644 --- a/test/ruby/test_hash.rb +++ b/test/ruby/test_hash.rb @@ -531,6 +531,8 @@ class TestHash < Test::Unit::TestCase e = assert_raise(KeyError) { @h.fetch('gumby'*20) } assert_match(/key not found: "gumbygumby/, e.message) assert_match(/\.\.\.\z/, e.message) + assert_same(@h, e.receiver) + assert_equal('gumby'*20, e.key) end def test_key2? @@ -591,9 +593,11 @@ class TestHash < Test::Unit::TestCase assert_equal(4, res.length) assert_equal %w( three two one nil ), res - assert_raise KeyError do + e = assert_raise KeyError do @h.fetch_values(3, 'invalid') end + assert_same(@h, e.receiver) + assert_equal('invalid', e.key) res = @h.fetch_values(3, 'invalid') { |k| k.upcase } assert_equal %w( three INVALID ), res diff --git a/test/ruby/test_sprintf.rb b/test/ruby/test_sprintf.rb index b46f5189e2..a07ac7908b 100644 --- a/test/ruby/test_sprintf.rb +++ b/test/ruby/test_sprintf.rb @@ -452,7 +452,10 @@ class TestSprintf < Test::Unit::TestCase assert_raise_with_message(ArgumentError, "named after numbered") {sprintf("%1$s", :key => "value")} assert_raise_with_message(ArgumentError, "named after unnumbered(2)") {sprintf("%s%s%s", "foo", "bar", :key => "value")} assert_raise_with_message(ArgumentError, "named after ") {sprintf("%s", :key => "value")} - assert_raise_with_message(KeyError, "key not found") {sprintf("%s", {})} + h = {} + e = assert_raise_with_message(KeyError, "key not found") {sprintf("%s", h)} + assert_same(h, e.receiver) + assert_equal(:key, e.key) end def test_named_untyped_enc