From c0f5c3aa3b0d404bc781a6fa159dbfd7a6f5df9f Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Wed, 6 Jun 2018 11:23:07 -0700 Subject: [PATCH] Make public access of a private constant call const_missing Calling obj.foo where foo is a private method of obj calls method_missing. You would expect klass::FOO where FOO is a private constant of klass to call const_missing, but currently it doesn't. This makes a small change so that const_missing will be called in such cases. In addition to similarity to method_missing, the main reason for doing this is it offers a way to deprecate public constants. Currently, if you have a public constant and want to make it a private constant, you can't do it without breaking possible callers. With this patch, you can make it a private constant, then override const_missing in the class, and have const_missing print a deprecation warning and then return the value of the constant. --- test/ruby/test_module.rb | 36 ++++++++++++++++++++++++++++++++++++ variable.c | 11 +++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index 570241a683..b038b13d78 100644 --- a/test/ruby/test_module.rb +++ b/test/ruby/test_module.rb @@ -1388,6 +1388,42 @@ class X RUBY end + def test_private_constant_const_missing + c = Class.new + c.const_set(:FOO, "foo") + c.private_constant(:FOO) + class << c + attr_reader :const_missing_arg + def const_missing(name) + @const_missing_arg = name + name == :FOO ? const_get(:FOO) : super + end + end + assert_equal("foo", c::FOO) + assert_equal(:FOO, c.const_missing_arg) + assert_raise(NameError) { c::BAR } + end + + def test_private_constant_nested_const_missing + c = Class.new + c.const_set(:FOO, "foo") + c.private_constant(:FOO) + sc = Class.new(c) + class << sc + attr_reader :const_missing_arg + attr_reader :const_missing_self + def const_missing(name) + @const_missing_arg = name + @const_missing_self = self + name == :FOO ? const_get(:FOO) : super + end + end + assert_equal("foo", sc::FOO) + assert_equal(:FOO, sc.const_missing_arg) + assert_equal(sc, sc.const_missing_self) + assert_raise(NameError) { sc::BAR } + end + class PrivateClass end private_constant :PrivateClass diff --git a/variable.c b/variable.c index cdc9efe954..589612e986 100644 --- a/variable.c +++ b/variable.c @@ -1796,6 +1796,9 @@ VALUE rb_mod_const_missing(VALUE klass, VALUE name) { rb_vm_pop_cfunc_frame(); + if (RB_TYPE_P(name, RUBY_T_SYMBOL)) { + rb_const_search(klass, SYM2ID(name), 0, 1, 2); + } uninitialized_constant(klass, name); UNREACHABLE; @@ -2362,8 +2365,12 @@ rb_const_search(VALUE klass, ID id, int exclude, int recurse, int visibility) while ((ce = rb_const_lookup(tmp, id))) { if (visibility && RB_CONST_PRIVATE_P(ce)) { - rb_name_err_raise("private constant %2$s::%1$s referenced", - tmp, ID2SYM(id)); + if (visibility == 2) { + rb_name_err_raise("private constant %2$s::%1$s referenced", + tmp, ID2SYM(id)); + } else { + return rb_const_missing(klass, ID2SYM(id)); + } } rb_const_warn_if_deprecated(ce, tmp, id); value = ce->value; -- 2.16.2