better_errors 2.3.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.coveralls.yml +1 -0
- data/.github/workflows/ci.yml +130 -0
- data/.github/workflows/release.yml +64 -0
- data/.gitignore +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +1 -1
- data/Gemfile +7 -8
- data/README.md +81 -6
- data/better_errors.gemspec +18 -1
- data/gemfiles/pry010.gemfile +10 -0
- data/gemfiles/pry011.gemfile +9 -0
- data/gemfiles/pry09.gemfile +9 -0
- data/gemfiles/rack.gemfile +8 -0
- data/gemfiles/rack_boc.gemfile +9 -0
- data/gemfiles/rails42.gemfile +10 -0
- data/gemfiles/rails42_boc.gemfile +11 -0
- data/gemfiles/rails42_haml.gemfile +11 -0
- data/gemfiles/rails50.gemfile +9 -0
- data/gemfiles/rails50_boc.gemfile +10 -0
- data/gemfiles/rails50_haml.gemfile +10 -0
- data/gemfiles/rails51.gemfile +9 -0
- data/gemfiles/rails51_boc.gemfile +10 -0
- data/gemfiles/rails51_haml.gemfile +10 -0
- data/gemfiles/rails52.gemfile +9 -0
- data/gemfiles/rails52_boc.gemfile +10 -0
- data/gemfiles/rails52_haml.gemfile +10 -0
- data/gemfiles/rails60.gemfile +8 -0
- data/gemfiles/rails60_boc.gemfile +9 -0
- data/gemfiles/rails60_haml.gemfile +9 -0
- data/lib/better_errors/editor.rb +99 -0
- data/lib/better_errors/error_page.rb +39 -9
- data/lib/better_errors/exception_hint.rb +29 -0
- data/lib/better_errors/inspectable_value.rb +45 -0
- data/lib/better_errors/middleware.rb +68 -15
- data/lib/better_errors/raised_exception.rb +25 -4
- data/lib/better_errors/stack_frame.rb +25 -7
- data/lib/better_errors/templates/main.erb +97 -42
- data/lib/better_errors/templates/text.erb +5 -2
- data/lib/better_errors/templates/variable_info.erb +12 -5
- data/lib/better_errors/version.rb +1 -1
- data/lib/better_errors.rb +29 -30
- metadata +119 -7
- data/.travis.yml +0 -9
@@ -0,0 +1,99 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module BetterErrors
|
4
|
+
class Editor
|
5
|
+
KNOWN_EDITORS = [
|
6
|
+
{ symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
|
7
|
+
{ symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
|
8
|
+
{ symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
|
9
|
+
{ symbols: [:macvim, :mvim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
|
10
|
+
{ symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
|
11
|
+
{ symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
|
12
|
+
{ symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
|
13
|
+
{ symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
|
14
|
+
{ symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
|
15
|
+
]
|
16
|
+
|
17
|
+
def self.for_formatting_string(formatting_string)
|
18
|
+
new proc { |file, line|
|
19
|
+
formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.for_proc(url_proc)
|
24
|
+
new url_proc
|
25
|
+
end
|
26
|
+
|
27
|
+
# Automatically sniffs a default editor preset based on
|
28
|
+
# environment variables.
|
29
|
+
#
|
30
|
+
# @return [Symbol]
|
31
|
+
def self.default_editor
|
32
|
+
editor_from_environment_formatting_string ||
|
33
|
+
editor_from_environment_editor ||
|
34
|
+
editor_from_symbol(:textmate)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.editor_from_environment_editor
|
38
|
+
if ENV["BETTER_ERRORS_EDITOR"]
|
39
|
+
editor = editor_from_command(ENV["BETTER_ERRORS_EDITOR"])
|
40
|
+
return editor if editor
|
41
|
+
puts "BETTER_ERRORS_EDITOR environment variable is not recognized as a supported Better Errors editor."
|
42
|
+
end
|
43
|
+
if ENV["EDITOR"]
|
44
|
+
editor = editor_from_command(ENV["EDITOR"])
|
45
|
+
return editor if editor
|
46
|
+
puts "EDITOR environment variable is not recognized as a supported Better Errors editor. Using TextMate by default."
|
47
|
+
else
|
48
|
+
puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.editor_from_command(editor_command)
|
53
|
+
env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] }
|
54
|
+
for_formatting_string(env_preset[:url]) if env_preset
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.editor_from_environment_formatting_string
|
58
|
+
return unless ENV['BETTER_ERRORS_EDITOR_URL']
|
59
|
+
|
60
|
+
for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL'])
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.editor_from_symbol(symbol)
|
64
|
+
KNOWN_EDITORS.each do |preset|
|
65
|
+
return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(url_proc)
|
70
|
+
@url_proc = url_proc
|
71
|
+
end
|
72
|
+
|
73
|
+
def url(raw_path, line)
|
74
|
+
if virtual_path && raw_path.start_with?(virtual_path)
|
75
|
+
if host_path
|
76
|
+
file = raw_path.sub(%r{\A#{virtual_path}}, host_path)
|
77
|
+
else
|
78
|
+
file = raw_path.sub(%r{\A#{virtual_path}/}, '')
|
79
|
+
end
|
80
|
+
else
|
81
|
+
file = raw_path
|
82
|
+
end
|
83
|
+
|
84
|
+
url_proc.call(file, line)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
attr_reader :url_proc
|
90
|
+
|
91
|
+
def virtual_path
|
92
|
+
@virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH']
|
93
|
+
end
|
94
|
+
|
95
|
+
def host_path
|
96
|
+
@host_path ||= ENV['BETTER_ERRORS_HOST_PATH']
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -26,8 +26,13 @@ module BetterErrors
|
|
26
26
|
@id ||= SecureRandom.hex(8)
|
27
27
|
end
|
28
28
|
|
29
|
-
def render(template_name = "main")
|
29
|
+
def render(template_name = "main", csrf_token = nil)
|
30
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)
|
@@ -59,7 +64,23 @@ module BetterErrors
|
|
59
64
|
end
|
60
65
|
|
61
66
|
def exception_message
|
62
|
-
exception.message.
|
67
|
+
exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
|
68
|
+
end
|
69
|
+
|
70
|
+
def exception_hint
|
71
|
+
exception.hint
|
72
|
+
end
|
73
|
+
|
74
|
+
def active_support_actions
|
75
|
+
return [] unless defined?(ActiveSupport::ActionableError)
|
76
|
+
|
77
|
+
ActiveSupport::ActionableError.actions(exception.type)
|
78
|
+
end
|
79
|
+
|
80
|
+
def action_dispatch_action_endpoint
|
81
|
+
return unless defined?(ActionDispatch::ActionableExceptions)
|
82
|
+
|
83
|
+
ActionDispatch::ActionableExceptions.endpoint
|
63
84
|
end
|
64
85
|
|
65
86
|
def application_frames
|
@@ -70,9 +91,10 @@ module BetterErrors
|
|
70
91
|
application_frames.first || backtrace_frames.first
|
71
92
|
end
|
72
93
|
|
73
|
-
|
94
|
+
private
|
95
|
+
|
74
96
|
def editor_url(frame)
|
75
|
-
BetterErrors.editor
|
97
|
+
BetterErrors.editor.url(frame.filename, frame.line)
|
76
98
|
end
|
77
99
|
|
78
100
|
def rack_session
|
@@ -104,11 +126,19 @@ module BetterErrors
|
|
104
126
|
end
|
105
127
|
|
106
128
|
def inspect_value(obj)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
129
|
+
if BetterErrors.ignored_classes.include? obj.class.name
|
130
|
+
"<span class='unsupported'>(Instance of ignored class. "\
|
131
|
+
"#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
|
132
|
+
" BetterErrors.ignored_classes if you need to see it.)</span>"
|
133
|
+
else
|
134
|
+
InspectableValue.new(obj).to_html
|
135
|
+
end
|
136
|
+
rescue BetterErrors::ValueLargerThanConfiguredMaximum
|
137
|
+
"<span class='unsupported'>(Object too large. "\
|
138
|
+
"#{obj.class.name ? "Modify #{CGI.escapeHTML(obj.class.name)}#inspect or a" : "A"}"\
|
139
|
+
"djust BetterErrors.maximum_variable_inspect_size if you need to see it.)</span>"
|
140
|
+
rescue Exception => e
|
141
|
+
"<span class='unsupported'>(exception #{CGI.escapeHTML(e.class.to_s)} was raised in inspect)</span>"
|
112
142
|
end
|
113
143
|
|
114
144
|
def eval_and_respond(index, code)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module BetterErrors
|
2
|
+
class ExceptionHint
|
3
|
+
def initialize(exception)
|
4
|
+
@exception = exception
|
5
|
+
end
|
6
|
+
|
7
|
+
def hint
|
8
|
+
case exception
|
9
|
+
when NoMethodError
|
10
|
+
/\Aundefined method `(?<method>[^']+)' for (?<val>[^:]+):(?<klass>\w+)/.match(exception.message) do |match|
|
11
|
+
if match[:val] == "nil"
|
12
|
+
return "Something is `nil` when it probably shouldn't be."
|
13
|
+
elsif !match[:klass].start_with? '0x'
|
14
|
+
return "`#{match[:method]}` is being called on a `#{match[:klass]}` object, "\
|
15
|
+
"which might not be the type of object you were expecting."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
when NameError
|
19
|
+
/\Aundefined local variable or method `(?<method>[^']+)' for/.match(exception.message) do |match|
|
20
|
+
return "`#{match[:method]}` is probably misspelled."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :exception
|
28
|
+
end
|
29
|
+
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-#{BetterErrors::VERSION}-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,11 +92,14 @@ 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 ]
|
@@ -104,32 +110,57 @@ module BetterErrors
|
|
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, path: "/", 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
134
|
message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n"
|
119
|
-
|
120
|
-
message << " #{frame}\n"
|
121
|
-
end
|
135
|
+
message += backtrace_frames.map { |frame| " #{frame}\n" }.join
|
122
136
|
|
123
137
|
BetterErrors.logger.fatal message
|
124
138
|
end
|
125
139
|
|
126
|
-
def
|
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
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def internal_call(env, id, method)
|
149
|
+
return not_found_json_response unless %w[variables eval].include?(method)
|
127
150
|
return no_errors_json_response unless @error_page
|
128
|
-
return invalid_error_json_response if
|
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']
|
159
|
+
|
160
|
+
return not_acceptable_json_response unless request.content_type == 'application/json'
|
129
161
|
|
130
|
-
|
131
|
-
|
132
|
-
[200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
|
162
|
+
response = @error_page.send("do_#{method}", body)
|
163
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]]
|
133
164
|
end
|
134
165
|
|
135
166
|
def no_errors_page
|
@@ -151,18 +182,40 @@ module BetterErrors
|
|
151
182
|
"The application has been restarted since this page loaded, " +
|
152
183
|
"or the framework is reloading all gems before each request "
|
153
184
|
end
|
154
|
-
[200, { "Content-Type" => "
|
185
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
155
186
|
error: 'No exception information available',
|
156
187
|
explanation: explanation,
|
157
188
|
)]]
|
158
189
|
end
|
159
190
|
|
160
191
|
def invalid_error_json_response
|
161
|
-
[200, { "Content-Type" => "
|
192
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
162
193
|
error: "Session expired",
|
163
194
|
explanation: "This page was likely opened from a previous exception, " +
|
164
195
|
"and the exception is no longer available in memory.",
|
165
196
|
)]]
|
166
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
|
167
220
|
end
|
168
221
|
end
|
@@ -1,12 +1,23 @@
|
|
1
|
+
require 'better_errors/exception_hint'
|
2
|
+
|
1
3
|
# @private
|
2
4
|
module BetterErrors
|
3
5
|
class RaisedException
|
4
|
-
attr_reader :exception, :message, :backtrace
|
6
|
+
attr_reader :exception, :message, :backtrace, :hint
|
5
7
|
|
6
8
|
def initialize(exception)
|
7
|
-
if exception.respond_to?(:cause)
|
9
|
+
if exception.class.name == "ActionView::Template::Error" && exception.respond_to?(:cause)
|
10
|
+
# Rails 6+ exceptions of this type wrap the "real" exception, and the real exception
|
11
|
+
# is actually more useful than the ActionView-provided wrapper. Once Better Errors
|
12
|
+
# supports showing all exceptions in the cause stack, this should go away. Or perhaps
|
13
|
+
# this can be changed to provide guidance by showing the second error in the cause stack
|
14
|
+
# under this condition.
|
8
15
|
exception = exception.cause if exception.cause
|
9
16
|
elsif exception.respond_to?(:original_exception) && exception.original_exception
|
17
|
+
# This supports some specific Rails exceptions, and this is not intended to act the same as
|
18
|
+
# the Ruby's {Exception#cause}.
|
19
|
+
# It's possible this should only support ActionView::Template::Error, but by not changing
|
20
|
+
# this we're preserving longstanding behavior of Better Errors with Rails < 6.
|
10
21
|
exception = exception.original_exception
|
11
22
|
end
|
12
23
|
|
@@ -14,6 +25,7 @@ module BetterErrors
|
|
14
25
|
@message = exception.message
|
15
26
|
|
16
27
|
setup_backtrace
|
28
|
+
setup_hint
|
17
29
|
massage_syntax_error
|
18
30
|
end
|
19
31
|
|
@@ -36,8 +48,13 @@ module BetterErrors
|
|
36
48
|
|
37
49
|
def setup_backtrace_from_bindings
|
38
50
|
@backtrace = exception.__better_errors_bindings_stack.map { |binding|
|
39
|
-
|
40
|
-
|
51
|
+
if binding.respond_to?(:source_location) # Ruby >= 2.6
|
52
|
+
file = binding.source_location[0]
|
53
|
+
line = binding.source_location[1]
|
54
|
+
else
|
55
|
+
file = binding.eval "__FILE__"
|
56
|
+
line = binding.eval "__LINE__"
|
57
|
+
end
|
41
58
|
name = binding.frame_description
|
42
59
|
StackFrame.new(file, line, name, binding)
|
43
60
|
}
|
@@ -64,5 +81,9 @@ module BetterErrors
|
|
64
81
|
end
|
65
82
|
end
|
66
83
|
end
|
84
|
+
|
85
|
+
def setup_hint
|
86
|
+
@hint = ExceptionHint.new(exception).hint
|
87
|
+
end
|
67
88
|
end
|
68
89
|
end
|
@@ -69,21 +69,26 @@ module BetterErrors
|
|
69
69
|
def local_variables
|
70
70
|
return {} unless frame_binding
|
71
71
|
|
72
|
-
frame_binding.eval("local_variables")
|
72
|
+
lv = frame_binding.eval("local_variables")
|
73
|
+
return {} unless lv
|
74
|
+
|
75
|
+
lv.each_with_object({}) do |name, hash|
|
73
76
|
# Ruby 2.2's local_variables will include the hidden #$! variable if
|
74
77
|
# called from within a rescue context. This is not a valid variable name,
|
75
78
|
# so the local_variable_get method complains. This should probably be
|
76
79
|
# considered a bug in Ruby itself, but we need to work around it.
|
77
80
|
next if name == :"\#$!"
|
78
81
|
|
79
|
-
|
80
|
-
hash[name] = frame_binding.local_variable_get(name)
|
81
|
-
else
|
82
|
-
hash[name] = frame_binding.eval(name.to_s)
|
83
|
-
end
|
82
|
+
hash[name] = local_variable(name)
|
84
83
|
end
|
85
84
|
end
|
86
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
|
+
|
87
92
|
def instance_variables
|
88
93
|
return {} unless frame_binding
|
89
94
|
Hash[visible_instance_variables.map { |x|
|
@@ -92,7 +97,10 @@ module BetterErrors
|
|
92
97
|
end
|
93
98
|
|
94
99
|
def visible_instance_variables
|
95
|
-
frame_binding.eval("instance_variables")
|
100
|
+
iv = frame_binding.eval("instance_variables")
|
101
|
+
return {} unless iv
|
102
|
+
|
103
|
+
iv - BetterErrors.ignored_instance_variables
|
96
104
|
end
|
97
105
|
|
98
106
|
def to_s
|
@@ -114,5 +122,15 @@ module BetterErrors
|
|
114
122
|
@method_name = "##{method_name}"
|
115
123
|
end
|
116
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
|
117
135
|
end
|
118
136
|
end
|