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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.github/workflows/ci.yml +130 -0
  4. data/.github/workflows/release.yml +64 -0
  5. data/.gitignore +10 -5
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +7 -7
  9. data/LICENSE.txt +1 -1
  10. data/README.md +103 -33
  11. data/better_errors.gemspec +26 -8
  12. data/gemfiles/pry010.gemfile +10 -0
  13. data/gemfiles/pry011.gemfile +9 -0
  14. data/gemfiles/pry09.gemfile +9 -0
  15. data/gemfiles/rack.gemfile +8 -0
  16. data/gemfiles/rack_boc.gemfile +9 -0
  17. data/gemfiles/rails42.gemfile +10 -0
  18. data/gemfiles/rails42_boc.gemfile +11 -0
  19. data/gemfiles/rails42_haml.gemfile +11 -0
  20. data/gemfiles/rails50.gemfile +9 -0
  21. data/gemfiles/rails50_boc.gemfile +10 -0
  22. data/gemfiles/rails50_haml.gemfile +10 -0
  23. data/gemfiles/rails51.gemfile +9 -0
  24. data/gemfiles/rails51_boc.gemfile +10 -0
  25. data/gemfiles/rails51_haml.gemfile +10 -0
  26. data/gemfiles/rails52.gemfile +9 -0
  27. data/gemfiles/rails52_boc.gemfile +10 -0
  28. data/gemfiles/rails52_haml.gemfile +10 -0
  29. data/gemfiles/rails60.gemfile +8 -0
  30. data/gemfiles/rails60_boc.gemfile +9 -0
  31. data/gemfiles/rails60_haml.gemfile +9 -0
  32. data/lib/better_errors/code_formatter/html.rb +1 -1
  33. data/lib/better_errors/code_formatter.rb +7 -7
  34. data/lib/better_errors/editor.rb +99 -0
  35. data/lib/better_errors/error_page.rb +83 -49
  36. data/lib/better_errors/exception_extension.rb +17 -0
  37. data/lib/better_errors/exception_hint.rb +29 -0
  38. data/lib/better_errors/inspectable_value.rb +45 -0
  39. data/lib/better_errors/middleware.rb +101 -21
  40. data/lib/better_errors/raised_exception.rb +89 -0
  41. data/lib/better_errors/repl/basic.rb +3 -3
  42. data/lib/better_errors/repl/pry.rb +20 -10
  43. data/lib/better_errors/repl.rb +6 -4
  44. data/lib/better_errors/stack_frame.rb +48 -71
  45. data/lib/better_errors/templates/main.erb +167 -94
  46. data/lib/better_errors/templates/text.erb +6 -3
  47. data/lib/better_errors/templates/variable_info.erb +33 -24
  48. data/lib/better_errors/version.rb +1 -1
  49. data/lib/better_errors.rb +35 -37
  50. metadata +150 -46
  51. data/.travis.yml +0 -6
  52. data/Rakefile +0 -4
  53. data/ext/mkrf_conf.rb +0 -11
  54. data/lib/better_errors/core_ext/exception.rb +0 -21
  55. data/spec/better_errors/code_formatter_spec.rb +0 -92
  56. data/spec/better_errors/error_page_spec.rb +0 -76
  57. data/spec/better_errors/middleware_spec.rb +0 -112
  58. data/spec/better_errors/repl/basic_spec.rb +0 -18
  59. data/spec/better_errors/repl/pry_spec.rb +0 -36
  60. data/spec/better_errors/repl/shared_examples.rb +0 -18
  61. data/spec/better_errors/stack_frame_spec.rb +0 -161
  62. data/spec/better_errors/support/my_source.rb +0 -20
  63. data/spec/better_errors_spec.rb +0 -73
  64. 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
- # REMOTE_ADDR is not in the rack spec, so some application servers do
66
- # not provide it.
67
- return true unless env["REMOTE_ADDR"] and !env["REMOTE_ADDR"].strip.empty?
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/(?<oid>-?\d+)/(?<method>\w+)\z}
75
- internal_call env, $~
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? 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, 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
- !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.class} - #{@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[:oid].to_i != @error_page.object_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']
159
+
160
+ return not_acceptable_json_response unless request.content_type == 'application/json'
130
161
 
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)]]
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 = {}
@@ -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
@@ -4,67 +4,26 @@ module BetterErrors
4
4
  # @private
5
5
  class StackFrame
6
6
  def self.from_exception(exception)
7
- if has_binding_stack?(exception)
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
- filename.index(root) == 0 if root
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 { |path| filename.index(path) == 0 }
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
- frame_binding.eval("local_variables").each_with_object({}) do |name, hash|
113
- begin
114
- if defined?(frame_binding.local_variable_get)
115
- hash[name] = frame_binding.local_variable_get(name)
116
- else
117
- hash[name] = frame_binding.eval(name.to_s)
118
- end
119
- rescue NameError => e
120
- # local_variables sometimes returns broken variables.
121
- # https://bugs.ruby-lang.org/issues/7536
122
- 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)
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") - BetterErrors.ignored_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 Kernel.instance_method(:is_a?).bind(recv).call Module
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