method_extensions 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ require "method_extensions/method/source_with_doc"
2
+ require "method_extensions/method/super"
@@ -0,0 +1,215 @@
1
+ ripper_available = true
2
+ begin
3
+ require "ripper"
4
+ rescue LoadError
5
+ ripper_available = false
6
+ end
7
+
8
+ module MethodSourceWithDoc
9
+ # Returns method source by parsing the file returned by `Method#source_location`.
10
+ #
11
+ # If method definition cannot be found `ArgumentError` exception is raised
12
+ # (this includes methods defined `attr_accessor`, `module_eval` etc.).
13
+ #
14
+ # Sample IRB session:
15
+ #
16
+ # ruby-1.9.2-head > require 'fileutils'
17
+ #
18
+ # ruby-1.9.2-head > puts FileUtils.method(:mkdir).source
19
+ # def mkdir(list, options = {})
20
+ # fu_check_options options, OPT_TABLE['mkdir']
21
+ # list = fu_list(list)
22
+ # fu_output_message "mkdir #{options[:mode] ? ('-m %03o ' % options[:mode]) : ''}#{list.join ' '}" if options[:verbose]
23
+ # return if options[:noop]
24
+ #
25
+ # list.each do |dir|
26
+ # fu_mkdir dir, options[:mode]
27
+ # end
28
+ # end
29
+ # => nil
30
+ def source
31
+ MethodSourceRipper.source_from_source_location(source_location)
32
+ end
33
+
34
+ # Returns comment preceding the method definition by parsing the file
35
+ # returned by `Method#source_location`
36
+ #
37
+ # Sample IRB session:
38
+ #
39
+ # ruby-1.9.2-head > require 'fileutils'
40
+ #
41
+ # ruby-1.9.2-head > puts FileUtils.method(:mkdir).doc
42
+ # #
43
+ # # Options: mode noop verbose
44
+ # #
45
+ # # Creates one or more directories.
46
+ # #
47
+ # # FileUtils.mkdir 'test'
48
+ # # FileUtils.mkdir %w( tmp data )
49
+ # # FileUtils.mkdir 'notexist', :noop => true # Does not really create.
50
+ # # FileUtils.mkdir 'tmp', :mode => 0700
51
+ # #
52
+ def doc
53
+ MethodDocRipper.doc_from_source_location(source_location)
54
+ end
55
+
56
+ # ruby-1.9.2-head > irb_context.inspect_mode = false # turn off inspect mode so that we can view sources
57
+ #
58
+ # ruby-1.9.2-head > ActiveRecord::Base.method(:find).source_with_doc
59
+ # ArgumentError: failed to find method definition around the lines:
60
+ # delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped
61
+ # delegate :find_each, :find_in_batches, :to => :scoped
62
+ #
63
+ # ruby-1.9.2-head > ActiveRecord::Base.method(:scoped).source_with_doc
64
+ # # Returns an anonymous scope.
65
+ # #
66
+ # # posts = Post.scoped
67
+ # # posts.size # Fires "select count(*) from posts" and returns the count
68
+ # # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
69
+ # #
70
+ # # fruits = Fruit.scoped
71
+ # # fruits = fruits.where(:colour => 'red') if options[:red_only]
72
+ # # fruits = fruits.limit(10) if limited?
73
+ # #
74
+ # # Anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
75
+ # # intermediate values (scopes) around as first-class objects is convenient.
76
+ # #
77
+ # # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
78
+ # def scoped(options = {}, &block)
79
+ # if options.present?
80
+ # relation = scoped.apply_finder_options(options)
81
+ # block_given? ? relation.extending(Module.new(&block)) : relation
82
+ # else
83
+ # current_scoped_methods ? unscoped.merge(current_scoped_methods) : unscoped.clone
84
+ # end
85
+ # end
86
+ #
87
+ # ruby-1.9.2-head > ActiveRecord::Base.method(:unscoped).source_with_doc
88
+ # => def unscoped
89
+ # @unscoped ||= Relation.new(self, arel_table)
90
+ # finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped
91
+ # end
92
+ #
93
+ # ruby-1.9.2-head > ActiveRecord::Relation.instance_method(:find).source_with_doc
94
+ # => # Find operates with four different retrieval approaches:
95
+ # ...
96
+ # def find(*args, &block)
97
+ # return to_a.find(&block) if block_given?
98
+ #
99
+ # options = args.extract_options!
100
+ #
101
+ # if options.present?
102
+ # ...
103
+ def source_with_doc
104
+ return unless source_location
105
+
106
+ [doc.to_s.chomp, source_unindent(source)].compact.reject(&:empty?).join("\n")
107
+ end
108
+
109
+ def full_inspect
110
+ "#{ inspect }\n#{ source_location }\n#{ source_with_doc }"
111
+ end
112
+
113
+ private
114
+
115
+ def source_unindent(src)
116
+ lines = src.split("\n")
117
+ indented_lines = lines[1 .. -1] # first line doesn't have proper indentation
118
+ indent_level = indented_lines.
119
+ reject { |line| line.strip.empty? }. # exclude empty lines from indent level calculation
120
+ map { |line| line[/^(\s*)/, 1].size }. # map to indent level of every line
121
+ min
122
+ [lines[0], *indented_lines.map { |line| line[indent_level .. -1] }].join("\n")
123
+ end
124
+
125
+ class ::Method
126
+ include MethodSourceWithDoc
127
+ end
128
+
129
+ class ::UnboundMethod
130
+ include MethodSourceWithDoc
131
+ end
132
+
133
+ class MethodSourceRipper < Ripper
134
+ def self.source_from_source_location(source_location)
135
+ return unless source_location
136
+ new(*source_location).method_source
137
+ end
138
+
139
+ def initialize(filename, method_definition_lineno)
140
+ super(IO.read(filename), filename)
141
+ @src_lines = IO.read(filename).split("\n")
142
+ @method_definition_lineno = method_definition_lineno
143
+ end
144
+
145
+ def method_source
146
+ parse
147
+ if @method_source
148
+ @method_source
149
+ else
150
+ raise ArgumentError.new("failed to find method definition around the lines:\n" <<
151
+ definition_lines.join("\n"))
152
+ end
153
+ end
154
+
155
+ def definition_lines
156
+ @src_lines[@method_definition_lineno - 1 .. @method_definition_lineno + 1]
157
+ end
158
+
159
+ Ripper::SCANNER_EVENTS.each do |meth|
160
+ define_method("on_#{ meth }") do |*args|
161
+ [lineno, column]
162
+ end
163
+ end
164
+
165
+ def on_def(name, params, body)
166
+ from_lineno, from_column = name
167
+ return unless @method_definition_lineno == from_lineno
168
+
169
+ to_lineno, to_column = lineno, column
170
+
171
+ @method_source = @src_lines[from_lineno - 1 .. to_lineno - 1].join("\n").strip
172
+ end
173
+
174
+ def on_defs(target, period, name, params, body)
175
+ on_def(target, params, body)
176
+ end
177
+ end
178
+
179
+ class MethodDocRipper < Ripper
180
+ def self.doc_from_source_location(source_location)
181
+ return unless source_location
182
+ new(*source_location).method_doc
183
+ end
184
+
185
+ def initialize(filename, method_definition_lineno)
186
+ super(IO.read(filename), filename)
187
+ @method_definition_lineno = method_definition_lineno
188
+ @last_comment_block = nil
189
+ end
190
+
191
+ def method_doc
192
+ parse
193
+ @method_doc
194
+ end
195
+
196
+ Ripper::SCANNER_EVENTS.each do |meth|
197
+ define_method("on_#{ meth }") do |token|
198
+ if @last_comment_block &&
199
+ lineno == @method_definition_lineno
200
+ @method_doc = @last_comment_block.join.gsub(/^\s*/, "")
201
+ end
202
+
203
+ @last_comment_block = nil
204
+ end
205
+ end
206
+
207
+ def on_comment(token)
208
+ (@last_comment_block ||= []) << token
209
+ end
210
+
211
+ def on_sp(token)
212
+ @last_comment_block << token if @last_comment_block
213
+ end
214
+ end
215
+ end if ripper_available
@@ -0,0 +1,331 @@
1
+ module MethodSuper
2
+ # Returns method which will be called if given Method/UnboundMethod would
3
+ # call `super`.
4
+ # Implementation is incomplete with regard to modules included in singleton
5
+ # class (`class C; extend M; end`), but such modules usually don't use `super`
6
+ # anyway.
7
+ #
8
+ # Examples
9
+ #
10
+ # class Base
11
+ # def meth; end
12
+ # end
13
+ #
14
+ # class Derived < Base
15
+ # def meth; end
16
+ # end
17
+ #
18
+ # ruby-1.9.2-head > Derived.instance_method(:meth)
19
+ # => #<UnboundMethod: Derived#meth>
20
+ #
21
+ # ruby-1.9.2-head > Derived.instance_method(:meth).super
22
+ # => #<UnboundMethod: Base#meth>
23
+ def super
24
+ raise ArgumentError, "method doesn't have required @context_for_super instance variable set" unless @context_for_super
25
+
26
+ klass, level, name = @context_for_super.values_at(:klass, :level, :name)
27
+
28
+ unless @methods_all
29
+ @methods_all = MethodSuper.methods_all(klass)
30
+
31
+ # on first call ignore first found method
32
+ superclass_index = MethodSuper.superclass_index(@methods_all,
33
+ level,
34
+ name)
35
+ @methods_all = @methods_all[superclass_index + 1 .. -1]
36
+
37
+ end
38
+
39
+ superclass_index = MethodSuper.superclass_index(@methods_all,
40
+ level,
41
+ name)
42
+
43
+ superclass = @methods_all[superclass_index].keys.first
44
+ rest_methods_all = @methods_all[superclass_index + 1 .. -1]
45
+
46
+ super_method = if level == :class && superclass.class == Class
47
+ superclass.method(name)
48
+ elsif level == :instance ||
49
+ (level == :class && superclass.class == Module)
50
+ superclass.instance_method(name)
51
+ end
52
+
53
+ super_method.instance_variable_set(:@context_for_super, @context_for_super)
54
+ super_method.instance_variable_set(:@methods_all, rest_methods_all)
55
+ super_method
56
+ end
57
+
58
+ private
59
+
60
+ def self.superclass_index(methods_all, level, name)
61
+ methods_all.index do |ancestor_with_methods|
62
+ ancestor, methods =
63
+ ancestor_with_methods.keys.first, ancestor_with_methods.values.first
64
+ methods[level] && methods[level].any? do |level, methods|
65
+ methods.include?(name)
66
+ end
67
+ end
68
+ end
69
+
70
+ def self.methods_all(klass)
71
+ MethodSuper::Methods.new(klass, :ancestor_name_formatter => proc { |ancestor, _| ancestor }).all
72
+ end
73
+
74
+ class Methods
75
+ def initialize(klass_or_module, options = {})
76
+ @klass_or_module = klass_or_module
77
+ @ancestor_name_formatter = options.fetch(:ancestor_name_formatter,
78
+ default_ancestor_name_formatter)
79
+ @exclude_trite = options.fetch(:exclude_trite, true)
80
+ end
81
+
82
+ def all
83
+ @all ||= find_all
84
+ end
85
+
86
+ VISIBILITIES = [ :public, :protected, :private ].freeze
87
+
88
+ protected
89
+
90
+ def default_ancestor_name_formatter
91
+ proc do |ancestor, singleton|
92
+ ancestor_name(ancestor, singleton)
93
+ end
94
+ end
95
+
96
+ def find_all
97
+ ancestors = [] # flattened ancestors (both normal and singleton)
98
+
99
+ (@klass_or_module.ancestors - trite_ancestors).each do |ancestor|
100
+ ancestor_singleton = ancestor.singleton_class
101
+
102
+ # Modules don't inherit class methods from included modules
103
+ unless @klass_or_module.instance_of?(Module) && ancestor != @klass_or_module
104
+ class_methods = collect_instance_methods(ancestor_singleton)
105
+ end
106
+
107
+ instance_methods = collect_instance_methods(ancestor)
108
+
109
+ append_ancestor_entry(ancestors, @ancestor_name_formatter[ancestor, false],
110
+ class_methods, instance_methods)
111
+
112
+ (singleton_ancestors(ancestor) || []).each do |singleton_ancestor|
113
+ class_methods = collect_instance_methods(singleton_ancestor)
114
+ append_ancestor_entry(ancestors, @ancestor_name_formatter[singleton_ancestor, true],
115
+ class_methods)
116
+ end
117
+ end
118
+
119
+ ancestors
120
+ end
121
+
122
+ # singleton ancestors which ancestor introduced
123
+ def singleton_ancestors(ancestor)
124
+ @singleton_ancestors ||= all_singleton_ancestors
125
+ @singleton_ancestors[ancestor]
126
+ end
127
+
128
+ def all_singleton_ancestors
129
+ all = {}
130
+ seen = []
131
+ (@klass_or_module.ancestors - trite_ancestors).reverse.each do |ancestor|
132
+ singleton_ancestors = ancestor.singleton_class.ancestors - trite_singleton_ancestors
133
+ introduces = singleton_ancestors - seen
134
+ all[ancestor] = introduces unless introduces.empty?
135
+ seen.concat singleton_ancestors
136
+ end
137
+ all
138
+ end
139
+
140
+ def ancestor_name(ancestor, singleton)
141
+ "#{ singleton ? "S" : ""}[#{ ancestor.is_a?(Class) ? "C" : "M" }] #{ ancestor.name || ancestor.to_s }"
142
+ end
143
+
144
+ # ancestor is included only when contributes some methods
145
+ def append_ancestor_entry(ancestors, ancestor, class_methods, instance_methods = nil)
146
+ if class_methods || instance_methods
147
+ ancestor_entry = {}
148
+ ancestor_entry[:class] = class_methods if class_methods
149
+ ancestor_entry[:instance] = instance_methods if instance_methods
150
+ ancestors << {ancestor => ancestor_entry}
151
+ end
152
+ end
153
+
154
+ # Returns hash { :public => [...public methods...],
155
+ # :protected => [...private methods...],
156
+ # :private => [...private methods...] }
157
+ # keys with empty values are excluded,
158
+ # when no methods are found - returns nil
159
+ def collect_instance_methods(klass)
160
+ methods_with_visibility = VISIBILITIES.map do |visibility|
161
+ methods = klass.send("#{ visibility }_instance_methods", false)
162
+ [visibility, methods] unless methods.empty?
163
+ end.compact
164
+ Hash[methods_with_visibility] unless methods_with_visibility.empty?
165
+ end
166
+
167
+ def trite_singleton_ancestors
168
+ return [] unless @exclude_trite
169
+ @trite_singleton_ancestors ||= Class.new.singleton_class.ancestors
170
+ end
171
+
172
+ def trite_ancestors
173
+ return [] unless @exclude_trite
174
+ @trite_ancestors ||= Class.new.ancestors
175
+ end
176
+ end
177
+ end
178
+
179
+ class Method
180
+ include MethodSuper
181
+ end
182
+
183
+ class UnboundMethod
184
+ include MethodSuper
185
+ end
186
+
187
+ class Module
188
+ def instance_method_with_ancestors_for_super(name)
189
+ method = instance_method_without_ancestors_for_super(name)
190
+ method.instance_variable_set(:@context_for_super,
191
+ :klass => self,
192
+ :level => :instance,
193
+ :name => name)
194
+
195
+ method
196
+ end
197
+
198
+ unless method_defined?(:instance_method_without_ancestors_for_super) ||
199
+ private_method_defined?(:instance_method_without_ancestors_for_super)
200
+ alias_method :instance_method_without_ancestors_for_super, :instance_method
201
+ alias_method :instance_method, :instance_method_with_ancestors_for_super
202
+ end
203
+ end
204
+
205
+ module Kernel
206
+ def method_with_ancestors_for_super(name)
207
+ method = method_without_ancestors_for_super(name)
208
+
209
+ if respond_to?(:ancestors)
210
+ method.instance_variable_set(:@context_for_super,
211
+ :klass => self,
212
+ :level => :class,
213
+ :name => name)
214
+ else
215
+ method.instance_variable_set(:@context_for_super,
216
+ :klass => self.class,
217
+ :level => :instance,
218
+ :name => name)
219
+ end
220
+
221
+ method
222
+ end
223
+
224
+ unless method_defined?(:method_without_ancestors_for_super) ||
225
+ private_method_defined?(:method_without_ancestors_for_super)
226
+ alias_method :method_without_ancestors_for_super, :method
227
+ alias_method :method, :method_with_ancestors_for_super
228
+ end
229
+ end
230
+
231
+ module BaseIncludedModule
232
+ def module_meth
233
+ end
234
+ end
235
+
236
+ module BaseExtendedModule
237
+ def module_meth
238
+ end
239
+ end
240
+
241
+ class BaseClass
242
+ include BaseIncludedModule
243
+ extend BaseExtendedModule
244
+
245
+ def self.singleton_meth
246
+ end
247
+
248
+ def meth
249
+ end
250
+ end
251
+
252
+ class DerivedClass < BaseClass
253
+ def self.singleton_meth
254
+ end
255
+
256
+ def self.module_meth
257
+ end
258
+
259
+ def meth
260
+ end
261
+
262
+ def module_meth
263
+ end
264
+ end
265
+
266
+ if $PROGRAM_NAME == __FILE__
267
+ require "rspec/core"
268
+ require "rspec/expectations"
269
+ require "rspec/matchers"
270
+
271
+
272
+ describe Method do
273
+ describe "#super" do
274
+ context "when called on result of DerivedClass.method(:singleton_meth)" do
275
+ it "returns BaseClass.method(:singleton_meth)" do
276
+ DerivedClass.method(:singleton_meth).super.should == BaseClass.method(:singleton_meth)
277
+ end
278
+
279
+ context "chained .super calls" do
280
+ context "when called on result of DerivedClass.method(:module_meth).super" do
281
+ it "returns BaseModule.instance_method(:module_meth)" do
282
+ DerivedClass.method(:module_meth).super.should == BaseExtendedModule.instance_method(:module_meth)
283
+ end
284
+ end
285
+
286
+ context "with class methods coming from extended modules only" do
287
+ it "returns proper super method" do
288
+ m1 = Module.new do
289
+ def m; end
290
+ end
291
+ m2 = Module.new do
292
+ def m; end
293
+ end
294
+ c = Class.new do
295
+ extend m1
296
+ extend m2
297
+ end
298
+ c.method(:m).super.should == m1.instance_method(:m)
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ context "when called on result of DerivedClass.new.method(:meth)" do
305
+ it "returns BaseClass.instance_method(:meth)" do
306
+ derived_instance = DerivedClass.new
307
+ derived_instance.method(:meth).super.should == BaseClass.instance_method(:meth)
308
+ end
309
+
310
+ context "chained .super calls" do
311
+ context "when called on result of DerivedClass.new.method(:module_meth).super" do
312
+ it "returns BaseModule.instance_method(:module_meth)" do
313
+ DerivedClass.new.method(:module_meth).super.should == BaseIncludedModule.instance_method(:module_meth)
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ end
320
+ end
321
+
322
+ describe UnboundMethod do
323
+ describe "#super" do
324
+ context "when called on result of DerivedClass.instance_method(:meth)" do
325
+ it "returns BaseClass.instance_method(:meth)" do
326
+ DerivedClass.instance_method(:meth).super.should == BaseClass.instance_method(:meth)
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,12 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "method_extensions"
6
+ s.version = "0.0.1"
7
+ s.authors = ["Evgeniy Dolzhenko"]
8
+ s.email = ["dolzenko@gmail.com"]
9
+ s.homepage = "http://github.com/dolzenko/method_extensions"
10
+ s.summary = "Method object extensions for better code navigation"
11
+ s.files = Dir.glob("lib/**/*") + %w(method_extensions.gemspec)
12
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: method_extensions
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Evgeniy Dolzhenko
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-27 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description:
22
+ email:
23
+ - dolzenko@gmail.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/method_extensions/method/source_with_doc.rb
32
+ - lib/method_extensions/method/super.rb
33
+ - lib/method_extensions.rb
34
+ - method_extensions.gemspec
35
+ has_rdoc: true
36
+ homepage: http://github.com/dolzenko/method_extensions
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ segments:
50
+ - 0
51
+ version: "0"
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.7
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Method object extensions for better code navigation
67
+ test_files: []
68
+