Bug #22276
openalias in a module falls back to Object even in classes not inheriting from Object
Description
When alias (or alias_method) is used in a module and the method is not found in the module, alias searches the method from Object. So a module can alias a method of Kernel or Object, and the alias works even if the module is later included in a class that does not have Object and Kernel as ancestors:
module M
alias foo puts
public :foo
end
class X < BasicObject
include ::M
end
X.new.foo("hello") #=> hello
X does not include Kernel, but X.new.foo calls Kernel#puts.
This fallback comes from Ruby 1.8, where Object was the root class, so every class that includes a module always had Object and Kernel as ancestors. Since Ruby 1.9 introduced BasicObject, this is no longer true, but the fallback is unchanged. As discussed in #22273, a module should be able to alias only its own methods and its ancestors' methods, and Object is not an ancestor of a module.
This behavior is intentional in the current implementation. There are tests for it (test_alias_in_module in test/ruby/test_alias.rb for #9663, and "accesses a method defined on Object from Kernel" in spec/ruby/core/module/alias_method_spec.rb), and the documentation of Module#alias_method has an example module Mod; alias_method :orig_exit, :exit; end. So code like module M; alias orig_to_s to_s; end exists, and simply raising NameError will break it.
Possible fixes:
- Remove the fallback and raise
NameError. This breaks existing code. - Do not resolve the method at alias time. Instead, resolve it at call time from the ancestors of the receiver's class, like ZSUPER methods. Existing code that includes the module in a subclass of
Objectkeeps working, andX.new.fooabove raisesNoMethodError. - Keep the current behavior and document it.
I think 2 is the best choice for compatibility.
Updated by shugo (Shugo Maeda) 3 days ago
I implemented option 2: https://github.com/ruby/ruby/pull/18553
When alias in a module does not find the method in the module, it now creates a ZSUPER method entry whose original name is the aliased method. Object is searched only to check that the method exists and to take its visibility, so alias foo bar in a module still raises NameError when bar is not defined anywhere.
The method is resolved at call time, starting from the ancestors after the module in the receiver's class, in the same way as super. So:
module M
alias foo puts
public :foo
end
class X < BasicObject
include ::M
end
X.new.foo("hello") #=> NoMethodError (undefined method 'foo' for an instance of X)
class Y
include M
end
Y.new.foo("hello") #=> hello
Existing code that includes the module in a subclass of Object keeps working, including the alias_method_chain pattern:
Because the alias is resolved from the ancestors after the module, an alias in Kernel of a method defined in Object can no longer be called (the alias itself is still created). This is the "descendant method" case discussed in #22273. The spec "accesses a method defined on Object from Kernel" only checks the method lists, so it still passes.
Other visible changes:
M.instance_method(:foo)on the module itself raisesNameError, because the method cannot be resolved without a receiver class.Y.instance_method(:foo)andY.new.method(:foo)work, andMethod#namereturns:foo.private :class; alias_method :xyz, :classin a module used to raiseNameErrorby accident (the lookup continued from the module's superclass, which is nil). Now it creates a private alias. The testtest_undef_method_error_message_with_zsuper_methodis changed to use a class for the same error message check.
Internally, ZSUPER dispatch now looks up def->original_id instead of the called name. They were always the same before, so existing ZSUPER methods are not affected.