summaryrefslogtreecommitdiff
path: root/lib/method_source.rb
blob: ecb546eaf7616d82f026089fb2cac582400b96ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# (C) John Mair (banisterfiend) 2010
# MIT License

direc = File.dirname(__FILE__)

require 'stringio'
require "#{direc}/method_source/version"

if RUBY_VERSION =~ /1.9/
  require 'ripper'
end

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
  end

  # Helper method responsible for extracting method body.
  # 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 }

    code = ""
    loop do
      val = file.readline
      code << val
      
      return code if MethodSource.valid_expression?(code)
    end
    
  ensure
    file.close if file
  end
  
  # Helper method responsible for opening source file and buffering up
  # the comments for a specified method. Defined here to avoid polluting 
  # `Method` class.
  # @param [Array] source_location The array returned by Method#source_location
  # @return [String] The comments up to the point of the method.
  def self.comment_helper(source_location)
    return nil if !source_location.is_a?(Array)
    
    file_name, line = source_location
    file = File.open(file_name)
    buffer = ""
    (line - 1).times do
      line = file.readline
      # Add any line that is a valid ruby comment, 
      # but clear as soon as we hit a non comment line.
      if (line =~ /^\s*#/) || (line =~ /^\s*$/)
        buffer << line.lstrip
      else
        buffer.clear
      end
    end
    
    buffer
  ensure
    file.close if 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
      if respond_to?(:source_location)
        source = MethodSource.source_helper(source_location)
        
        raise "Cannot locate source for this method: #{name}" if !source
      else
        raise "Method#source not supported by this Ruby version (#{RUBY_VERSION})"
      end
      
      source
    end
    
    # Return the comments associated with the method as a string.
    # (This functionality is only supported in Ruby 1.9 and above)
    # @return [String] The method's comments as a string
    # @example
    #  Set.instance_method(:clear).comment.display
    #  =>
    #     # Removes all elements and returns self.
    def comment
      if respond_to?(:source_location)
        comment = MethodSource.comment_helper(source_location)
        
        raise "Cannot locate source for this method: #{name}" if !comment
      else
        raise "Method#comment not supported by this Ruby version (#{RUBY_VERSION})"
      end

      comment
    end
  end
end

class Method
  include MethodSource::MethodExtensions
end

class UnboundMethod
  include MethodSource::MethodExtensions
end

class Proc
  include MethodSource::MethodExtensions
end