blob: aa2b34a15a1d69f629c0e383bcc380b2b5f6af4b (
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
# (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 [String] The method body
def self.source_helper(source_location)
return nil if !source_location.is_a?(Array)
# 1st try: simple eval
code = extract_code(source_location)
unless code
# 2nd try: attempt to re-scan method body, this time, assume we're inside an eval string simulate interpolation of #{} expressions by replacing it with placeholder
#
# A temporary work around for cases where method body is defined inside a
# string (i.e. class_evaled methods), and the resulting valid_expression
# doesn't return true due to string not being interpolated.
# (see https://github.com/banister/method_source/issues/13)
#
code = extract_code(source_location) { |code| code.gsub(/\#\{.*?\}/,"temp") }
end
code
rescue Errno::ENOENT
# source_location[0] of evaled methods would return (eval) if __FILE__ and __LINE__ is not given. File.readlines "(eval)" would raise ENOENT
nil
end
# @param [Array] source_location The array containing file_name [String], line [Fixnum]
# @param [Block] An optional block that can be passed that will be used to modify
# the code buffer before its syntax is evaluated
# @return [String] The method body
def self.extract_code(source_location)
file_name, line = source_location
code = ""
lines_for_file(file_name)[(line - 1)..-1].each do |line|
code << line
expression = block_given? ? yield(code) : code
return code if valid_expression?(expression)
end
nil
end
def self.lines_for_file(file_name)
@lines_for_file ||= {}
@lines_for_file[file_name] ||= File.readlines(file_name)
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
|