RSpec::Support.require_rspec_support 'comparable_version' module RSpec module Mocks # Represents a method on an object that may or may not be defined. # The method may be an instance method on a module or a method on # any object. # # @private class MethodReference def self.for(object_reference, method_name) new(object_reference, method_name) end def initialize(object_reference, method_name) @object_reference = object_reference @method_name = method_name end # A method is implemented if sending the message does not result in # a `NoMethodError`. It might be dynamically implemented by # `method_missing`. def implemented? @object_reference.when_loaded do |m| method_implemented?(m) end end # Returns true if we definitively know that sending the method # will result in a `NoMethodError`. # # This is not simply the inverse of `implemented?`: there are # cases when we don't know if a method is implemented and # both `implemented?` and `unimplemented?` will return false. def unimplemented? @object_reference.when_loaded do |_m| return !implemented? end # If it's not loaded, then it may be implemented but we can't check. false end # A method is defined if we are able to get a `Method` object for it. # In that case, we can assert against metadata like the arity. def defined? @object_reference.when_loaded do |m| method_defined?(m) end end def with_signature return unless (original = original_method) yield Support::MethodSignature.new(original) end def visibility @object_reference.when_loaded do |m| return visibility_from(m) end # When it's not loaded, assume it's public. We don't want to # wrongly treat the method as private. :public end private def original_method @object_reference.when_loaded do |m| self.defined? && find_method(m) end end def self.instance_method_visibility_for(klass, method_name) if klass.public_method_defined?(method_name) :public elsif klass.private_method_defined?(method_name) :private elsif klass.protected_method_defined?(method_name) :protected end end class << self alias method_defined_at_any_visibility? instance_method_visibility_for end def self.method_visibility_for(object, method_name) vis = instance_method_visibility_for(class << object; self; end, method_name) # If the method is not defined on the class, `instance_method_visibility_for` # returns `nil`. However, it may be handled dynamically by `method_missing`, # so here we check `respond_to` (passing false to not check private methods). # # This only considers the public case, but I don't think it's possible to # write `method_missing` in such a way that it handles a dynamic message # with private or protected visibility. Ruby doesn't provide you with # the caller info. return vis unless vis.nil? proxy = RSpec::Mocks.space.proxy_for(object) respond_to = proxy.method_double_if_exists_for_message(:respond_to?) visible = respond_to && respond_to.original_method.call(method_name) || object.respond_to?(method_name) return :public if visible end end # @private class InstanceMethodReference < MethodReference private def method_implemented?(mod) MethodReference.method_defined_at_any_visibility?(mod, @method_name) end # Ideally, we'd use `respond_to?` for `method_implemented?` but we need a # reference to an instance to do that and we don't have one. Note that # we may get false negatives: if the method is implemented via # `method_missing`, we'll return `false` even though it meets our # definition of "implemented". However, it's the best we can do. alias method_defined? method_implemented? # works around the fact that repeated calls for method parameters will # falsely return empty arrays on JRuby in certain circumstances, this # is necessary here because we can't dup/clone UnboundMethods. # # This is necessary due to a bug in JRuby prior to 1.7.5 fixed in: # https://github.com/jruby/jruby/commit/99a0613fe29935150d76a9a1ee4cf2b4f63f4a27 if RUBY_PLATFORM == 'java' && RSpec::Support::ComparableVersion.new(JRUBY_VERSION) < '1.7.5' def find_method(mod) mod.dup.instance_method(@method_name) end else def find_method(mod) mod.instance_method(@method_name) end end def visibility_from(mod) MethodReference.instance_method_visibility_for(mod, @method_name) end end # @private class ObjectMethodReference < MethodReference def self.for(object_reference, method_name) if ClassNewMethodReference.applies_to?(method_name) { object_reference.when_loaded { |o| o } } ClassNewMethodReference.new(object_reference, method_name) else super end end private def method_implemented?(object) object.respond_to?(@method_name, true) end def method_defined?(object) (class << object; self; end).method_defined?(@method_name) end def find_method(object) object.method(@method_name) end def visibility_from(object) MethodReference.method_visibility_for(object, @method_name) end end # When a class's `.new` method is stubbed, we want to use the method # signature from `#initialize` because `.new`'s signature is a generic # `def new(*args)` and it simply delegates to `#initialize` and forwards # all args...so the method with the actually used signature is `#initialize`. # # This method reference implementation handles that specific case. # @private class ClassNewMethodReference < ObjectMethodReference def self.applies_to?(method_name) return false unless method_name == :new klass = yield return false unless klass.respond_to?(:new, true) # We only want to apply our special logic to normal `new` methods. # Methods that the user has monkeyed with should be left as-is. ::RSpec::Support.method_handle_for(klass, :new).owner == ::Class end def with_signature @object_reference.when_loaded do |klass| yield Support::MethodSignature.new(klass.instance_method(:initialize)) end end end end end