better_errors 2.0.0 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.coveralls.yml +1 -0
- data/.gitignore +3 -0
- data/.travis.yml +96 -2
- data/CHANGELOG.md +1 -1
- data/Gemfile +2 -7
- data/LICENSE.txt +1 -1
- data/README.md +99 -39
- data/better_errors.gemspec +23 -4
- data/gemfiles/pry010.gemfile +9 -0
- data/gemfiles/pry011.gemfile +8 -0
- data/gemfiles/pry09.gemfile +8 -0
- data/gemfiles/rack.gemfile +7 -0
- data/gemfiles/rack_boc.gemfile +8 -0
- data/gemfiles/rails42.gemfile +9 -0
- data/gemfiles/rails42_boc.gemfile +10 -0
- data/gemfiles/rails42_haml.gemfile +10 -0
- data/gemfiles/rails50.gemfile +8 -0
- data/gemfiles/rails50_boc.gemfile +9 -0
- data/gemfiles/rails50_haml.gemfile +9 -0
- data/gemfiles/rails51.gemfile +8 -0
- data/gemfiles/rails51_boc.gemfile +9 -0
- data/gemfiles/rails51_haml.gemfile +9 -0
- data/gemfiles/rails52.gemfile +8 -0
- data/gemfiles/rails52_boc.gemfile +9 -0
- data/gemfiles/rails52_haml.gemfile +9 -0
- data/gemfiles/rails60.gemfile +7 -0
- data/gemfiles/rails60_boc.gemfile +8 -0
- data/gemfiles/rails60_haml.gemfile +8 -0
- data/lib/better_errors/code_formatter/html.rb +1 -1
- data/lib/better_errors/code_formatter.rb +7 -7
- data/lib/better_errors/error_page.rb +56 -15
- data/lib/better_errors/inspectable_value.rb +45 -0
- data/lib/better_errors/middleware.rb +96 -16
- data/lib/better_errors/raised_exception.rb +13 -3
- data/lib/better_errors/repl/basic.rb +3 -3
- data/lib/better_errors/repl/pry.rb +18 -8
- data/lib/better_errors/repl.rb +6 -4
- data/lib/better_errors/stack_frame.rb +33 -8
- data/lib/better_errors/templates/main.erb +71 -34
- data/lib/better_errors/templates/text.erb +2 -2
- data/lib/better_errors/templates/variable_info.erb +32 -23
- data/lib/better_errors/version.rb +1 -1
- data/lib/better_errors.rb +21 -3
- metadata +118 -35
- data/Rakefile +0 -13
- data/spec/better_errors/code_formatter_spec.rb +0 -92
- data/spec/better_errors/error_page_spec.rb +0 -76
- data/spec/better_errors/middleware_spec.rb +0 -154
- data/spec/better_errors/raised_exception_spec.rb +0 -52
- data/spec/better_errors/repl/basic_spec.rb +0 -18
- data/spec/better_errors/repl/pry_spec.rb +0 -40
- data/spec/better_errors/repl/shared_examples.rb +0 -18
- data/spec/better_errors/stack_frame_spec.rb +0 -157
- data/spec/better_errors/support/my_source.rb +0 -20
- data/spec/better_errors_spec.rb +0 -73
- data/spec/spec_helper.rb +0 -5
- data/spec/without_binding_of_caller.rb +0 -9
@@ -11,9 +11,9 @@ module BetterErrors
|
|
11
11
|
".erb" => :erb,
|
12
12
|
".haml" => :haml
|
13
13
|
}
|
14
|
-
|
14
|
+
|
15
15
|
attr_reader :filename, :line, :context
|
16
|
-
|
16
|
+
|
17
17
|
def initialize(filename, line, context = 5)
|
18
18
|
@filename = filename
|
19
19
|
@line = line
|
@@ -29,7 +29,7 @@ module BetterErrors
|
|
29
29
|
def formatted_code
|
30
30
|
formatted_lines.join
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
def coderay_scanner
|
34
34
|
ext = File.extname(filename)
|
35
35
|
FILE_TYPES[ext] || :text
|
@@ -40,20 +40,20 @@ module BetterErrors
|
|
40
40
|
yield (current_line == line), current_line, str
|
41
41
|
}
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
def highlighted_lines
|
45
45
|
CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
def context_lines
|
49
49
|
range = line_range
|
50
50
|
source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
|
51
51
|
end
|
52
|
-
|
52
|
+
|
53
53
|
def source_lines
|
54
54
|
@source_lines ||= File.readlines(filename)
|
55
55
|
end
|
56
|
-
|
56
|
+
|
57
57
|
def line_range
|
58
58
|
min = [line - context, 1].max
|
59
59
|
max = [line + context, source_lines.count].min
|
@@ -10,7 +10,7 @@ module BetterErrors
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.template(template_name)
|
13
|
-
|
13
|
+
Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
|
14
14
|
end
|
15
15
|
|
16
16
|
attr_reader :exception, :env, :repls
|
@@ -26,8 +26,13 @@ module BetterErrors
|
|
26
26
|
@id ||= SecureRandom.hex(8)
|
27
27
|
end
|
28
28
|
|
29
|
-
def render(template_name = "main")
|
30
|
-
self.class.template(template_name).
|
29
|
+
def render(template_name = "main", csrf_token = nil)
|
30
|
+
binding.eval(self.class.template(template_name).src)
|
31
|
+
rescue => e
|
32
|
+
# Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
|
33
|
+
# We don't know the line number, so just injecting the template path has to be enough.
|
34
|
+
e.backtrace.unshift "#{self.class.template_path(template_name)}:0"
|
35
|
+
raise
|
31
36
|
end
|
32
37
|
|
33
38
|
def do_variables(opts)
|
@@ -41,23 +46,39 @@ module BetterErrors
|
|
41
46
|
index = opts["index"].to_i
|
42
47
|
code = opts["source"]
|
43
48
|
|
44
|
-
unless binding = backtrace_frames[index].frame_binding
|
49
|
+
unless (binding = backtrace_frames[index].frame_binding)
|
45
50
|
return { error: "REPL unavailable in this stack frame" }
|
46
51
|
end
|
47
52
|
|
48
|
-
|
49
|
-
(@repls[index] ||= REPL.provider.new(binding)).send_input(code)
|
53
|
+
@repls[index] ||= REPL.provider.new(binding, exception)
|
50
54
|
|
51
|
-
|
52
|
-
prompt: prompt,
|
53
|
-
prefilled_input: prefilled_input,
|
54
|
-
highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
|
55
|
+
eval_and_respond(index, code)
|
55
56
|
end
|
56
57
|
|
57
58
|
def backtrace_frames
|
58
59
|
exception.backtrace
|
59
60
|
end
|
60
61
|
|
62
|
+
def exception_type
|
63
|
+
exception.type
|
64
|
+
end
|
65
|
+
|
66
|
+
def exception_message
|
67
|
+
exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
|
68
|
+
end
|
69
|
+
|
70
|
+
def active_support_actions
|
71
|
+
return [] unless defined?(ActiveSupport::ActionableError)
|
72
|
+
|
73
|
+
ActiveSupport::ActionableError.actions(exception.type)
|
74
|
+
end
|
75
|
+
|
76
|
+
def action_dispatch_action_endpoint
|
77
|
+
return unless defined?(ActionDispatch::ActionableExceptions)
|
78
|
+
|
79
|
+
ActionDispatch::ActionableExceptions.endpoint
|
80
|
+
end
|
81
|
+
|
61
82
|
def application_frames
|
62
83
|
backtrace_frames.select(&:application?)
|
63
84
|
end
|
@@ -66,7 +87,8 @@ module BetterErrors
|
|
66
87
|
application_frames.first || backtrace_frames.first
|
67
88
|
end
|
68
89
|
|
69
|
-
|
90
|
+
private
|
91
|
+
|
70
92
|
def editor_url(frame)
|
71
93
|
BetterErrors.editor[frame.filename, frame.line]
|
72
94
|
end
|
@@ -100,11 +122,30 @@ module BetterErrors
|
|
100
122
|
end
|
101
123
|
|
102
124
|
def inspect_value(obj)
|
103
|
-
|
104
|
-
|
105
|
-
|
125
|
+
if BetterErrors.ignored_classes.include? obj.class.name
|
126
|
+
"<span class='unsupported'>(Instance of ignored class. "\
|
127
|
+
"#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
|
128
|
+
" BetterErrors.ignored_classes if you need to see it.)</span>"
|
129
|
+
else
|
130
|
+
InspectableValue.new(obj).to_html
|
131
|
+
end
|
132
|
+
rescue BetterErrors::ValueLargerThanConfiguredMaximum
|
133
|
+
"<span class='unsupported'>(Object too large. "\
|
134
|
+
"#{obj.class.name ? "Modify #{CGI.escapeHTML(obj.class.name)}#inspect or a" : "A"}"\
|
135
|
+
"djust BetterErrors.maximum_variable_inspect_size if you need to see it.)</span>"
|
106
136
|
rescue Exception => e
|
107
|
-
"<span class='unsupported'>(exception was raised in inspect)</span>"
|
137
|
+
"<span class='unsupported'>(exception #{CGI.escapeHTML(e.class.to_s)} was raised in inspect)</span>"
|
138
|
+
end
|
139
|
+
|
140
|
+
def eval_and_respond(index, code)
|
141
|
+
result, prompt, prefilled_input = @repls[index].send_input(code)
|
142
|
+
|
143
|
+
{
|
144
|
+
highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil),
|
145
|
+
prefilled_input: prefilled_input,
|
146
|
+
prompt: prompt,
|
147
|
+
result: result
|
148
|
+
}
|
108
149
|
end
|
109
150
|
end
|
110
151
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "objspace" rescue nil
|
3
|
+
|
4
|
+
module BetterErrors
|
5
|
+
class ValueLargerThanConfiguredMaximum < StandardError; end
|
6
|
+
|
7
|
+
class InspectableValue
|
8
|
+
def initialize(value)
|
9
|
+
@original_value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_html
|
13
|
+
raise ValueLargerThanConfiguredMaximum unless value_small_enough_to_inspect?
|
14
|
+
value_as_html
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :original_value
|
20
|
+
|
21
|
+
def value_as_html
|
22
|
+
@value_as_html ||= CGI.escapeHTML(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def value
|
26
|
+
@value ||= begin
|
27
|
+
if original_value.respond_to? :inspect
|
28
|
+
original_value.inspect
|
29
|
+
else
|
30
|
+
original_value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def value_small_enough_to_inspect?
|
36
|
+
return true if BetterErrors.maximum_variable_inspect_size.nil?
|
37
|
+
|
38
|
+
if defined?(ObjectSpace) && defined?(ObjectSpace.memsize_of) && ObjectSpace.memsize_of(value)
|
39
|
+
ObjectSpace.memsize_of(value) <= BetterErrors.maximum_variable_inspect_size
|
40
|
+
else
|
41
|
+
value_as_html.length <= BetterErrors.maximum_variable_inspect_size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require "json"
|
2
2
|
require "ipaddr"
|
3
|
+
require "securerandom"
|
3
4
|
require "set"
|
4
5
|
require "rack"
|
5
6
|
|
@@ -33,12 +34,14 @@ module BetterErrors
|
|
33
34
|
# Adds an address to the set of IP addresses allowed to access Better
|
34
35
|
# Errors.
|
35
36
|
def self.allow_ip!(addr)
|
36
|
-
ALLOWED_IPS << IPAddr.new(addr)
|
37
|
+
ALLOWED_IPS << (addr.is_a?(IPAddr) ? addr : IPAddr.new(addr))
|
37
38
|
end
|
38
39
|
|
39
40
|
allow_ip! "127.0.0.0/8"
|
40
41
|
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
|
41
42
|
|
43
|
+
CSRF_TOKEN_COOKIE_NAME = 'BetterErrors-CSRF-Token'
|
44
|
+
|
42
45
|
# A new instance of BetterErrors::Middleware
|
43
46
|
#
|
44
47
|
# @param app The Rack app/middleware to wrap with Better Errors
|
@@ -72,7 +75,7 @@ module BetterErrors
|
|
72
75
|
def better_errors_call(env)
|
73
76
|
case env["PATH_INFO"]
|
74
77
|
when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
|
75
|
-
internal_call
|
78
|
+
internal_call(env, $~[:id], $~[:method])
|
76
79
|
when %r{/__better_errors/?\z}
|
77
80
|
show_error_page env
|
78
81
|
else
|
@@ -89,53 +92,130 @@ module BetterErrors
|
|
89
92
|
end
|
90
93
|
|
91
94
|
def show_error_page(env, exception=nil)
|
95
|
+
request = Rack::Request.new(env)
|
96
|
+
csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid
|
97
|
+
|
92
98
|
type, content = if @error_page
|
93
99
|
if text?(env)
|
94
100
|
[ 'plain', @error_page.render('text') ]
|
95
101
|
else
|
96
|
-
[ 'html', @error_page.render ]
|
102
|
+
[ 'html', @error_page.render('main', csrf_token) ]
|
97
103
|
end
|
98
104
|
else
|
99
105
|
[ 'html', no_errors_page ]
|
100
106
|
end
|
101
107
|
|
102
108
|
status_code = 500
|
103
|
-
if defined?
|
109
|
+
if defined?(ActionDispatch::ExceptionWrapper) && exception
|
104
110
|
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
|
105
111
|
end
|
106
112
|
|
107
|
-
|
113
|
+
response = Rack::Response.new(content, status_code, { "Content-Type" => "text/#{type}; charset=utf-8" })
|
114
|
+
|
115
|
+
unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
|
116
|
+
response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, httponly: true, same_site: :strict)
|
117
|
+
end
|
118
|
+
|
119
|
+
# In older versions of Rack, the body returned here is actually a Rack::BodyProxy which seems to be a bug.
|
120
|
+
# (It contains status, headers and body and does not act like an array of strings.)
|
121
|
+
# Since we already have status code and body here, there's no need to use the ones in the Rack::Response.
|
122
|
+
(_status_code, headers, _body) = response.finish
|
123
|
+
[status_code, headers, [content]]
|
108
124
|
end
|
109
125
|
|
110
126
|
def text?(env)
|
111
127
|
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" ||
|
112
|
-
|
128
|
+
!env["HTTP_ACCEPT"].to_s.include?('html')
|
113
129
|
end
|
114
130
|
|
115
131
|
def log_exception
|
116
132
|
return unless BetterErrors.logger
|
117
133
|
|
118
|
-
message = "\n#{@error_page.
|
119
|
-
|
120
|
-
message << " #{frame}\n"
|
121
|
-
end
|
134
|
+
message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n"
|
135
|
+
message += backtrace_frames.map { |frame| " #{frame}\n" }.join
|
122
136
|
|
123
137
|
BetterErrors.logger.fatal message
|
124
138
|
end
|
125
139
|
|
126
|
-
def
|
127
|
-
if
|
128
|
-
|
140
|
+
def backtrace_frames
|
141
|
+
if defined?(Rails) && defined?(Rails.backtrace_cleaner)
|
142
|
+
Rails.backtrace_cleaner.clean @error_page.backtrace_frames.map(&:to_s)
|
143
|
+
else
|
144
|
+
@error_page.backtrace_frames
|
129
145
|
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def internal_call(env, id, method)
|
149
|
+
return not_found_json_response unless %w[variables eval].include?(method)
|
150
|
+
return no_errors_json_response unless @error_page
|
151
|
+
return invalid_error_json_response if id != @error_page.id
|
152
|
+
|
153
|
+
request = Rack::Request.new(env)
|
154
|
+
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
|
155
|
+
|
156
|
+
request.body.rewind
|
157
|
+
body = JSON.parse(request.body.read)
|
158
|
+
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME] == body['csrfToken']
|
130
159
|
|
131
|
-
|
132
|
-
|
133
|
-
|
160
|
+
return not_acceptable_json_response unless request.content_type == 'application/json'
|
161
|
+
|
162
|
+
response = @error_page.send("do_#{method}", body)
|
163
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]]
|
134
164
|
end
|
135
165
|
|
136
166
|
def no_errors_page
|
137
167
|
"<h1>No errors</h1><p>No errors have been recorded yet.</p><hr>" +
|
138
168
|
"<code>Better Errors v#{BetterErrors::VERSION}</code>"
|
139
169
|
end
|
170
|
+
|
171
|
+
def no_errors_json_response
|
172
|
+
explanation = if defined? Middleman
|
173
|
+
"Middleman reloads all dependencies for each request, " +
|
174
|
+
"which breaks Better Errors."
|
175
|
+
elsif defined?(Shotgun) && defined?(Hanami)
|
176
|
+
"Hanami is likely running with code-reloading enabled, which is the default. " +
|
177
|
+
"You can disable this by running hanami with the `--no-code-reloading` option."
|
178
|
+
elsif defined? Shotgun
|
179
|
+
"The shotgun gem causes everything to be reloaded for every request. " +
|
180
|
+
"You can disable shotgun in the Gemfile temporarily to use Better Errors."
|
181
|
+
else
|
182
|
+
"The application has been restarted since this page loaded, " +
|
183
|
+
"or the framework is reloading all gems before each request "
|
184
|
+
end
|
185
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
186
|
+
error: 'No exception information available',
|
187
|
+
explanation: explanation,
|
188
|
+
)]]
|
189
|
+
end
|
190
|
+
|
191
|
+
def invalid_error_json_response
|
192
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
193
|
+
error: "Session expired",
|
194
|
+
explanation: "This page was likely opened from a previous exception, " +
|
195
|
+
"and the exception is no longer available in memory.",
|
196
|
+
)]]
|
197
|
+
end
|
198
|
+
|
199
|
+
def invalid_csrf_token_json_response
|
200
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
201
|
+
error: "Invalid CSRF Token",
|
202
|
+
explanation: "The browser session might have been cleared, " +
|
203
|
+
"or something went wrong.",
|
204
|
+
)]]
|
205
|
+
end
|
206
|
+
|
207
|
+
def not_found_json_response
|
208
|
+
[404, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
209
|
+
error: "Not found",
|
210
|
+
explanation: "Not a recognized internal call.",
|
211
|
+
)]]
|
212
|
+
end
|
213
|
+
|
214
|
+
def not_acceptable_json_response
|
215
|
+
[406, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
216
|
+
error: "Request not acceptable",
|
217
|
+
explanation: "The internal request did not match an acceptable content type.",
|
218
|
+
)]]
|
219
|
+
end
|
140
220
|
end
|
141
221
|
end
|
@@ -5,6 +5,7 @@ module BetterErrors
|
|
5
5
|
|
6
6
|
def initialize(exception)
|
7
7
|
if exception.respond_to?(:original_exception) && exception.original_exception
|
8
|
+
# This supports some specific Rails exceptions, and is not intended to act the same as `#cause`.
|
8
9
|
exception = exception.original_exception
|
9
10
|
end
|
10
11
|
|
@@ -34,8 +35,13 @@ module BetterErrors
|
|
34
35
|
|
35
36
|
def setup_backtrace_from_bindings
|
36
37
|
@backtrace = exception.__better_errors_bindings_stack.map { |binding|
|
37
|
-
|
38
|
-
|
38
|
+
if binding.respond_to?(:source_location) # Ruby >= 2.6
|
39
|
+
file = binding.source_location[0]
|
40
|
+
line = binding.source_location[1]
|
41
|
+
else
|
42
|
+
file = binding.eval "__FILE__"
|
43
|
+
line = binding.eval "__LINE__"
|
44
|
+
end
|
39
45
|
name = binding.frame_description
|
40
46
|
StackFrame.new(file, line, name, binding)
|
41
47
|
}
|
@@ -51,7 +57,11 @@ module BetterErrors
|
|
51
57
|
|
52
58
|
def massage_syntax_error
|
53
59
|
case exception.class.to_s
|
54
|
-
when "
|
60
|
+
when "ActionView::Template::Error"
|
61
|
+
if exception.respond_to?(:file_name) && exception.respond_to?(:line_number)
|
62
|
+
backtrace.unshift(StackFrame.new(exception.file_name, exception.line_number.to_i, "view template"))
|
63
|
+
end
|
64
|
+
when "Haml::SyntaxError", "Sprockets::Coffeelint::Error"
|
55
65
|
if /\A(.+?):(\d+)/ =~ exception.backtrace.first
|
56
66
|
backtrace.unshift(StackFrame.new($1, $2.to_i, ""))
|
57
67
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module BetterErrors
|
2
2
|
module REPL
|
3
3
|
class Basic
|
4
|
-
def initialize(binding)
|
4
|
+
def initialize(binding, _exception)
|
5
5
|
@binding = binding
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
def send_input(str)
|
9
9
|
[execute(str), ">>", ""]
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
private
|
13
13
|
def execute(str)
|
14
14
|
"=> #{@binding.eval(str).inspect}\n"
|
@@ -9,30 +9,34 @@ module BetterErrors
|
|
9
9
|
Fiber.yield
|
10
10
|
end
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
class Output
|
14
14
|
def initialize
|
15
15
|
@buffer = ""
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def puts(*args)
|
19
19
|
args.each do |arg|
|
20
20
|
@buffer << "#{arg.chomp}\n"
|
21
21
|
end
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def tty?
|
25
25
|
false
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def read_buffer
|
29
29
|
@buffer
|
30
30
|
ensure
|
31
31
|
@buffer = ""
|
32
32
|
end
|
33
|
+
|
34
|
+
def print(*args)
|
35
|
+
@buffer << args.join(' ')
|
36
|
+
end
|
33
37
|
end
|
34
|
-
|
35
|
-
def initialize(binding)
|
38
|
+
|
39
|
+
def initialize(binding, exception)
|
36
40
|
@fiber = Fiber.new do
|
37
41
|
@pry.repl binding
|
38
42
|
end
|
@@ -40,9 +44,15 @@ module BetterErrors
|
|
40
44
|
@output = BetterErrors::REPL::Pry::Output.new
|
41
45
|
@pry = ::Pry.new input: @input, output: @output
|
42
46
|
@pry.hooks.clear_all if defined?(@pry.hooks.clear_all)
|
47
|
+
store_last_exception exception
|
43
48
|
@fiber.resume
|
44
49
|
end
|
45
|
-
|
50
|
+
|
51
|
+
def store_last_exception(exception)
|
52
|
+
return unless defined? ::Pry::LastException
|
53
|
+
@pry.instance_variable_set(:@last_exception, ::Pry::LastException.new(exception.exception))
|
54
|
+
end
|
55
|
+
|
46
56
|
def send_input(str)
|
47
57
|
local ::Pry.config, color: false, pager: false do
|
48
58
|
@fiber.resume "#{str}\n"
|
@@ -59,7 +69,7 @@ module BetterErrors
|
|
59
69
|
rescue
|
60
70
|
[">>", ""]
|
61
71
|
end
|
62
|
-
|
72
|
+
|
63
73
|
private
|
64
74
|
def local(obj, attrs)
|
65
75
|
old_attrs = {}
|
data/lib/better_errors/repl.rb
CHANGED
@@ -9,19 +9,21 @@ module BetterErrors
|
|
9
9
|
def self.provider
|
10
10
|
@provider ||= const_get detect[:const]
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
def self.provider=(prov)
|
14
14
|
@provider = prov
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def self.detect
|
18
18
|
PROVIDERS.find { |prov|
|
19
19
|
test_provider prov
|
20
20
|
}
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def self.test_provider(provider)
|
24
|
-
require
|
24
|
+
# We must load this file instead of `require`ing it, since during our tests we want the file
|
25
|
+
# to be reloaded. In practice, this will only be called once, so `require` is not necessary.
|
26
|
+
load "#{provider[:impl]}.rb"
|
25
27
|
true
|
26
28
|
rescue LoadError
|
27
29
|
false
|
@@ -33,7 +33,7 @@ module BetterErrors
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def gem_path
|
36
|
-
if path = Gem.path.detect { |
|
36
|
+
if path = Gem.path.detect { |p| filename.index(p) == 0 }
|
37
37
|
gem_name_and_version, path = filename.sub("#{path}/gems/", "").split("/", 2)
|
38
38
|
/(?<gem_name>.+)-(?<gem_version>[\w.]+)/ =~ gem_name_and_version
|
39
39
|
"#{gem_name} (#{gem_version}) #{path}"
|
@@ -68,15 +68,27 @@ module BetterErrors
|
|
68
68
|
|
69
69
|
def local_variables
|
70
70
|
return {} unless frame_binding
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
71
|
+
|
72
|
+
lv = frame_binding.eval("local_variables")
|
73
|
+
return {} unless lv
|
74
|
+
|
75
|
+
lv.each_with_object({}) do |name, hash|
|
76
|
+
# Ruby 2.2's local_variables will include the hidden #$! variable if
|
77
|
+
# called from within a rescue context. This is not a valid variable name,
|
78
|
+
# so the local_variable_get method complains. This should probably be
|
79
|
+
# considered a bug in Ruby itself, but we need to work around it.
|
80
|
+
next if name == :"\#$!"
|
81
|
+
|
82
|
+
hash[name] = local_variable(name)
|
77
83
|
end
|
78
84
|
end
|
79
85
|
|
86
|
+
def local_variable(name)
|
87
|
+
get_local_variable(name) || eval_local_variable(name)
|
88
|
+
rescue NameError => ex
|
89
|
+
"#{ex.class}: #{ex.message}"
|
90
|
+
end
|
91
|
+
|
80
92
|
def instance_variables
|
81
93
|
return {} unless frame_binding
|
82
94
|
Hash[visible_instance_variables.map { |x|
|
@@ -85,7 +97,10 @@ module BetterErrors
|
|
85
97
|
end
|
86
98
|
|
87
99
|
def visible_instance_variables
|
88
|
-
frame_binding.eval("instance_variables")
|
100
|
+
iv = frame_binding.eval("instance_variables")
|
101
|
+
return {} unless iv
|
102
|
+
|
103
|
+
iv - BetterErrors.ignored_instance_variables
|
89
104
|
end
|
90
105
|
|
91
106
|
def to_s
|
@@ -107,5 +122,15 @@ module BetterErrors
|
|
107
122
|
@method_name = "##{method_name}"
|
108
123
|
end
|
109
124
|
end
|
125
|
+
|
126
|
+
def get_local_variable(name)
|
127
|
+
if defined?(frame_binding.local_variable_get)
|
128
|
+
frame_binding.local_variable_get(name)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def eval_local_variable(name)
|
133
|
+
frame_binding.eval(name.to_s)
|
134
|
+
end
|
110
135
|
end
|
111
136
|
end
|