better_errors 2.0.0 → 2.8.0

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.
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