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
|
module MethodSource
module CodeHelpers
# @return [Boolean]
JRUBY_9200 = (defined?(JRUBY_VERSION) || false) && JRUBY_VERSION == '9.2.0.0'
# Retrieve the first expression starting on the given line of the given file.
#
# This is useful to get module or method source code.
#
# @param [Array<String>, File, String] file The file to parse, either as a File or as
# @param [Integer] line_number The line number at which to look.
# NOTE: The first line in a file is
# line 1!
# @param [Hash] options The optional configuration parameters.
# @option options [Boolean] :strict If set to true, then only completely
# valid expressions are returned. Otherwise heuristics are used to extract
# expressions that may have been valid inside an eval.
# @option options [Integer] :consume A number of lines to automatically
# consume (add to the expression buffer) without checking for validity.
# @return [String] The first complete expression
# @raise [SyntaxError] If the first complete expression can't be identified
def expression_at(file, line_number, options={})
options = {
:strict => false,
:consume => 0
}.merge!(options)
lines = file.is_a?(Array) ? file : file.each_line.to_a
relevant_lines = lines[(line_number - 1)..-1] || []
extract_first_expression(relevant_lines, options[:consume])
rescue SyntaxError => e
# JRuby 9.2.0.0 breaks #source_location for Procs (it reports line number
# as the last line of the Proc). This raises SyntaxError.
# See https://github.com/pry/pry/issues/1804 for details.
#
# To fix this, this hack rewinds source location one step at a time and
# tries to see if the new location is a complete expression.
#
# TODO: delete this once latest JRuby version is bumped.
# See https://github.com/banister/method_source/issues/52
if JRUBY_9200 && line_number > 0
loop do
line_number -= 1
# Skip empty lines since they are not real expressions.
break unless lines[line_number - 1] == "\n"
end
retry
end
raise if options[:strict]
begin
extract_first_expression(relevant_lines) do |code|
code.gsub(/\#\{.*?\}/, "temp")
end
rescue SyntaxError
raise e
end
end
# Retrieve the comment describing the expression on the given line of the given file.
#
# This is useful to get module or method documentation.
#
# @param [Array<String>, File, String] file The file to parse, either as a File or as
# a String or an Array of lines.
# @param [Integer] line_number The line number at which to look.
# NOTE: The first line in a file is line 1!
# @return [String] The comment
def comment_describing(file, line_number)
lines = file.is_a?(Array) ? file : file.each_line.to_a
extract_last_comment(lines[0..(line_number - 2)])
end
# Determine if a string of code is a complete Ruby expression.
# @param [String] code The code to validate.
# @return [Boolean] Whether or not the code is a complete Ruby expression.
# @raise [SyntaxError] Any SyntaxError that does not represent incompleteness.
# @example
# complete_expression?("class Hello") #=> false
# complete_expression?("class Hello; end") #=> true
# complete_expression?("class 123") #=> SyntaxError: unexpected tINTEGER
def complete_expression?(str)
old_verbose = $VERBOSE
$VERBOSE = nil
catch(:valid) do
eval("BEGIN{throw :valid}\n#{str}")
end
# Assert that a line which ends with a , or \ is incomplete.
str !~ /[,\\]\s*\z/
rescue IncompleteExpression
false
ensure
$VERBOSE = old_verbose
end
private
# Get the first expression from the input.
#
# @param [Array<String>] lines
# @param [Integer] consume A number of lines to automatically
# consume (add to the expression buffer) without checking for validity.
# @yield a clean-up function to run before checking for complete_expression
# @return [String] a valid ruby expression
# @raise [SyntaxError]
def extract_first_expression(lines, consume=0, &block)
code = consume.zero? ? "" : lines.slice!(0..(consume - 1)).join
lines.each do |v|
code << v
return code if complete_expression?(block ? block.call(code) : code)
end
raise SyntaxError, "unexpected $end"
end
# Get the last comment from the input.
#
# @param [Array<String>] lines
# @return [String]
def extract_last_comment(lines)
buffer = ""
lines.each do |line|
# 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
# An exception matcher that matches only subsets of SyntaxErrors that can be
# fixed by adding more input to the buffer.
module IncompleteExpression
GENERIC_REGEXPS = [
/unexpected (\$end|end-of-file|end-of-input|END_OF_FILE)/, # mri, jruby, ruby-2.0, ironruby
/embedded document meets end of file/, # =begin
/unterminated (quoted string|string|regexp) meets end of file/, # "quoted string" is ironruby
/can't find string ".*" anywhere before EOF/, # rbx and jruby
/missing 'end' for/, /expecting kWHEN/ # rbx
]
RBX_ONLY_REGEXPS = [
/expecting '[})\]]'(?:$|:)/, /expecting keyword_end/
]
def self.===(ex)
return false unless SyntaxError === ex
case ex.message
when *GENERIC_REGEXPS
true
when *RBX_ONLY_REGEXPS
rbx?
else
false
end
end
def self.rbx?
RbConfig::CONFIG['ruby_install_name'] == 'rbx'
end
end
end
end
|