Skip to content

Commit

Permalink
Allow setting the name of a class or module. (#7483)
Browse files Browse the repository at this point in the history
Introduce `Module#set_temporary_name` for setting identifiers for otherwise
anonymous modules/classes.
  • Loading branch information
ioquatix committed Jun 21, 2023
1 parent e25403d commit a87bce8
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 0 deletions.
5 changes: 5 additions & 0 deletions NEWS.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions internal/variable.h
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions object.c
Expand Up @@ -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);
Expand Down
49 changes: 49 additions & 0 deletions spec/ruby/core/module/name_spec.rb
Expand Up @@ -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#<Module:0x\h+>::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#<Module:0x\h+>::N\z/
m::N.set_temporary_name(nil)

m::M = m::N
m::M.name.should =~ /\A#<Module:0x\h+>::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
Expand Down
73 changes: 73 additions & 0 deletions variable.c
Expand Up @@ -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 # => #<Module:0x0000000102c68f38>
* m.name #=> nil
*
* m.set_temporary_name("fake_name") # => fake_name
* m.name #=> "fake_name"
*
* m.set_temporary_name(nil) # => #<Module:0x0000000102c68f38>
* 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)
{
Expand Down

0 comments on commit a87bce8

Please sign in to comment.