Project

General

Profile

Actions

Bug #21316

open

Namespaces leak with permanent names

Added by fxn (Xavier Noria) 23 days ago. Updated 10 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:121950]

Description

Namespaces are not transparent for this program

C = Class.new
C.name == 'C'

because under a non-main user namespace, the name of C has the namespace as a prefix.


Related issues 1 (1 open0 closed)

Related to Ruby - Feature #21335: Namespaces should be present in the backtraceOpenActions

Updated by mame (Yusuke Endoh) 23 days ago

This is definitely not ideal. Class#name could end up referring to a different constant.

# main.rb
NS = Namespace.new
NS.require "./sub"

# sub.rb
class C; end
p C #=> expected: C
    #=> actual: NS::C ← This could refer to a different constant, which is problematic

@tagomoris (Satoshi Tagomori) suggested that Class#name should behave as follows:

  • If the current namespace is at the front, omit it and just return "C"
  • Otherwise, return something like "#<Namespace: ...>::C"
# main.rb
NS = Namespace.new
NS.require "./sub"
p NS::C #=> #<Namespace: ...>::C

# sub.rb
class C; end
p C #=> C

Updated by fxn (Xavier Noria) 23 days ago · Edited

Main problem here is that there are many programs that depend on that name.

They may store it somewhere for later const_get. For example, polymorphic Active Record associations store names in the database. Users may also set class and module names in configuration, to be later lazily loaded. Associations do this to specify the name of the target model. Active Job queue adapters are configured this way too, etc.

Also, the other way around. You may pass the name to a callback, and when the program detects a class or module object with that name is created (by looking at Module#name), the callback is invoked. This is the case in on_load callbacks in Zeitwerk, for instance.

That the name matches the constant path of the class or module being defined is a strong property of the language people rely on.

Updated by Eregon (Benoit Daloze) 22 days ago

Right I think in the namespace defining the class/module the Module#name needs to not have a prefix, or it will break many gems.
OTOH when used outside, it could be very confusing without a prefix.

suggested that Class#name should behave as follows:

Yeah, that's probably the best trade-off, although of course it means the Module#name for a given Module changes based on which Namespace is the current one.


BTW, I think the main namespace constants should also be prefixed when seen in another namespace, currently it's not the case:

$ RUBY_NAMESPACE=1 ruby -ve 'main = Namespace.current; ns = Namespace.new; class C; end; ns::MAIN_C = C; File.write "ns.rb", "p MAIN_C; p eval(MAIN_C.name)"; ns.require "./ns"'
ruby 3.5.0dev (2025-05-10T07:50:29Z namespace-on-read-.. bd4f57f96b) +PRISM [x86_64-linux]
ruby: warning: Namespace is experimental, and the behavior may change in the future!
See doc/namespace.md for know issues, etc.
C
(eval at /home/eregon/ns.rb:1):1:in '<top (required)>': uninitialized constant #<Namespace:24,user,optional>::C (NameError)
	from /home/eregon/ns.rb:1:in 'Kernel#eval'
	from /home/eregon/ns.rb:1:in '<top (required)>'
	from -e:1:in 'Namespace#require'
	from -e:1:in '<main>'

Updated by make_now_just (Hiroya Fujinami) 19 days ago · Edited

I found an issue on Marshal and Namespace maybe related to this ticket.

When dumping an object defined in a namespace using Marshal, the result will vary depending on whether the namespace is held as a variable or constant, and whether Marshal.dump is performed inside or outside the namespace.

In other words, we have the following files:

ns.rb:

class Foo
  def dump_in_ns = Marshal.dump(self)
end

main.rb:

ns = Namespace.new
ns.require("./ns.rb")

NS = Namespace.new
NS.require("./ns.rb")

puts "var / in ns"
begin
  ns::Foo.new.dump_in_ns
  p :ok
rescue => ex
  p :error
  p ex
end
puts

puts "const / in ns"
begin
  NS::Foo.new.dump_in_ns
  p :ok
rescue => ex
  p :error
  p ex
end
puts

puts "var / out ns"
begin
  Marshal.dump(ns::Foo.new)
  p :ok
rescue => ex
  p :error
  p ex
end
puts

puts "const / out ns"
begin
  Marshal.dump(NS::Foo.new)
  p :ok
rescue => ex
  p :error
  p ex
end

Then, the result is as follows:

$ RUBY_NAMESPACE=1 ruby main.rb
var / in ns
:error
#<TypeError: can't dump anonymous class #<Namespace:0x000000011bf3ed98>::Foo>

const / in ns
:error
#<ArgumentError: undefined class/module NS::>

var / out ns
:error
#<TypeError: can't dump anonymous class #<Namespace:0x000000011bf3ed98>::Foo>

const / out ns
:ok

I'm not sure this issue is completely related to this ticket, but Marshal.dump should work in any case.

Updated by Eregon (Benoit Daloze) 19 days ago

@make_now_just (Hiroya Fujinami) In the first two cases, it should be .dump_in_ns not .dump

Updated by make_now_just (Hiroya Fujinami) 19 days ago

@Eregon (Benoit Daloze) It has already been fixed. Thank you.

Actions #7

Updated by mame (Yusuke Endoh) 19 days ago

  • Related to Feature #21335: Namespaces should be present in the backtrace added

Updated by fxn (Xavier Noria) 10 days ago · Edited

@ko1 (Koichi Sasada) yeah, in Ruby you can have two classes with the same permanent name today. You know that, but let me show an example for the archives:

c = Class.new
C = c

Object.send(:remove_const, :C)

d = Class.new
C = d

p c.name == d.name # true

So, where possible, in arbitrary settings, you better work with class objects.

But there are common scenarios (configuration, etc., I described a few above) in which you do not have the object and the natural way to refer to the class is by its name.

Today, that is a common practice, and if you want to be able to load arbitrary code within a namespace, I think this has to be preserved. Otherwise, such code won't work correctly under a namespace.

Actions

Also available in: Atom PDF

Like2
Like1Like0Like0Like0Like0Like0Like0Like0Like0