From 2895bb245fd216be96da6e4d6b9e361248934450 Mon Sep 17 00:00:00 2001 From: John Mair Date: Fri, 17 Dec 2010 02:58:01 +1300 Subject: first commit --- .yardopts | 1 + README.markdown | 48 ++++++++++++++++++++++ Rakefile | 57 +++++++++++++++++++++++++++ lib/method_source.rb | 94 ++++++++++++++++++++++++++++++++++++++++++++ lib/method_source/version.rb | 3 ++ test/test.rb | 32 +++++++++++++++ 6 files changed, 235 insertions(+) create mode 100644 .yardopts create mode 100644 README.markdown create mode 100644 Rakefile create mode 100644 lib/method_source.rb create mode 100644 lib/method_source/version.rb create mode 100644 test/test.rb diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..a4e7838 --- /dev/null +++ b/.yardopts @@ -0,0 +1 @@ +-m markdown diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..0d73f38 --- /dev/null +++ b/README.markdown @@ -0,0 +1,48 @@ +method_source +============= + +(C) John Mair (banisterfiend) 2010 + +_retrieve the sourcecode for a method_ + +`method_source` is a utility to return a method's sourcecode as a +Ruby string. + +It is written in pure Ruby (no C). + +`method_source` provides the `source` method to the `Method` and +`UnboundMethod` classes. + +* Install the [gem](https://rubygems.org/gems/method_source): `gem install method_source` +* Read the [documentation](http://rdoc.info/github/banister/method_source/master/file/README.markdown) +* See the [source code](http://github.com/banister/method_source) + +example: +--------- + + Set.instance_method(:merge).source.display + # => + def merge(enum) + if enum.instance_of?(self.class) + @hash.update(enum.instance_variable_get(:@hash)) + else + do_with_enum(enum) { |o| add(o) } + end + + self + end + +Limitations: +------------ + +* Only works with Ruby 1.9+ +* Cannot return source for C methods. +* Cannot return source for dynamically defined methods. + +Possible Applications: +---------------------- + +* Combine with [RubyParser](https://github.com/seattlerb/ruby_parser) + for extra fun. + + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..4edeaac --- /dev/null +++ b/Rakefile @@ -0,0 +1,57 @@ +dlext = Config::CONFIG['DLEXT'] +direc = File.dirname(__FILE__) + +require 'rake/clean' +require 'rake/gempackagetask' +require "#{direc}/lib/method_source/version" + +CLOBBER.include("**/*.#{dlext}", "**/*~", "**/*#*", "**/*.log", "**/*.o") +CLEAN.include("ext/**/*.#{dlext}", "ext/**/*.log", "ext/**/*.o", + "ext/**/*~", "ext/**/*#*", "ext/**/*.obj", + "ext/**/*.def", "ext/**/*.pdb", "**/*_flymake*.*", "**/*_flymake") + +def apply_spec_defaults(s) + s.name = "method_source" + s.summary = "retrieve the sourcecode for a method" + s.version = MethodSource::VERSION + s.date = Time.now.strftime '%Y-%m-%d' + s.author = "John Mair (banisterfiend)" + s.email = 'jrmair@gmail.com' + s.description = s.summary + s.require_path = 'lib' + s.homepage = "http://banisterfiend.wordpress.com" + s.has_rdoc = 'yard' + s.files = Dir["ext/**/extconf.rb", "ext/**/*.h", "ext/**/*.c", "lib/**/*.rb", + "test/*.rb", "CHANGELOG", "README.markdown", "Rakefile"] +end + +task :test do + sh "bacon -k #{direc}/test/test.rb" +end + +namespace :ruby do + spec = Gem::Specification.new do |s| + apply_spec_defaults(s) + s.platform = Gem::Platform::RUBY + end + + Rake::GemPackageTask.new(spec) do |pkg| + pkg.need_zip = false + pkg.need_tar = false + end +end + +desc "build all platform gems at once" +task :gems => [:rmgems, "ruby:gem"] + +desc "remove all platform gems" +task :rmgems => ["ruby:clobber_package"] + +desc "build and push latest gems" +task :pushgems => :gems do + chdir("#{direc}/pkg") do + Dir["*.gem"].each do |gemfile| + sh "gem push #{gemfile}" + end + end +end diff --git a/lib/method_source.rb b/lib/method_source.rb new file mode 100644 index 0000000..bdb00ed --- /dev/null +++ b/lib/method_source.rb @@ -0,0 +1,94 @@ +# (C) John Mair (banisterfiend) 2010 +# MIT License + +direc = File.dirname(__FILE__) + +require 'stringio' +require "#{direc}/method_source/version" + +module MethodSource + + # Helper method used to find end of method body + # @param [String] code The string of Ruby code to check for + # correctness + # @return [Boolean] + def self.valid_expression?(code) + suppress_stderr do + RubyVM::InstructionSequence.new(code) + end + rescue Exception + false + else + true + end + + # Helper method used to suppress stderr output by the + # `RubyVM::InstructionSequence` method + # @yield The block where stderr is suppressed + def self.suppress_stderr + real_stderr, $stderr = $stderr, StringIO.new + yield + ensure + $stderr = real_stderr + end + + # Helper method responsible for opening source file and advancing to + # the correct linenumber. Defined here to avoid polluting `Method` + # class. + # @param [Array] source_location The array returned by Method#source_location + # @return [File] The opened source file + def self.source_helper(source_location) + return nil if !source_location.is_a?(Array) + + file_name, line = source_location + file = File.open(file_name) + (line - 1).times { file.readline } + file + end + + # This module is to be included by `Method` and `UnboundMethod` and + # provides the `#source` functionality + module MethodExtensions + + # Return the sourcecode for the method as a string + # (This functionality is only supported in Ruby 1.9 and above) + # @return [String] The method sourcecode as a string + # @example + # Set.instance_method(:clear).source.display + # => + # def clear + # @hash.clear + # self + # end + def source + file = nil + + if respond_to?(:source_location) + file = MethodSource.source_helper(source_location) + + raise "Cannot locate source for this method: #{name}" if !file + else + raise "Method#source not supported by this Ruby version (#{RUBY_VERSION})" + end + + code = "" + loop do + val = file.readline + code += val + + return code if MethodSource.valid_expression?(code) + end + + ensure + file.close if file + end + end +end + +class Method + include MethodSource::MethodExtensions +end + +class UnboundMethod + include MethodSource::MethodExtensions +end diff --git a/lib/method_source/version.rb b/lib/method_source/version.rb new file mode 100644 index 0000000..30c0683 --- /dev/null +++ b/lib/method_source/version.rb @@ -0,0 +1,3 @@ +module MethodSource + VERSION = "0.1.0" +end diff --git a/test/test.rb b/test/test.rb new file mode 100644 index 0000000..8a4beaa --- /dev/null +++ b/test/test.rb @@ -0,0 +1,32 @@ +direc = File.dirname(__FILE__) + +require 'bacon' +require "#{direc}/../lib/method_source" + +hello_source = "def hello; :hello; end\n" + +def hello; :hello; end + +describe MethodSource do + + it 'should define methods on both Method and UnboundMethod' do + Method.method_defined?(:source).should == true + UnboundMethod.method_defined?(:source).should == true + end + + if RUBY_VERSION =~ /1.9/ + it 'should return source for method' do + method(:hello).source.should == hello_source + end + + it 'should raise for C methods' do + lambda { method(:puts).source }.should.raise RuntimeError + end + + else + it 'should raise on #source' do + lambda { method(:hello).source }.should.raise RuntimeError + end + end +end + -- cgit v1.2.1