diff --git a/NEWS.md b/NEWS.md index 849188a8f1..6de6d97e8b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,6 +29,7 @@ Note: We're only listing outstanding class updates. * Module * Module.used_refinements has been added. [[Feature #14332]] * Module#refinements has been added. [[Feature #12737]] + * Module#const_added has been added. [[Feature #17881]] * Proc * Proc#dup returns an instance of subclass. [[Bug #17545]] diff --git a/defs/id.def b/defs/id.def index 8df6cf12e2..097e34e405 100644 --- a/defs/id.def +++ b/defs/id.def @@ -7,6 +7,7 @@ firstline, predefined = __LINE__+1, %[\ inspect intern object_id + const_added const_missing method_missing MethodMissing method_added diff --git a/object.c b/object.c index 9243df5587..ef8a855dfb 100644 --- a/object.c +++ b/object.c @@ -1005,6 +1005,28 @@ rb_class_search_ancestor(VALUE cl, VALUE c) */ #define rb_obj_singleton_method_undefined rb_obj_dummy1 +/* Document-method: const_added + * + * call-seq: + * const_added(const_name) + * + * Invoked as a callback whenever a constant is assigned on the receiver + * + * module Chatty + * def self.const_added(const_name) + * super + * puts "Added #{const_name.inspect}" + * end + * FOO = 1 + * end + * + * produces: + * + * Added :FOO + * + */ +#define rb_obj_mod_const_added rb_obj_dummy1 + /* * Document-method: extended * @@ -4419,6 +4441,7 @@ InitVM_Object(void) rb_define_private_method(rb_cModule, "extended", rb_obj_mod_extended, 1); rb_define_private_method(rb_cModule, "prepended", rb_obj_mod_prepended, 1); rb_define_private_method(rb_cModule, "method_added", rb_obj_mod_method_added, 1); + rb_define_private_method(rb_cModule, "const_added", rb_obj_mod_const_added, 1); rb_define_private_method(rb_cModule, "method_removed", rb_obj_mod_method_removed, 1); rb_define_private_method(rb_cModule, "method_undefined", rb_obj_mod_method_undefined, 1); diff --git a/spec/ruby/core/module/const_added_spec.rb b/spec/ruby/core/module/const_added_spec.rb new file mode 100644 index 0000000000..ff2ee0987f --- /dev/null +++ b/spec/ruby/core/module/const_added_spec.rb @@ -0,0 +1,125 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#const_added" do + ruby_version_is "3.1" do + it "is a private instance method" do + Module.should have_private_instance_method(:const_added) + end + + it "returns nil in the default implementation" do + Module.new do + const_added(:TEST).should == nil + end + end + + it "is called when a new constant is assigned on self" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + TEST = 1 + RUBY + + ScratchPad.recorded.should == [:TEST] + end + + it "is called when a new constant is assigned on self throught const_set" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.const_set(:TEST, 1) + + ScratchPad.recorded.should == [:TEST] + end + + it "is called when a new module is defined under self" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + module SubModule + end + + module SubModule + end + RUBY + + ScratchPad.recorded.should == [:SubModule] + end + + it "is called when a new class is defined under self" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + class SubClass + end + + class SubClass + end + RUBY + + ScratchPad.recorded.should == [:SubClass] + end + + it "is called when an autoload is defined" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.autoload :Autoload, "foo" + ScratchPad.recorded.should == [:Autoload] + end + + it "is called with a precise caller location with the line of definition" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + location = caller_locations(1, 1)[0] + ScratchPad << location.lineno + end + end + + line = __LINE__ + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + TEST = 1 + + module SubModule + end + + class SubClass + end + RUBY + + mod.const_set(:CONST_SET, 1) + + ScratchPad.recorded.should == [line + 2, line + 4, line + 7, line + 11] + end + end +end diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index 9368940050..0a6959c5e6 100644 --- a/test/ruby/test_module.rb +++ b/test/ruby/test_module.rb @@ -1675,6 +1675,45 @@ class TestModule < Test::Unit::TestCase assert_match(/::X\u{df}:/, c.new.to_s) end + + def test_const_added + eval(<<~RUBY) + module TestConstAdded + @memo = [] + class << self + attr_accessor :memo + + def const_added(sym) + memo << sym + end + end + CONST = 1 + module SubModule + end + + class SubClass + end + end + TestConstAdded::OUTSIDE_CONST = 2 + module TestConstAdded::OutsideSubModule; end + class TestConstAdded::OutsideSubClass; end + RUBY + TestConstAdded.const_set(:CONST_SET, 3) + assert_equal [ + :CONST, + :SubModule, + :SubClass, + :OUTSIDE_CONST, + :OutsideSubModule, + :OutsideSubClass, + :CONST_SET, + ], TestConstAdded.memo + ensure + if self.class.const_defined? :TestConstAdded + self.class.send(:remove_const, :TestConstAdded) + end + end + def test_method_added memo = [] mod = Module.new do diff --git a/test/ruby/test_settracefunc.rb b/test/ruby/test_settracefunc.rb index e973e3a384..0524c35873 100644 --- a/test/ruby/test_settracefunc.rb +++ b/test/ruby/test_settracefunc.rb @@ -108,6 +108,10 @@ class TestSetTraceFunc < Test::Unit::TestCase events.shift) assert_equal(["line", 4, __method__, self.class], events.shift) + assert_equal(["c-call", 4, :const_added, Module], + events.shift) + assert_equal(["c-return", 4, :const_added, Module], + events.shift) assert_equal(["c-call", 4, :inherited, Class], events.shift) assert_equal(["c-return", 4, :inherited, Class], @@ -345,6 +349,8 @@ class TestSetTraceFunc < Test::Unit::TestCase [["c-return", 2, :add_trace_func, Thread], ["line", 3, __method__, self.class], + ["c-call", 3, :const_added, Module], + ["c-return", 3, :const_added, Module], ["c-call", 3, :inherited, Class], ["c-return", 3, :inherited, Class], ["class", 3, nil, nil], @@ -487,6 +493,8 @@ class TestSetTraceFunc < Test::Unit::TestCase [:line, 5, 'xyzzy', self.class, method, self, :inner, :nothing], [:c_return, 4, "xyzzy", Integer, :times, 1, :outer, 1], [:line, 7, 'xyzzy', self.class, method, self, :outer, :nothing], + [:c_call, 7, "xyzzy", Module, :const_added, TestSetTraceFunc, :outer, :nothing], + [:c_return, 7, "xyzzy", Module, :const_added, TestSetTraceFunc, :outer, nil], [:c_call, 7, "xyzzy", Class, :inherited, Object, :outer, :nothing], [:c_return, 7, "xyzzy", Class, :inherited, Object, :outer, nil], [:class, 7, "xyzzy", nil, nil, xyzzy.class, nil, :nothing], @@ -620,6 +628,8 @@ CODE [:line, 7, 'xyzzy', self.class, method, self, :outer, :nothing], [:c_call, 7, "xyzzy", Class, :inherited, Object, :outer, :nothing], [:c_return, 7, "xyzzy", Class, :inherited, Object, :outer, nil], + [:c_call, 7, "xyzzy", Class, :const_added, Object, :outer, :nothing], + [:c_return, 7, "xyzzy", Class, :const_added, Object, :outer, nil], [:class, 7, "xyzzy", nil, nil, xyzzy.class, nil, :nothing], [:line, 8, "xyzzy", nil, nil, xyzzy.class, nil, :nothing], [:line, 9, "xyzzy", nil, nil, xyzzy.class, :XYZZY_outer, :nothing], diff --git a/variable.c b/variable.c index eaad6eb497..ebfbf017c1 100644 --- a/variable.c +++ b/variable.c @@ -3102,6 +3102,15 @@ set_namespace_path(VALUE named_namespace, VALUE namespace_path) RB_VM_LOCK_LEAVE(); } +static void +const_added(VALUE klass, ID const_name) +{ + if (GET_VM()->running) { + VALUE name = ID2SYM(const_name); + rb_funcallv(klass, idConst_added, 1, &name); + } +} + void rb_const_set(VALUE klass, ID id, VALUE val) { @@ -3166,6 +3175,7 @@ rb_const_set(VALUE klass, ID id, VALUE val) } } } + const_added(klass, id); } static struct autoload_data_i *