-
-
Notifications
You must be signed in to change notification settings - Fork 357
/
method_reference.rb
202 lines (170 loc) · 6.7 KB
/
method_reference.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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
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
private
def original_method
@object_reference.when_loaded do |m|
self.defined? && find_method(m)
end
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