From 52ef85631d136a4ff31c0f4c2587b19db3daf512 Mon Sep 17 00:00:00 2001 From: John Mair Date: Mon, 28 Feb 2011 03:40:47 +1300 Subject: version 0.3.0, some ruby 1.8 support --- README.markdown | 17 +++----- Rakefile | 4 +- lib/method_source.rb | 48 +++++++++++++++------ lib/method_source/source_location.rb | 56 ++++++++++++++++++++++++ lib/method_source/version.rb | 2 +- test/test.rb | 84 +++++++++++++++--------------------- test/test_helper.rb | 8 +++- 7 files changed, 145 insertions(+), 74 deletions(-) create mode 100644 lib/method_source/source_location.rb diff --git a/README.markdown b/README.markdown index bc42a82..bb1c83d 100644 --- a/README.markdown +++ b/README.markdown @@ -1,11 +1,11 @@ method_source ============= -(C) John Mair (banisterfiend) 2010 +(C) John Mair (banisterfiend) 2011 _retrieve the sourcecode for a method_ -*NOTE:* This simply utilizes `Method#source_location` in Ruby 1.9; it +*NOTE:* This simply utilizes `Method#source_location`; it does not access the live AST. `method_source` is a utility to return a method's sourcecode as a @@ -15,6 +15,8 @@ Method comments can also be extracted using the `comment` method. It is written in pure Ruby (no C). +* Some Ruby 1.8 support now available. + `method_source` provides the `source` and `comment` methods to the `Method` and `UnboundMethod` and `Proc` classes. @@ -48,19 +50,14 @@ Example: display method comments Limitations: ------------ -* Only works with Ruby 1.9+ +* Proc#source not available in Ruby 1.8 +* Occasional strange behaviour in Ruby 1.8 * 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. - - Special Thanks -------------- [Adam Sanderson](https://github.com/adamsanderson) for `comment` functionality. +[Dmitry Elastic](https://github.com/dmitryelastic) for the brilliant Ruby 1.8 `source_location` hack. diff --git a/Rakefile b/Rakefile index 4edeaac..b24114d 100644 --- a/Rakefile +++ b/Rakefile @@ -19,10 +19,12 @@ def apply_spec_defaults(s) s.email = 'jrmair@gmail.com' s.description = s.summary s.require_path = 'lib' + s.add_dependency("ruby_parser",">=2.0.5") + s.add_development_dependency("bacon",">=1.1.0") 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"] + "test/*.rb", "CHANGELOG", "README.markdown", "Rakefile", ".gemtest"] end task :test do diff --git a/lib/method_source.rb b/lib/method_source.rb index d6a6e29..43651b9 100644 --- a/lib/method_source.rb +++ b/lib/method_source.rb @@ -4,19 +4,41 @@ direc = File.dirname(__FILE__) require "#{direc}/method_source/version" - -if RUBY_VERSION =~ /1.9/ - require 'ripper' -end +require "#{direc}/method_source/source_location" 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) - !!Ripper::SexpBuilder.new(code).parse + if RUBY_VERSION =~ /1.9/ + require 'ripper' + + # Determine if a string of code is a valid Ruby expression. + # Ruby 1.9 uses Ripper, Ruby 1.8 uses RubyParser. + # @param [String] code The code to validate. + # @return [Boolean] Whether or not the code is a valid Ruby expression. + # @example + # valid_expression?("class Hello") #=> false + # valid_expression?("class Hello; end") #=> true + def self.valid_expression?(code) + !!Ripper::SexpBuilder.new(code).parse + end + + else + require 'ruby_parser' + + # Determine if a string of code is a valid Ruby expression. + # Ruby 1.9 uses Ripper, Ruby 1.8 uses RubyParser. + # @param [String] code The code to validate. + # @return [Boolean] Whether or not the code is a valid Ruby expression. + # @example + # valid_expression?("class Hello") #=> false + # valid_expression?("class Hello; end") #=> true + def self.valid_expression?(code) + RubyParser.new.parse(code) + rescue Racc::ParseError, SyntaxError + false + else + true + end end # Helper method responsible for extracting method body. @@ -86,7 +108,7 @@ module MethodSource raise "Cannot locate source for this method: #{name}" if !source else - raise "Method#source not supported by this Ruby version (#{RUBY_VERSION})" + raise "#{self.class}#source not supported by this Ruby version (#{RUBY_VERSION})" end source @@ -105,7 +127,7 @@ module MethodSource raise "Cannot locate source for this method: #{name}" if !comment else - raise "Method#comment not supported by this Ruby version (#{RUBY_VERSION})" + raise "#{self.class}#comment not supported by this Ruby version (#{RUBY_VERSION})" end comment @@ -114,10 +136,12 @@ module MethodSource end class Method + include MethodSource::SourceLocation::MethodExtensions include MethodSource::MethodExtensions end class UnboundMethod + include MethodSource::SourceLocation::UnboundMethodExtensions include MethodSource::MethodExtensions end diff --git a/lib/method_source/source_location.rb b/lib/method_source/source_location.rb new file mode 100644 index 0000000..1878b18 --- /dev/null +++ b/lib/method_source/source_location.rb @@ -0,0 +1,56 @@ +module MethodSource + module SourceLocation + module MethodExtensions + def trace_func(event, file, line, id, binding, classname) + return unless event == 'call' + set_trace_func nil + + @file, @line = file, line + raise :found + end + + # Return the source location of a method for Ruby 1.8. + # @return [Array] A two element array. First element is the + # file, second element is the line in the file where the + # method definition is found. + def source_location + if @file.nil? + args =[*(1..(arity<-1 ? -arity-1 : arity ))] + + set_trace_func method(:trace_func).to_proc + call *args rescue nil + set_trace_func nil + @file = File.expand_path(@file) if @file && File.exist?(File.expand_path(@file)) + end + return [@file, @line] if File.exist?(@file.to_s) + end + end + + module UnboundMethodExtensions + + # Return the source location of an instance method for Ruby 1.8. + # @return [Array] A two element array. First element is the + # file, second element is the line in the file where the + # method definition is found. + def source_location + klass = case owner + when Class + owner + when Module + Class.new.tap { |v| v.send(:include, owner) } + end + + begin + klass.allocate.method(name).source_location + rescue TypeError + + # Assume we are dealing with a Singleton Class: + # 1. Get the instance object + # 2. Forward the source_location lookup to the instance + instance ||= ObjectSpace.each_object(owner).first + instance.method(name).source_location + end + end + end + end +end diff --git a/lib/method_source/version.rb b/lib/method_source/version.rb index 2ac241c..6a830a9 100644 --- a/lib/method_source/version.rb +++ b/lib/method_source/version.rb @@ -1,3 +1,3 @@ module MethodSource - VERSION = "0.2.0" + VERSION = "0.3.0" end diff --git a/test/test.rb b/test/test.rb index 48f3ed8..8b84538 100644 --- a/test/test.rb +++ b/test/test.rb @@ -21,28 +21,21 @@ describe MethodSource do end describe "Methods" do - if RUBY_VERSION =~ /1.9/ - it 'should return source for method' do - method(:hello).source.should == @hello_source - end - - it 'should return a comment for method' do - method(:hello).comment.should == @hello_comment - end - - it 'should raise for C methods' do - lambda { method(:puts).source }.should.raise RuntimeError - end + it 'should return source for method' do + method(:hello).source.should == @hello_source + end + + it 'should return a comment for method' do + method(:hello).comment.should == @hello_comment + end - else - it 'should raise on #source for 1.8' do - lambda { method(:hello).source }.should.raise RuntimeError - end + it 'should raise for C methods' do + lambda { method(:puts).source }.should.raise RuntimeError end end - describe "Lambdas and Procs" do - if RUBY_VERSION =~ /1.9/ + if RUBY_VERSION =~ /1.9/ + describe "Lambdas and Procs" do it 'should return source for proc' do MyProc.source.should == @proc_source end @@ -58,42 +51,35 @@ describe MethodSource do it 'should return comment for lambda' do MyLambda.comment.should == @lambda_comment end - else - it 'should raise on #source for 1.8' do - lambda { method(:hello).source }.should.raise RuntimeError - end end end + describe "Comment tests" do + before do + @comment1 = "# a\n# b\n" + @comment2 = "# a\n# b\n" + @comment3 = "# a\n#\n# b\n" + @comment4 = "# a\n# b\n" + @comment5 = "# a\n# b\n# c\n# d\n" + end - if RUBY_VERSION =~ /1.9/ - describe "Comment tests" do - before do - @comment1 = "# a\n# b\n" - @comment2 = "# a\n# b\n" - @comment3 = "# a\n#\n# b\n" - @comment4 = "# a\n# b\n" - @comment5 = "# a\n# b\n# c\n# d\n" - end - - it "should correctly extract multi-line comments" do - method(:comment_test1).comment.should == @comment1 - end - - it "should correctly strip leading whitespace before comments" do - method(:comment_test2).comment.should == @comment2 - end + it "should correctly extract multi-line comments" do + method(:comment_test1).comment.should == @comment1 + end - it "should keep empty comment lines" do - method(:comment_test3).comment.should == @comment3 - end - - it "should ignore blank lines between comments" do - method(:comment_test4).comment.should == @comment4 - end + it "should correctly strip leading whitespace before comments" do + method(:comment_test2).comment.should == @comment2 + end - it "should align all comments to same indent level" do - method(:comment_test5).comment.should == @comment5 - end + it "should keep empty comment lines" do + method(:comment_test3).comment.should == @comment3 end + + it "should ignore blank lines between comments" do + method(:comment_test4).comment.should == @comment4 + end + + it "should align all comments to same indent level" do + method(:comment_test5).comment.should == @comment5 + end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 78ef513..bb28e29 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,10 @@ - # A comment for hello +class String + def clear + replace("") + end +end + +# A comment for hello # It spans two lines and is indented by 2 spaces def hello; :hello; end -- cgit v1.2.1