Bug #21193
openInherited callback returns `nil` for `Object.const_source_location`
Description
Since https://github.com/ruby/ruby/pull/12927 the inherited callback no longer can access the Object.const_source_location
. A simplified reproduction is the following:
class A
def self.inherited(other)
super
p Object.const_source_location(other.name)
end
end
class B < A
end
Prior to this change Object.const_source_location(other.name)
would return ["test.rb", 8]
and now returns nil
.
Shopify's application uses the inherited
callback to load configuration classes and since it's unable to find these classes they aren't loaded when they are called. It's not clear if this was intended or if there's a workaround. In chatting with folks at GitHub, they have a similar issue (although that case sounds less straightforward). A more accurate example of what we have (I did strip this down a bunch too):
def self.inherited(config)
return super unless config.superclass == Config
if (class_name = config.name) &&
(config_location = Object.const_source_location(class_name)) &&
(parent_dirname = File.dirname(config_location[0]))
config_dirname = File.join(parent_dirname, "config")
config.autoload(:Something, File.join(config_dirname, "something.rb"))
end
super
end
Original issue: https://bugs.ruby-lang.org/issues/21143
cc/ @byroot (Jean Boussier) @fxn
Updated by byroot (Jean Boussier) 5 days ago
- Assignee set to byroot (Jean Boussier)
Updated by fxn (Xavier Noria) 5 days ago
· Edited
Eileen, I proposed to document the previous ordering (ruby#12759), but there was some speculative discussion in the associated ticket in Redmine (https://bugs.ruby-lang.org/issues/21143).
Could not provide a real use case for a dependency on the existing order, but I explained it could theoretically happen. Maybe this helps taking a final decision.
Updated by Eregon (Benoit Daloze) 5 days ago
- Related to Misc #21143: Speficy order of execution const_added vs inherited added
Updated by byroot (Jean Boussier) 5 days ago
I tried to manage both considerations in https://github.com/ruby/ruby/pull/12956.
Updated by Eregon (Benoit Daloze) 5 days ago
Probably worth trying:
- set the superclass, without calling
inherited
- set the constant (which also names the module/class), without triggering
const_added
inherited
const_added
If that's not compatible enough, then we'd have to go back to before #21143, which is the same except 3 and 4 are swapped.
Updated by Eregon (Benoit Daloze) 5 days ago
That's what your PR does, I reviewed it, LGTM.
Updated by byroot (Jean Boussier) 5 days ago
- Status changed from Open to Closed
Applied in changeset git|de097fbe5f3df105bd2a26e72db06b0f5139bc1a.
Trigger inherited
and const_set
callbacks after const has been defined
The previous change caused a backward compatibility issue with code
that called Object.const_source_location
from the inherited
callback.
To fix this, the order is now:
- Define the constant
- Invoke
inherited
- Invoke
const_set
Updated by mame (Yusuke Endoh) 5 days ago
- Status changed from Closed to Assigned
- Assignee changed from byroot (Jean Boussier) to matz (Yukihiro Matsumoto)
Hmmm, I think this handling is dirty. @matz (Yukihiro Matsumoto) said in #21143
But I don't want to break any existing code. I'd like to experiment inherited-then-const_added order works (or not).
If it works, I'd like to change the order. If it doesn't I'like to keep the current order (and document somewhere).
We actually found the real-world examples that are affected by the change. This means the change breaks existing code. I think we should revert the change and keep the previous order, instead of introducing dirty special handling.
At the least, we should ask matz to reconfirm this change before putting it in. @matz (Yukihiro Matsumoto) what do you think?
Updated by Eregon (Benoit Daloze) 5 days ago
mame (Yusuke Endoh) wrote in #note-8:
I think this handling is dirty
I think it's fine and not dirty, based on https://bugs.ruby-lang.org/issues/21193#note-5 it's clear that Ruby has always set the superclass separately from calling inherited
.
So doing the same for setting the constant, i.e. setting it separately from calling const_added
is not different.
I think the current state is the best: set the state internally (both superclass and constant) so when both hooks are called it's fully setup as expected and there is no "half-initialized state".
Updated by fxn (Xavier Noria) 5 days ago
· Edited
Agree with @mame (Yusuke Endoh).
And, looking at that code from Shopify that showed up in nothing, I can imagine some other application loading with Zeitwerk and doing something like this
class AbstractConnection
def self.inherited(base)
base::Manager.configure # ConcreteConnection::Manager is autoloaded here.
end
end
That works today, would not work with the new order.
Updated by fxn (Xavier Noria) 5 days ago
To clarify, when I said that I agree with @mame (Yusuke Endoh) I am personally not qualifying the current approach. Just saying the patches took a velocity and direction that does not seem to be aligned with what @matz (Yukihiro Matsumoto) said.
Updated by fxn (Xavier Noria) 5 days ago
I think the current state is the best: set the state internally (both superclass and constant) so when both hooks are called it's fully setup as expected and there is no "half-initialized state".
Let me stress this was already the case in the previous order, as I explained in the original ticket.
Updated by mame (Yusuke Endoh) 5 days ago
It was understandable that you wanted class C < D
to behave the same as C = Class.new(D)
for the sake of consistency.
However, the change proposed in this ticket intentionally differentiates these two behaviors, which seems to spoil the original motivation.
If we cannot make it consistent eventually, it is unclear to me why this order change was necessary.
If @matz (Yukihiro Matsumoto) prefers to change the order even in this halfway state, I don't mind, but we need to confirm.