summaryrefslogtreecommitdiff
path: root/lib/method_source.rb
blob: 9a3c325f75f22e1436cfff538d119ed427944637 (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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# (C) John Mair (banisterfiend) 2011
# MIT License

direc = File.dirname(__FILE__)

require "#{direc}/method_source/version"
require "#{direc}/method_source/source_location"

module MethodSource
  # Determine if a string of code is a valid Ruby expression.
  # @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?(str)
    if defined?(Rubinius::Melbourne19) && RUBY_VERSION =~ /^1\.9/
      Rubinius::Melbourne19.parse_string(str)
    elsif defined?(Rubinius::Melbourne)
      Rubinius::Melbourne.parse_string(str)
    else
      catch(:valid) {
        eval("BEGIN{throw :valid}\n#{str}")
      }
    end
    true
  rescue SyntaxError
    false
  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.open(file_name) do |file|
      (line - 1).times { file.readline }

      code = ""
      loop do
        val = file.readline
        code << val

        return code if valid_expression?(code)
      end
    end
  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.open(file_name) do |file|
      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.replace("")
        end
      end

      buffer
    end
  end

  # This module is to be included by `Method` and `UnboundMethod` and
  # provides the `#source` functionality
  module MethodExtensions

    # We use the included hook to patch Method#source on rubinius.
    # We need to use the included hook as Rubinius defines a `source`
    # on Method so including a module will have no effect (as it's
    # higher up the MRO).
    # @param [Class] klass The class that includes the module.
    def self.included(klass)
      if klass.method_defined?(:source) && Object.const_defined?(:RUBY_ENGINE) &&
          RUBY_ENGINE =~ /rbx/

        klass.class_eval do
          orig_source = instance_method(:source)

          define_method(:source) do
            begin
              super
            rescue
              orig_source.bind(self).call
            end
          end

        end
      end
    end

    # 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 "#{self.class}#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 "#{self.class}#comment not supported by this Ruby version (#{RUBY_VERSION})"
      end

      comment
    end
  end
end

class Method
  include MethodSource::SourceLocation::MethodExtensions
  include MethodSource::MethodExtensions
end

class UnboundMethod
  include MethodSource::SourceLocation::UnboundMethodExtensions
  include MethodSource::MethodExtensions
end

class Proc
  include MethodSource::SourceLocation::ProcExtensions
  include MethodSource::MethodExtensions
end