diff --git a/NEWS.md b/NEWS.md index 570f16a7053d36..b8aabd0f3105ef 100644 --- a/NEWS.md +++ b/NEWS.md @@ -45,6 +45,10 @@ Note: We're only listing outstanding class updates. The class use equality semantic to lookup keys like a regular hash, but it doesn't hold strong references on the keys. [[Feature #18498]] +* Module + + * `Module#set_temporary_name` added for setting a temporary name for a module. [[Feature #19521]] + ## Stdlib updates The following default gems are updated. @@ -134,3 +138,4 @@ changelog for details of the default gems or bundled gems. [Feature #19347]: https://bugs.ruby-lang.org/issues/19347 [Feature #19538]: https://bugs.ruby-lang.org/issues/19538 [Feature #19591]: https://bugs.ruby-lang.org/issues/19591 +[Feature #19521]: https://bugs.ruby-lang.org/issues/19521 diff --git a/internal/variable.h b/internal/variable.h index 4436cd789cbec4..4fb7b23cf5b62b 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -36,6 +36,22 @@ static inline bool ROBJ_TRANSIENT_P(VALUE obj); static inline void ROBJ_TRANSIENT_SET(VALUE obj); static inline void ROBJ_TRANSIENT_UNSET(VALUE obj); +/** + * Sets the name of a module. + * + * Non-permanently named classes can have a temporary name assigned (or + * cleared). In that case the name will be used for `#inspect` and `#to_s`, and + * nested classes/modules will be named with the temporary name as a prefix. + * + * After the module is assigned to a constant, the temporary name will be + * discarded, and the name will be computed based on the nesting. + * + * @param[in] mod An instance of ::rb_cModule. + * @param[in] name An instance of ::rb_cString. + * @retval mod + */ +VALUE rb_mod_set_temporary_name(VALUE, VALUE); + struct gen_ivtbl; int rb_gen_ivtbl_get(VALUE obj, ID id, struct gen_ivtbl **ivtbl); int rb_obj_evacuate_ivs_to_hash_table(ID key, VALUE val, st_data_t arg); diff --git a/object.c b/object.c index 94e85ed3165a86..823b8df2cf6305 100644 --- a/object.c +++ b/object.c @@ -4508,6 +4508,7 @@ InitVM_Object(void) rb_define_method(rb_cModule, "included_modules", rb_mod_included_modules, 0); /* in class.c */ rb_define_method(rb_cModule, "include?", rb_mod_include_p, 1); /* in class.c */ rb_define_method(rb_cModule, "name", rb_mod_name, 0); /* in variable.c */ + rb_define_method(rb_cModule, "set_temporary_name", rb_mod_set_temporary_name, 1); /* in variable.c */ rb_define_method(rb_cModule, "ancestors", rb_mod_ancestors, 0); /* in class.c */ rb_define_method(rb_cModule, "attr", rb_mod_attr, -1); diff --git a/spec/ruby/core/module/name_spec.rb b/spec/ruby/core/module/name_spec.rb index b78bbfcc804f26..bbc18412393d02 100644 --- a/spec/ruby/core/module/name_spec.rb +++ b/spec/ruby/core/module/name_spec.rb @@ -6,6 +6,55 @@ Module.new.name.should be_nil end + ruby_version_is "3.3" do + it "can assign a temporary name" do + m = Module.new + m.name.should be_nil + + m.set_temporary_name("fake_name") + m.name.should == "fake_name" + + m.set_temporary_name(nil) + m.name.should be_nil + end + + it "can't assign empty string as name" do + m = Module.new + -> { m.set_temporary_name("") }.should raise_error(ArgumentError, "empty class/module name") + end + + it "can't assign a constant name as a temporary name" do + m = Module.new + -> { m.set_temporary_name("Object") }.should raise_error(ArgumentError, "name must not be valid constant name") + end + + it "can't assign name to permanent module" do + -> { Object.set_temporary_name("fake_name") }.should raise_error(RuntimeError, "can't change permanent name") + end + + it "can assign a temporary name to a nested module" do + m = Module.new + module m::N; end + m::N.name.should =~ /\A#::N\z/ + + m::N.set_temporary_name("fake_name") + m::N.name.should == "fake_name" + + m::N.set_temporary_name(nil) + m::N.name.should be_nil + end + + it "can update the name when assigned to a constant" do + m = Module.new + m::N = Module.new + m::N.name.should =~ /\A#::N\z/ + m::N.set_temporary_name(nil) + + m::M = m::N + m::M.name.should =~ /\A#::M\z/m + end + end + ruby_version_is ""..."3.0" do it "is nil when assigned to a constant in an anonymous module" do m = Module.new diff --git a/variable.c b/variable.c index ce6feda885b707..1e6367e3c51cb6 100644 --- a/variable.c +++ b/variable.c @@ -134,6 +134,79 @@ rb_mod_name(VALUE mod) return classname(mod, &permanent); } +/* + * call-seq: + * mod.set_temporary_name(string) -> self + * mod.set_temporary_name(nil) -> self + * + * Sets the temporary name of the module +mod+. This name is used as a prefix + * for the names of constants declared in +mod+. If the module is assigned a + * permanent name, the temporary name is discarded. + * + * After a permanent name is assigned, a temporary name can no longer be set, + * and this method raises a RuntimeError. + * + * If the name given is not a string or is a zero length string, this method + * raises an ArgumentError. + * + * The temporary name must not be a valid constant name, to avoid confusion + * with actual constants. If you attempt to set a temporary name that is a + * a valid constant name, this method raises an ArgumentError. + * + * If the given name is +nil+, the module becomes anonymous. + * + * Example: + * + * m = Module.new # => # + * m.name #=> nil + * + * m.set_temporary_name("fake_name") # => fake_name + * m.name #=> "fake_name" + * + * m.set_temporary_name(nil) # => # + * m.name #=> nil + * + * n = Module.new + * n.set_temporary_name("fake_name") + * + * n::M = m + * n::M.name #=> "fake_name::M" + * N = n + * + * N.name #=> "nested_fake_name" + * N::M.name #=> "N::M" + */ + +VALUE +rb_mod_set_temporary_name(VALUE mod, VALUE name) +{ + // We don't allow setting the name if the classpath is already permanent: + if (RCLASS_EXT(mod)->permanent_classpath) { + rb_raise(rb_eRuntimeError, "can't change permanent name"); + } + + if (NIL_P(name)) { + // Set the temporary classpath to NULL (anonymous): + RCLASS_SET_CLASSPATH(mod, 0, FALSE); + } else { + // Ensure the name is a string: + StringValue(name); + + if (RSTRING_LEN(name) == 0) { + rb_raise(rb_eArgError, "empty class/module name"); + } + + if (rb_is_const_name(name)) { + rb_raise(rb_eArgError, "name must not be valid constant name"); + } + + // Set the temporary classpath to the given name: + RCLASS_SET_CLASSPATH(mod, name, FALSE); + } + + return mod; +} + static VALUE make_temporary_path(VALUE obj, VALUE klass) {