Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First commit

  • Loading branch information...
commit f20ab26a22f7f3e6e0c9b6c8a2bfa1f16dbe5fc3 0 parents
@dolzenko authored
2  lib/method_extensions.rb
@@ -0,0 +1,2 @@
+require "method_extensions/method/source_with_doc"
+require "method_extensions/method/super"
215 lib/method_extensions/method/source_with_doc.rb
@@ -0,0 +1,215 @@
+ripper_available = true
+begin
+ require "ripper"
+rescue LoadError
+ ripper_available = false
+end
+
+module MethodSourceWithDoc
+ # Returns method source by parsing the file returned by `Method#source_location`.
+ #
+ # If method definition cannot be found `ArgumentError` exception is raised
+ # (this includes methods defined `attr_accessor`, `module_eval` etc.).
+ #
+ # Sample IRB session:
+ #
+ # ruby-1.9.2-head > require 'fileutils'
+ #
+ # ruby-1.9.2-head > puts FileUtils.method(:mkdir).source
+ # def mkdir(list, options = {})
+ # fu_check_options options, OPT_TABLE['mkdir']
+ # list = fu_list(list)
+ # fu_output_message "mkdir #{options[:mode] ? ('-m %03o ' % options[:mode]) : ''}#{list.join ' '}" if options[:verbose]
+ # return if options[:noop]
+ #
+ # list.each do |dir|
+ # fu_mkdir dir, options[:mode]
+ # end
+ # end
+ # => nil
+ def source
+ MethodSourceRipper.source_from_source_location(source_location)
+ end
+
+ # Returns comment preceding the method definition by parsing the file
+ # returned by `Method#source_location`
+ #
+ # Sample IRB session:
+ #
+ # ruby-1.9.2-head > require 'fileutils'
+ #
+ # ruby-1.9.2-head > puts FileUtils.method(:mkdir).doc
+ # #
+ # # Options: mode noop verbose
+ # #
+ # # Creates one or more directories.
+ # #
+ # # FileUtils.mkdir 'test'
+ # # FileUtils.mkdir %w( tmp data )
+ # # FileUtils.mkdir 'notexist', :noop => true # Does not really create.
+ # # FileUtils.mkdir 'tmp', :mode => 0700
+ # #
+ def doc
+ MethodDocRipper.doc_from_source_location(source_location)
+ end
+
+ # ruby-1.9.2-head > irb_context.inspect_mode = false # turn off inspect mode so that we can view sources
+ #
+ # ruby-1.9.2-head > ActiveRecord::Base.method(:find).source_with_doc
+ # ArgumentError: failed to find method definition around the lines:
+ # delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped
+ # delegate :find_each, :find_in_batches, :to => :scoped
+ #
+ # ruby-1.9.2-head > ActiveRecord::Base.method(:scoped).source_with_doc
+ # # Returns an anonymous scope.
+ # #
+ # # posts = Post.scoped
+ # # posts.size # Fires "select count(*) from posts" and returns the count
+ # # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
+ # #
+ # # fruits = Fruit.scoped
+ # # fruits = fruits.where(:colour => 'red') if options[:red_only]
+ # # fruits = fruits.limit(10) if limited?
+ # #
+ # # Anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
+ # # intermediate values (scopes) around as first-class objects is convenient.
+ # #
+ # # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
+ # def scoped(options = {}, &block)
+ # if options.present?
+ # relation = scoped.apply_finder_options(options)
+ # block_given? ? relation.extending(Module.new(&block)) : relation
+ # else
+ # current_scoped_methods ? unscoped.merge(current_scoped_methods) : unscoped.clone
+ # end
+ # end
+ #
+ # ruby-1.9.2-head > ActiveRecord::Base.method(:unscoped).source_with_doc
+ # => def unscoped
+ # @unscoped ||= Relation.new(self, arel_table)
+ # finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped
+ # end
+ #
+ # ruby-1.9.2-head > ActiveRecord::Relation.instance_method(:find).source_with_doc
+ # => # Find operates with four different retrieval approaches:
+ # ...
+ # def find(*args, &block)
+ # return to_a.find(&block) if block_given?
+ #
+ # options = args.extract_options!
+ #
+ # if options.present?
+ # ...
+ def source_with_doc
+ return unless source_location
+
+ [doc.to_s.chomp, source_unindent(source)].compact.reject(&:empty?).join("\n")
+ end
+
+ def full_inspect
+ "#{ inspect }\n#{ source_location }\n#{ source_with_doc }"
+ end
+
+ private
+
+ def source_unindent(src)
+ lines = src.split("\n")
+ indented_lines = lines[1 .. -1] # first line doesn't have proper indentation
+ indent_level = indented_lines.
+ reject { |line| line.strip.empty? }. # exclude empty lines from indent level calculation
+ map { |line| line[/^(\s*)/, 1].size }. # map to indent level of every line
+ min
+ [lines[0], *indented_lines.map { |line| line[indent_level .. -1] }].join("\n")
+ end
+
+ class ::Method
+ include MethodSourceWithDoc
+ end
+
+ class ::UnboundMethod
+ include MethodSourceWithDoc
+ end
+
+ class MethodSourceRipper < Ripper
+ def self.source_from_source_location(source_location)
+ return unless source_location
+ new(*source_location).method_source
+ end
+
+ def initialize(filename, method_definition_lineno)
+ super(IO.read(filename), filename)
+ @src_lines = IO.read(filename).split("\n")
+ @method_definition_lineno = method_definition_lineno
+ end
+
+ def method_source
+ parse
+ if @method_source
+ @method_source
+ else
+ raise ArgumentError.new("failed to find method definition around the lines:\n" <<
+ definition_lines.join("\n"))
+ end
+ end
+
+ def definition_lines
+ @src_lines[@method_definition_lineno - 1 .. @method_definition_lineno + 1]
+ end
+
+ Ripper::SCANNER_EVENTS.each do |meth|
+ define_method("on_#{ meth }") do |*args|
+ [lineno, column]
+ end
+ end
+
+ def on_def(name, params, body)
+ from_lineno, from_column = name
+ return unless @method_definition_lineno == from_lineno
+
+ to_lineno, to_column = lineno, column
+
+ @method_source = @src_lines[from_lineno - 1 .. to_lineno - 1].join("\n").strip
+ end
+
+ def on_defs(target, period, name, params, body)
+ on_def(target, params, body)
+ end
+ end
+
+ class MethodDocRipper < Ripper
+ def self.doc_from_source_location(source_location)
+ return unless source_location
+ new(*source_location).method_doc
+ end
+
+ def initialize(filename, method_definition_lineno)
+ super(IO.read(filename), filename)
+ @method_definition_lineno = method_definition_lineno
+ @last_comment_block = nil
+ end
+
+ def method_doc
+ parse
+ @method_doc
+ end
+
+ Ripper::SCANNER_EVENTS.each do |meth|
+ define_method("on_#{ meth }") do |token|
+ if @last_comment_block &&
+ lineno == @method_definition_lineno
+ @method_doc = @last_comment_block.join.gsub(/^\s*/, "")
+ end
+
+ @last_comment_block = nil
+ end
+ end
+
+ def on_comment(token)
+ (@last_comment_block ||= []) << token
+ end
+
+ def on_sp(token)
+ @last_comment_block << token if @last_comment_block
+ end
+ end
+end if ripper_available
331 lib/method_extensions/method/super.rb
@@ -0,0 +1,331 @@
+module MethodSuper
+ # Returns method which will be called if given Method/UnboundMethod would
+ # call `super`.
+ # Implementation is incomplete with regard to modules included in singleton
+ # class (`class C; extend M; end`), but such modules usually don't use `super`
+ # anyway.
+ #
+ # Examples
+ #
+ # class Base
+ # def meth; end
+ # end
+ #
+ # class Derived < Base
+ # def meth; end
+ # end
+ #
+ # ruby-1.9.2-head > Derived.instance_method(:meth)
+ # => #<UnboundMethod: Derived#meth>
+ #
+ # ruby-1.9.2-head > Derived.instance_method(:meth).super
+ # => #<UnboundMethod: Base#meth>
+ def super
+ raise ArgumentError, "method doesn't have required @context_for_super instance variable set" unless @context_for_super
+
+ klass, level, name = @context_for_super.values_at(:klass, :level, :name)
+
+ unless @methods_all
+ @methods_all = MethodSuper.methods_all(klass)
+
+ # on first call ignore first found method
+ superclass_index = MethodSuper.superclass_index(@methods_all,
+ level,
+ name)
+ @methods_all = @methods_all[superclass_index + 1 .. -1]
+
+ end
+
+ superclass_index = MethodSuper.superclass_index(@methods_all,
+ level,
+ name)
+
+ superclass = @methods_all[superclass_index].keys.first
+ rest_methods_all = @methods_all[superclass_index + 1 .. -1]
+
+ super_method = if level == :class && superclass.class == Class
+ superclass.method(name)
+ elsif level == :instance ||
+ (level == :class && superclass.class == Module)
+ superclass.instance_method(name)
+ end
+
+ super_method.instance_variable_set(:@context_for_super, @context_for_super)
+ super_method.instance_variable_set(:@methods_all, rest_methods_all)
+ super_method
+ end
+
+ private
+
+ def self.superclass_index(methods_all, level, name)
+ methods_all.index do |ancestor_with_methods|
+ ancestor, methods =
+ ancestor_with_methods.keys.first, ancestor_with_methods.values.first
+ methods[level] && methods[level].any? do |level, methods|
+ methods.include?(name)
+ end
+ end
+ end
+
+ def self.methods_all(klass)
+ MethodSuper::Methods.new(klass, :ancestor_name_formatter => proc { |ancestor, _| ancestor }).all
+ end
+
+ class Methods
+ def initialize(klass_or_module, options = {})
+ @klass_or_module = klass_or_module
+ @ancestor_name_formatter = options.fetch(:ancestor_name_formatter,
+ default_ancestor_name_formatter)
+ @exclude_trite = options.fetch(:exclude_trite, true)
+ end
+
+ def all
+ @all ||= find_all
+ end
+
+ VISIBILITIES = [ :public, :protected, :private ].freeze
+
+ protected
+
+ def default_ancestor_name_formatter
+ proc do |ancestor, singleton|
+ ancestor_name(ancestor, singleton)
+ end
+ end
+
+ def find_all
+ ancestors = [] # flattened ancestors (both normal and singleton)
+
+ (@klass_or_module.ancestors - trite_ancestors).each do |ancestor|
+ ancestor_singleton = ancestor.singleton_class
+
+ # Modules don't inherit class methods from included modules
+ unless @klass_or_module.instance_of?(Module) && ancestor != @klass_or_module
+ class_methods = collect_instance_methods(ancestor_singleton)
+ end
+
+ instance_methods = collect_instance_methods(ancestor)
+
+ append_ancestor_entry(ancestors, @ancestor_name_formatter[ancestor, false],
+ class_methods, instance_methods)
+
+ (singleton_ancestors(ancestor) || []).each do |singleton_ancestor|
+ class_methods = collect_instance_methods(singleton_ancestor)
+ append_ancestor_entry(ancestors, @ancestor_name_formatter[singleton_ancestor, true],
+ class_methods)
+ end
+ end
+
+ ancestors
+ end
+
+ # singleton ancestors which ancestor introduced
+ def singleton_ancestors(ancestor)
+ @singleton_ancestors ||= all_singleton_ancestors
+ @singleton_ancestors[ancestor]
+ end
+
+ def all_singleton_ancestors
+ all = {}
+ seen = []
+ (@klass_or_module.ancestors - trite_ancestors).reverse.each do |ancestor|
+ singleton_ancestors = ancestor.singleton_class.ancestors - trite_singleton_ancestors
+ introduces = singleton_ancestors - seen
+ all[ancestor] = introduces unless introduces.empty?
+ seen.concat singleton_ancestors
+ end
+ all
+ end
+
+ def ancestor_name(ancestor, singleton)
+ "#{ singleton ? "S" : ""}[#{ ancestor.is_a?(Class) ? "C" : "M" }] #{ ancestor.name || ancestor.to_s }"
+ end
+
+ # ancestor is included only when contributes some methods
+ def append_ancestor_entry(ancestors, ancestor, class_methods, instance_methods = nil)
+ if class_methods || instance_methods
+ ancestor_entry = {}
+ ancestor_entry[:class] = class_methods if class_methods
+ ancestor_entry[:instance] = instance_methods if instance_methods
+ ancestors << {ancestor => ancestor_entry}
+ end
+ end
+
+ # Returns hash { :public => [...public methods...],
+ # :protected => [...private methods...],
+ # :private => [...private methods...] }
+ # keys with empty values are excluded,
+ # when no methods are found - returns nil
+ def collect_instance_methods(klass)
+ methods_with_visibility = VISIBILITIES.map do |visibility|
+ methods = klass.send("#{ visibility }_instance_methods", false)
+ [visibility, methods] unless methods.empty?
+ end.compact
+ Hash[methods_with_visibility] unless methods_with_visibility.empty?
+ end
+
+ def trite_singleton_ancestors
+ return [] unless @exclude_trite
+ @trite_singleton_ancestors ||= Class.new.singleton_class.ancestors
+ end
+
+ def trite_ancestors
+ return [] unless @exclude_trite
+ @trite_ancestors ||= Class.new.ancestors
+ end
+ end
+end
+
+class Method
+ include MethodSuper
+end
+
+class UnboundMethod
+ include MethodSuper
+end
+
+class Module
+ def instance_method_with_ancestors_for_super(name)
+ method = instance_method_without_ancestors_for_super(name)
+ method.instance_variable_set(:@context_for_super,
+ :klass => self,
+ :level => :instance,
+ :name => name)
+
+ method
+ end
+
+ unless method_defined?(:instance_method_without_ancestors_for_super) ||
+ private_method_defined?(:instance_method_without_ancestors_for_super)
+ alias_method :instance_method_without_ancestors_for_super, :instance_method
+ alias_method :instance_method, :instance_method_with_ancestors_for_super
+ end
+end
+
+module Kernel
+ def method_with_ancestors_for_super(name)
+ method = method_without_ancestors_for_super(name)
+
+ if respond_to?(:ancestors)
+ method.instance_variable_set(:@context_for_super,
+ :klass => self,
+ :level => :class,
+ :name => name)
+ else
+ method.instance_variable_set(:@context_for_super,
+ :klass => self.class,
+ :level => :instance,
+ :name => name)
+ end
+
+ method
+ end
+
+ unless method_defined?(:method_without_ancestors_for_super) ||
+ private_method_defined?(:method_without_ancestors_for_super)
+ alias_method :method_without_ancestors_for_super, :method
+ alias_method :method, :method_with_ancestors_for_super
+ end
+end
+
+module BaseIncludedModule
+ def module_meth
+ end
+end
+
+module BaseExtendedModule
+ def module_meth
+ end
+end
+
+class BaseClass
+ include BaseIncludedModule
+ extend BaseExtendedModule
+
+ def self.singleton_meth
+ end
+
+ def meth
+ end
+end
+
+class DerivedClass < BaseClass
+ def self.singleton_meth
+ end
+
+ def self.module_meth
+ end
+
+ def meth
+ end
+
+ def module_meth
+ end
+end
+
+if $PROGRAM_NAME == __FILE__
+ require "rspec/core"
+ require "rspec/expectations"
+ require "rspec/matchers"
+
+
+ describe Method do
+ describe "#super" do
+ context "when called on result of DerivedClass.method(:singleton_meth)" do
+ it "returns BaseClass.method(:singleton_meth)" do
+ DerivedClass.method(:singleton_meth).super.should == BaseClass.method(:singleton_meth)
+ end
+
+ context "chained .super calls" do
+ context "when called on result of DerivedClass.method(:module_meth).super" do
+ it "returns BaseModule.instance_method(:module_meth)" do
+ DerivedClass.method(:module_meth).super.should == BaseExtendedModule.instance_method(:module_meth)
+ end
+ end
+
+ context "with class methods coming from extended modules only" do
+ it "returns proper super method" do
+ m1 = Module.new do
+ def m; end
+ end
+ m2 = Module.new do
+ def m; end
+ end
+ c = Class.new do
+ extend m1
+ extend m2
+ end
+ c.method(:m).super.should == m1.instance_method(:m)
+ end
+ end
+ end
+ end
+
+ context "when called on result of DerivedClass.new.method(:meth)" do
+ it "returns BaseClass.instance_method(:meth)" do
+ derived_instance = DerivedClass.new
+ derived_instance.method(:meth).super.should == BaseClass.instance_method(:meth)
+ end
+
+ context "chained .super calls" do
+ context "when called on result of DerivedClass.new.method(:module_meth).super" do
+ it "returns BaseModule.instance_method(:module_meth)" do
+ DerivedClass.new.method(:module_meth).super.should == BaseIncludedModule.instance_method(:module_meth)
+ end
+ end
+ end
+ end
+
+ end
+ end
+
+ describe UnboundMethod do
+ describe "#super" do
+ context "when called on result of DerivedClass.instance_method(:meth)" do
+ it "returns BaseClass.instance_method(:meth)" do
+ DerivedClass.instance_method(:meth).super.should == BaseClass.instance_method(:meth)
+ end
+ end
+ end
+ end
+end
12 method_extensions.gemspec
@@ -0,0 +1,12 @@
+lib = File.expand_path('../lib/', __FILE__)
+$:.unshift lib unless $:.include?(lib)
+
+Gem::Specification.new do |s|
+ s.name = "method_extensions"
+ s.version = "0.0.1"
+ s.authors = ["Evgeniy Dolzhenko"]
+ s.email = ["dolzenko@gmail.com"]
+ s.homepage = "http://github.com/dolzenko/method_extensions"
+ s.summary = "Method object extensions for better code navigation"
+ s.files = Dir.glob("lib/**/*") + %w(dolzenko.gemspec)
+end
18 push_and_release.rb
@@ -0,0 +1,18 @@
+require "yaml"
+
+GEM_NAME = "method_extensions"
+
+new_version = ENV["GEM_VERSION"]
+
+puts "Releasing #{ GEM_NAME } #{ new_version }"
+
+system "gem build #{ GEM_NAME }.gemspec --verbose"
+
+system "gem push #{ GEM_NAME }-#{ new_version }.gem --verbose"
+
+system "gem install #{ GEM_NAME } --version=#{ new_version } --local --verbose"
+
+File.delete("#{ GEM_NAME }-#{ new_version }.gem")
+
+system "git push"
+
Please sign in to comment.
Something went wrong with that request. Please try again.