method_extensions 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+