better_errors 1.0.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 +10 -5
- data/.ruby-version +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +7 -7
- data/LICENSE.txt +1 -1
- data/README.md +103 -33
- data/better_errors.gemspec +26 -8
- 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/code_formatter/html.rb +1 -1
- data/lib/better_errors/code_formatter.rb +7 -7
- data/lib/better_errors/editor.rb +99 -0
- data/lib/better_errors/error_page.rb +83 -49
- data/lib/better_errors/exception_extension.rb +17 -0
- data/lib/better_errors/exception_hint.rb +29 -0
- data/lib/better_errors/inspectable_value.rb +45 -0
- data/lib/better_errors/middleware.rb +101 -21
- data/lib/better_errors/raised_exception.rb +89 -0
- data/lib/better_errors/repl/basic.rb +3 -3
- data/lib/better_errors/repl/pry.rb +20 -10
- data/lib/better_errors/repl.rb +6 -4
- data/lib/better_errors/stack_frame.rb +48 -71
- data/lib/better_errors/templates/main.erb +167 -94
- data/lib/better_errors/templates/text.erb +6 -3
- data/lib/better_errors/templates/variable_info.erb +33 -24
- data/lib/better_errors/version.rb +1 -1
- data/lib/better_errors.rb +35 -37
- metadata +150 -46
- data/.travis.yml +0 -6
- data/Rakefile +0 -4
- data/ext/mkrf_conf.rb +0 -11
- data/lib/better_errors/core_ext/exception.rb +0 -21
- 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 -112
- data/spec/better_errors/repl/basic_spec.rb +0 -18
- data/spec/better_errors/repl/pry_spec.rb +0 -36
- data/spec/better_errors/repl/shared_examples.rb +0 -18
- data/spec/better_errors/stack_frame_spec.rb +0 -161
- data/spec/better_errors/support/my_source.rb +0 -20
- data/spec/better_errors_spec.rb +0 -73
- data/spec/spec_helper.rb +0 -7
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "ipaddr"
|
|
3
|
+
require "securerandom"
|
|
3
4
|
require "set"
|
|
5
|
+
require "rack"
|
|
4
6
|
|
|
5
7
|
module BetterErrors
|
|
6
8
|
# Better Errors' error handling middleware. Including this in your middleware
|
|
@@ -32,12 +34,14 @@ module BetterErrors
|
|
|
32
34
|
# Adds an address to the set of IP addresses allowed to access Better
|
|
33
35
|
# Errors.
|
|
34
36
|
def self.allow_ip!(addr)
|
|
35
|
-
ALLOWED_IPS << IPAddr.new(addr)
|
|
37
|
+
ALLOWED_IPS << (addr.is_a?(IPAddr) ? addr : IPAddr.new(addr))
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
allow_ip! "127.0.0.0/8"
|
|
39
41
|
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
|
|
40
42
|
|
|
43
|
+
CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token"
|
|
44
|
+
|
|
41
45
|
# A new instance of BetterErrors::Middleware
|
|
42
46
|
#
|
|
43
47
|
# @param app The Rack app/middleware to wrap with Better Errors
|
|
@@ -62,17 +66,16 @@ module BetterErrors
|
|
|
62
66
|
private
|
|
63
67
|
|
|
64
68
|
def allow_ip?(env)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
ip = IPAddr.new env["REMOTE_ADDR"].split("%").first
|
|
69
|
+
request = Rack::Request.new(env)
|
|
70
|
+
return true unless request.ip and !request.ip.strip.empty?
|
|
71
|
+
ip = IPAddr.new request.ip.split("%").first
|
|
69
72
|
ALLOWED_IPS.any? { |subnet| subnet.include? ip }
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
def better_errors_call(env)
|
|
73
76
|
case env["PATH_INFO"]
|
|
74
|
-
when %r{/__better_errors/(?<
|
|
75
|
-
internal_call
|
|
77
|
+
when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
|
|
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, 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
|
-
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']
|
|
159
|
+
|
|
160
|
+
return not_acceptable_json_response unless request.content_type == 'application/json'
|
|
130
161
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
[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)]]
|
|
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
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'better_errors/exception_hint'
|
|
2
|
+
|
|
3
|
+
# @private
|
|
4
|
+
module BetterErrors
|
|
5
|
+
class RaisedException
|
|
6
|
+
attr_reader :exception, :message, :backtrace, :hint
|
|
7
|
+
|
|
8
|
+
def initialize(exception)
|
|
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.
|
|
15
|
+
exception = exception.cause if exception.cause
|
|
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.
|
|
21
|
+
exception = exception.original_exception
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@exception = exception
|
|
25
|
+
@message = exception.message
|
|
26
|
+
|
|
27
|
+
setup_backtrace
|
|
28
|
+
setup_hint
|
|
29
|
+
massage_syntax_error
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def type
|
|
33
|
+
exception.class
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
def has_bindings?
|
|
38
|
+
exception.respond_to?(:__better_errors_bindings_stack) && exception.__better_errors_bindings_stack.any?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def setup_backtrace
|
|
42
|
+
if has_bindings?
|
|
43
|
+
setup_backtrace_from_bindings
|
|
44
|
+
else
|
|
45
|
+
setup_backtrace_from_backtrace
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def setup_backtrace_from_bindings
|
|
50
|
+
@backtrace = exception.__better_errors_bindings_stack.map { |binding|
|
|
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
|
|
58
|
+
name = binding.frame_description
|
|
59
|
+
StackFrame.new(file, line, name, binding)
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def setup_backtrace_from_backtrace
|
|
64
|
+
@backtrace = (exception.backtrace || []).map { |frame|
|
|
65
|
+
if /\A(?<file>.*?):(?<line>\d+)(:in `(?<name>.*)')?/ =~ frame
|
|
66
|
+
StackFrame.new(file, line.to_i, name)
|
|
67
|
+
end
|
|
68
|
+
}.compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def massage_syntax_error
|
|
72
|
+
case exception.class.to_s
|
|
73
|
+
when "Haml::SyntaxError", "Sprockets::Coffeelint::Error"
|
|
74
|
+
if /\A(.+?):(\d+)/ =~ exception.backtrace.first
|
|
75
|
+
backtrace.unshift(StackFrame.new($1, $2.to_i, ""))
|
|
76
|
+
end
|
|
77
|
+
when "SyntaxError"
|
|
78
|
+
if /\A(.+?):(\d+): (.*)/m =~ exception.message
|
|
79
|
+
backtrace.unshift(StackFrame.new($1, $2.to_i, ""))
|
|
80
|
+
@message = $3
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def setup_hint
|
|
86
|
+
@hint = ExceptionHint.new(exception).hint
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
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,40 +9,50 @@ 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
|
|
39
|
-
@input = Input.new
|
|
40
|
-
@output = Output.new
|
|
43
|
+
@input = BetterErrors::REPL::Pry::Input.new
|
|
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
|
|
@@ -4,67 +4,26 @@ module BetterErrors
|
|
|
4
4
|
# @private
|
|
5
5
|
class StackFrame
|
|
6
6
|
def self.from_exception(exception)
|
|
7
|
-
|
|
8
|
-
list = exception.__better_errors_bindings_stack.map { |binding|
|
|
9
|
-
file = binding.eval "__FILE__"
|
|
10
|
-
line = binding.eval "__LINE__"
|
|
11
|
-
name = binding.frame_description
|
|
12
|
-
StackFrame.new(file, line, name, binding)
|
|
13
|
-
}
|
|
14
|
-
else
|
|
15
|
-
list = (exception.backtrace || []).map { |frame|
|
|
16
|
-
next unless md = /\A(?<file>.*?):(?<line>\d+)(:in `(?<name>.*)')?/.match(frame)
|
|
17
|
-
StackFrame.new(md[:file], md[:line].to_i, md[:name])
|
|
18
|
-
}.compact
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
if syntax_error?(exception)
|
|
22
|
-
if trace = exception.backtrace and trace.first =~ /\A(.*):(\d+)/
|
|
23
|
-
list.unshift StackFrame.new($1, $2.to_i, "")
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
list
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.syntax_error_classes
|
|
31
|
-
# Better Errors may be loaded before some of the gems that provide these
|
|
32
|
-
# classes, so we lazily set up the set of syntax error classes at runtime
|
|
33
|
-
# after everything has hopefully had a chance to load.
|
|
34
|
-
#
|
|
35
|
-
@syntax_error_classes ||= begin
|
|
36
|
-
class_names = %w[
|
|
37
|
-
Haml::SyntaxError
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
Set.new(class_names.map { |klass| eval(klass) rescue nil }.compact)
|
|
41
|
-
end
|
|
7
|
+
RaisedException.new(exception).backtrace
|
|
42
8
|
end
|
|
43
9
|
|
|
44
|
-
def self.syntax_error?(exception)
|
|
45
|
-
exception.is_a?(SyntaxError) || syntax_error_classes.include?(exception.class)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def self.has_binding_stack?(exception)
|
|
49
|
-
exception.respond_to?(:__better_errors_bindings_stack) && exception.__better_errors_bindings_stack.any?
|
|
50
|
-
end
|
|
51
|
-
|
|
52
10
|
attr_reader :filename, :line, :name, :frame_binding
|
|
53
|
-
|
|
11
|
+
|
|
54
12
|
def initialize(filename, line, name, frame_binding = nil)
|
|
55
13
|
@filename = filename
|
|
56
14
|
@line = line
|
|
57
15
|
@name = name
|
|
58
16
|
@frame_binding = frame_binding
|
|
59
|
-
|
|
17
|
+
|
|
60
18
|
set_pretty_method_name if frame_binding
|
|
61
19
|
end
|
|
62
|
-
|
|
20
|
+
|
|
63
21
|
def application?
|
|
64
|
-
root = BetterErrors.application_root
|
|
65
|
-
|
|
22
|
+
if root = BetterErrors.application_root
|
|
23
|
+
filename.index(root) == 0 && filename.index("#{root}/vendor") != 0
|
|
24
|
+
end
|
|
66
25
|
end
|
|
67
|
-
|
|
26
|
+
|
|
68
27
|
def application_path
|
|
69
28
|
filename[(BetterErrors.application_root.length+1)..-1]
|
|
70
29
|
end
|
|
@@ -72,9 +31,9 @@ module BetterErrors
|
|
|
72
31
|
def gem?
|
|
73
32
|
Gem.path.any? { |path| filename.index(path) == 0 }
|
|
74
33
|
end
|
|
75
|
-
|
|
34
|
+
|
|
76
35
|
def gem_path
|
|
77
|
-
if path = Gem.path.detect { |
|
|
36
|
+
if path = Gem.path.detect { |p| filename.index(p) == 0 }
|
|
78
37
|
gem_name_and_version, path = filename.sub("#{path}/gems/", "").split("/", 2)
|
|
79
38
|
/(?<gem_name>.+)-(?<gem_version>[\w.]+)/ =~ gem_name_and_version
|
|
80
39
|
"#{gem_name} (#{gem_version}) #{path}"
|
|
@@ -88,7 +47,7 @@ module BetterErrors
|
|
|
88
47
|
def method_name
|
|
89
48
|
@method_name || @name
|
|
90
49
|
end
|
|
91
|
-
|
|
50
|
+
|
|
92
51
|
def context
|
|
93
52
|
if gem?
|
|
94
53
|
:gem
|
|
@@ -98,7 +57,7 @@ module BetterErrors
|
|
|
98
57
|
:dunno
|
|
99
58
|
end
|
|
100
59
|
end
|
|
101
|
-
|
|
60
|
+
|
|
102
61
|
def pretty_path
|
|
103
62
|
case context
|
|
104
63
|
when :application; application_path
|
|
@@ -106,23 +65,30 @@ module BetterErrors
|
|
|
106
65
|
else filename
|
|
107
66
|
end
|
|
108
67
|
end
|
|
109
|
-
|
|
68
|
+
|
|
110
69
|
def local_variables
|
|
111
70
|
return {} unless frame_binding
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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)
|
|
123
83
|
end
|
|
124
84
|
end
|
|
125
|
-
|
|
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
|
+
|
|
126
92
|
def instance_variables
|
|
127
93
|
return {} unless frame_binding
|
|
128
94
|
Hash[visible_instance_variables.map { |x|
|
|
@@ -131,23 +97,24 @@ module BetterErrors
|
|
|
131
97
|
end
|
|
132
98
|
|
|
133
99
|
def visible_instance_variables
|
|
134
|
-
frame_binding.eval("instance_variables")
|
|
100
|
+
iv = frame_binding.eval("instance_variables")
|
|
101
|
+
return {} unless iv
|
|
102
|
+
|
|
103
|
+
iv - BetterErrors.ignored_instance_variables
|
|
135
104
|
end
|
|
136
105
|
|
|
137
106
|
def to_s
|
|
138
107
|
"#{pretty_path}:#{line}:in `#{name}'"
|
|
139
108
|
end
|
|
140
|
-
|
|
109
|
+
|
|
141
110
|
private
|
|
142
111
|
def set_pretty_method_name
|
|
143
|
-
return if RUBY_VERSION < "2.0.0"
|
|
144
|
-
|
|
145
112
|
name =~ /\A(block (\([^)]+\) )?in )?/
|
|
146
113
|
recv = frame_binding.eval("self")
|
|
147
114
|
|
|
148
115
|
return unless method_name = frame_binding.eval("::Kernel.__method__")
|
|
149
116
|
|
|
150
|
-
if
|
|
117
|
+
if Module === recv
|
|
151
118
|
@class_name = "#{$1}#{recv}"
|
|
152
119
|
@method_name = ".#{method_name}"
|
|
153
120
|
else
|
|
@@ -155,5 +122,15 @@ module BetterErrors
|
|
|
155
122
|
@method_name = "##{method_name}"
|
|
156
123
|
end
|
|
157
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
|
|
158
135
|
end
|
|
159
136
|
end
|