-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathmethodfinder.rb
208 lines (185 loc) · 6.1 KB
/
methodfinder.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
203
204
205
206
207
208
# frozen_string_literal: true
require 'methodfinder/version'
require 'stringio'
class Object
# An alternative interface to the functionality of
# <tt>MethodFinder.find</tt>. Also allows to test for state other
# than the return value of the method.
#
# %w[a b c].find_method { |a| a.unknown(1) ; a == %w[a c] }
# #=> ["Array#delete_at", "Array#slice!"]
# 10.find_method { |n| n.unknown(3) == 1 }
# #=> ["Fixnum#%", "Fixnum#<=>", "Fixnum#>>", ...]
#
# Inside <tt>find_method</tt>'s block, the receiver is available as
# block argument and the special method <tt>unknown</tt> is used as
# a placeholder for the desired method.
#
# <tt>find_method</tt> can be called without passing a block. This
# is the same as calling <tt>MethodFinder.find</tt>.
#
# 10.find_method(1, 3)
# #=> ["Fixnum#%", "Fixnum#<=>", "Fixnum#>>", ...]
def find_method(*args, &block)
return MethodFinder.find(self, *args) unless block_given?
MethodFinder.find_unknown(self, &block)
end
end
module MethodFinder
# Default arguments for methods
# :nodoc:
ARGS = {
cycle: [1], # prevent cycling forever
tally: [], # Since Ruby 3.1 Enumberable tally takes an optional hash
}.freeze
# Ignoring methods, e.g. { :Object => [:ri, :vim] }
INSTANCE_METHOD_IGNORELIST = Hash.new { |h, k| h[k] = [] }
# Ignoring class methods
CLASS_METHOD_IGNORELIST = Hash.new { |h, k| h[k] = [] }
# IGNORING classes
CLASS_IGNORELIST = []
if RUBY_VERSION.start_with?('3')
CLASS_IGNORELIST << :SortedSet # this moved to a gem
end
INSTANCE_METHOD_IGNORELIST[:Object] << :find_method # prevent stack overflow
INSTANCE_METHOD_IGNORELIST[:Object] << :gem # funny testing stuff w/ Bundler
if defined?(Pry)
INSTANCE_METHOD_IGNORELIST[:Object] << :pry
CLASS_METHOD_IGNORELIST[:Object] << :pry
end
# true if METHOD_FINDER_DEBUG is truthy, false otherwise e.g.:
#
# $ METHOD_FINDER_DEBUG=1 irb # true
# $ METHOD_FINDER_DEBUG=0 irb # false
# $ METHOD_FINDER_DEBUG=false irb # false
# $ METHOD_FINDER_DEBUG= irb # false
@debug = !ENV.fetch('METHOD_FINDER_DEBUG', '').match?(/\A(0|false)?\z/i)
# Checks whether or not debugging is currently enabled
# :doc:
def self.debug?
@debug
end
# Toggles the debug mode
def self.toggle_debug!
@debug = !@debug
end
# Provided with a receiver, the desired result and possibly some
# arguments, <tt>MethodFinder.find</tt> will list all methods that
# produce the given result when called on the receiver with the
# provided arguments.
#
# MethodFinder.find(10, 1, 3)
# #=> ["Fixnum#%", "Fixnum#<=>", "Fixnum#>>", "Fixnum#[]", ...]
# MethodFinder.find("abc","ABC")
# #=> ["String#swapcase", "String#swapcase!", "String#upcase", ...]
# MethodFinder.find(10, 100, 2)
# #=> ["Fixnum#**"]
# MethodFinder.find(['a','b','c'], ['A','B','C']) { |x| x.upcase }
# #=> ["Array#collect", "Array#collect!", "Enumerable#collect_concat", ...]
def self.find(obj, res, *args, &block)
find_methods(obj) do |met|
o = begin
obj.dup
rescue StandardError
obj
end
m = o.method(met)
next unless m.arity <= args.size
warn(met) if debug?
a = ARGS.key?(met) ? ARGS[met] : args
begin
m.call(*a, &block) == res
rescue StandardError
nil
end
end
end
# Returns all currently defined modules and classes.
def self.find_classes_and_modules
with_redirected_streams do
candidates = Object.constants - CLASS_IGNORELIST
constants = candidates.sort.map { |c| Object.const_get(c) }
constants.select do |c|
c.instance_of?(Class) || c.instance_of?(Module)
end
end
end
# Searches for a given name within a class. The first parameter
# can either be a class object, a symbol or a string whereas the
# optional second parameter can be a string or a regular
# expression:
#
# MethodFinder.find_in_class_or_module('Array', 'shuff')
# #=> [:shuffle, :shuffle!]
# MethodFinder.find_in_class_or_module(Float, /^to/)
# #=> [:to_f, :to_i, :to_int, :to_r, :to_s]
#
# If the second parameter is omitted, all methods of the class or
# module will be returned.
#
# MethodFinder.find_in_class_or_module(Math)
# #=> [:acos, :acosh, :asin ... :tanh]
# :doc:
def self.find_in_class_or_module(klass, pattern = /./)
klasses = Object.const_get(klass.to_s)
class_methods = begin
klasses.methods(false)
rescue StandardError
[]
end
instance_methods = klasses.instance_methods(false)
all_methods = class_methods + instance_methods
all_methods.grep(/#{pattern}/).sort
end
# Returns a list of candidate methods for a given object. Added by Jan Lelis.
def self.methods_to_try(obj)
ret = obj.methods
ignorelist = select_ignorelist(obj)
klass = obj.is_a?(Module) ? obj : obj.class
klass.ancestors.each { |ancestor| ret -= ignorelist[ancestor.to_s.intern] }
ret.sort
end
private_class_method :methods_to_try
# Used by Object.find_method
# :nodoc:
def self.find_unknown(obj, &block)
find_methods(obj) do |met|
warn(met) if debug?
obj.class.class_eval("alias :unknown #{met}", __FILE__, __LINE__)
subject = begin
obj.dup
rescue StandardError # dup doesn't work for immutable types
obj
end
begin
block.call(subject)
rescue StandardError
nil
end
end
end
def self.find_methods(obj, &block)
with_redirected_streams do
found = methods_to_try(obj).select(&block)
found.map { |m| "#{obj.method(m).owner}##{m}" }
end
end
private_class_method :find_methods
def self.with_redirected_streams
orig_stdout = $stdout
orig_stderr = $stderr
unless debug?
$stdout = StringIO.new
$stderr = StringIO.new
end
yield
ensure
$stdout = orig_stdout
$stderr = orig_stderr
end
private_class_method :with_redirected_streams
def self.select_ignorelist(object)
object.is_a?(Module) ? CLASS_METHOD_IGNORELIST : INSTANCE_METHOD_IGNORELIST
end
private_class_method :select_ignorelist
end