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.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +3 -0
  4. data/.travis.yml +96 -2
  5. data/CHANGELOG.md +1 -1
  6. data/Gemfile +2 -7
  7. data/LICENSE.txt +1 -1
  8. data/README.md +99 -39
  9. data/better_errors.gemspec +23 -4
  10. data/gemfiles/pry010.gemfile +9 -0
  11. data/gemfiles/pry011.gemfile +8 -0
  12. data/gemfiles/pry09.gemfile +8 -0
  13. data/gemfiles/rack.gemfile +7 -0
  14. data/gemfiles/rack_boc.gemfile +8 -0
  15. data/gemfiles/rails42.gemfile +9 -0
  16. data/gemfiles/rails42_boc.gemfile +10 -0
  17. data/gemfiles/rails42_haml.gemfile +10 -0
  18. data/gemfiles/rails50.gemfile +8 -0
  19. data/gemfiles/rails50_boc.gemfile +9 -0
  20. data/gemfiles/rails50_haml.gemfile +9 -0
  21. data/gemfiles/rails51.gemfile +8 -0
  22. data/gemfiles/rails51_boc.gemfile +9 -0
  23. data/gemfiles/rails51_haml.gemfile +9 -0
  24. data/gemfiles/rails52.gemfile +8 -0
  25. data/gemfiles/rails52_boc.gemfile +9 -0
  26. data/gemfiles/rails52_haml.gemfile +9 -0
  27. data/gemfiles/rails60.gemfile +7 -0
  28. data/gemfiles/rails60_boc.gemfile +8 -0
  29. data/gemfiles/rails60_haml.gemfile +8 -0
  30. data/lib/better_errors/code_formatter/html.rb +1 -1
  31. data/lib/better_errors/code_formatter.rb +7 -7
  32. data/lib/better_errors/error_page.rb +56 -15
  33. data/lib/better_errors/inspectable_value.rb +45 -0
  34. data/lib/better_errors/middleware.rb +96 -16
  35. data/lib/better_errors/raised_exception.rb +13 -3
  36. data/lib/better_errors/repl/basic.rb +3 -3
  37. data/lib/better_errors/repl/pry.rb +18 -8
  38. data/lib/better_errors/repl.rb +6 -4
  39. data/lib/better_errors/stack_frame.rb +33 -8
  40. data/lib/better_errors/templates/main.erb +71 -34
  41. data/lib/better_errors/templates/text.erb +2 -2
  42. data/lib/better_errors/templates/variable_info.erb +32 -23
  43. data/lib/better_errors/version.rb +1 -1
  44. data/lib/better_errors.rb +21 -3
  45. metadata +118 -35
  46. data/Rakefile +0 -13
  47. data/spec/better_errors/code_formatter_spec.rb +0 -92
  48. data/spec/better_errors/error_page_spec.rb +0 -76
  49. data/spec/better_errors/middleware_spec.rb +0 -154
  50. data/spec/better_errors/raised_exception_spec.rb +0 -52
  51. data/spec/better_errors/repl/basic_spec.rb +0 -18
  52. data/spec/better_errors/repl/pry_spec.rb +0 -40
  53. data/spec/better_errors/repl/shared_examples.rb +0 -18
  54. data/spec/better_errors/stack_frame_spec.rb +0 -157
  55. data/spec/better_errors/support/my_source.rb +0 -20
  56. data/spec/better_errors_spec.rb +0 -73
  57. data/spec/spec_helper.rb +0 -5
  58. 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
- Erubis::EscapedEruby.new(File.read(template_path(template_name)))
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).result binding
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
- result, prompt, prefilled_input =
49
- (@repls[index] ||= REPL.provider.new(binding)).send_input(code)
53
+ @repls[index] ||= REPL.provider.new(binding, exception)
50
54
 
51
- { result: result,
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
- private
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
- CGI.escapeHTML(obj.inspect)
104
- rescue NoMethodError
105
- "<span class='unsupported'>(object doesn't support inspect)</span>"
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 env, $~
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? ActionDispatch::ExceptionWrapper
109
+ if defined?(ActionDispatch::ExceptionWrapper) && exception
104
110
  status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
105
111
  end
106
112
 
107
- [status_code, { "Content-Type" => "text/#{type}; charset=utf-8" }, [content]]
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
- !env["HTTP_ACCEPT"].to_s.include?('html')
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.exception.type} - #{@error_page.exception.message}:\n"
119
- @error_page.backtrace_frames.each do |frame|
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 internal_call(env, opts)
127
- if opts[:id] != @error_page.id
128
- return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]]
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
- env["rack.input"].rewind
132
- response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read))
133
- [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
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
- file = binding.eval "__FILE__"
38
- line = binding.eval "__LINE__"
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 "Haml::SyntaxError"
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 = {}
@@ -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 provider[:impl]
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 { |path| filename.index(path) == 0 }
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
- frame_binding.eval("local_variables").each_with_object({}) do |name, hash|
72
- if defined?(frame_binding.local_variable_get)
73
- hash[name] = frame_binding.local_variable_get(name)
74
- else
75
- hash[name] = frame_binding.eval(name.to_s)
76
- end
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") - BetterErrors.ignored_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