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
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 5.1.0"
4
+ gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
5
+ gem "haml"
6
+
7
+ gem 'simplecov', require: false
8
+ gem 'simplecov-lcov', require: false
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 5.2.0"
4
+ gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
5
+
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 5.2.0"
4
+ gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
5
+ gem "binding_of_caller"
6
+
7
+ gem 'simplecov', require: false
8
+ gem 'simplecov-lcov', require: false
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 5.2.0"
4
+ gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
5
+ gem "haml"
6
+
7
+ gem 'simplecov', require: false
8
+ gem 'simplecov-lcov', require: false
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 6.0.0"
4
+
5
+ gem 'simplecov', require: false
6
+ gem 'simplecov-lcov', require: false
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 6.0.0"
4
+ gem "binding_of_caller"
5
+
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 6.0.0"
4
+ gem "haml"
5
+
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
8
+
9
+ gemspec path: "../"
@@ -11,7 +11,7 @@ module BetterErrors
11
11
  sprintf '<pre class="%s">%s</pre>', class_name, str
12
12
  }
13
13
  end
14
-
14
+
15
15
  def formatted_nums
16
16
  each_line_of(highlighted_lines) { |highlight, current_line, str|
17
17
  class_name = highlight ? "highlight" : ""
@@ -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
@@ -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
@@ -1,5 +1,6 @@
1
1
  require "cgi"
2
2
  require "json"
3
+ require "securerandom"
3
4
 
4
5
  module BetterErrors
5
6
  # @private
@@ -7,65 +8,95 @@ module BetterErrors
7
8
  def self.template_path(template_name)
8
9
  File.expand_path("../templates/#{template_name}.erb", __FILE__)
9
10
  end
10
-
11
+
11
12
  def self.template(template_name)
12
- Erubis::EscapedEruby.new(File.read(template_path(template_name)))
13
+ Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
13
14
  end
14
-
15
+
15
16
  attr_reader :exception, :env, :repls
16
-
17
+
17
18
  def initialize(exception, env)
18
- @exception = real_exception(exception)
19
+ @exception = RaisedException.new(exception)
19
20
  @env = env
20
21
  @start_time = Time.now.to_f
21
22
  @repls = []
22
23
  end
23
-
24
- def render(template_name = "main")
25
- self.class.template(template_name).result binding
24
+
25
+ def id
26
+ @id ||= SecureRandom.hex(8)
27
+ end
28
+
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
26
36
  end
27
-
37
+
28
38
  def do_variables(opts)
29
39
  index = opts["index"].to_i
30
40
  @frame = backtrace_frames[index]
31
41
  @var_start_time = Time.now.to_f
32
42
  { html: render("variable_info") }
33
43
  end
34
-
44
+
35
45
  def do_eval(opts)
36
46
  index = opts["index"].to_i
37
47
  code = opts["source"]
38
-
39
- unless binding = backtrace_frames[index].frame_binding
48
+
49
+ unless (binding = backtrace_frames[index].frame_binding)
40
50
  return { error: "REPL unavailable in this stack frame" }
41
51
  end
42
-
43
- result, prompt, prefilled_input =
44
- (@repls[index] ||= REPL.provider.new(binding)).send_input(code)
45
-
46
- { result: result,
47
- prompt: prompt,
48
- prefilled_input: prefilled_input,
49
- highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
52
+
53
+ @repls[index] ||= REPL.provider.new(binding, exception)
54
+
55
+ eval_and_respond(index, code)
50
56
  end
51
57
 
52
58
  def backtrace_frames
53
- @backtrace_frames ||= StackFrame.from_exception(exception)
59
+ exception.backtrace
60
+ end
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 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
54
84
  end
55
85
 
56
86
  def application_frames
57
- backtrace_frames.select { |frame| frame.context == :application }
87
+ backtrace_frames.select(&:application?)
58
88
  end
59
89
 
60
90
  def first_frame
61
- backtrace_frames.detect { |frame| frame.context == :application } || backtrace_frames.first
91
+ application_frames.first || backtrace_frames.first
62
92
  end
63
-
64
- private
93
+
94
+ private
95
+
65
96
  def editor_url(frame)
66
- BetterErrors.editor[frame.filename, frame.line]
97
+ BetterErrors.editor.url(frame.filename, frame.line)
67
98
  end
68
-
99
+
69
100
  def rack_session
70
101
  env['rack.session']
71
102
  end
@@ -77,31 +108,15 @@ module BetterErrors
77
108
  def uri_prefix
78
109
  env["SCRIPT_NAME"] || ""
79
110
  end
80
-
81
- def exception_message
82
- if exception.is_a?(SyntaxError) && exception.message =~ /\A.*:\d*: (.*)$/
83
- $1
84
- else
85
- exception.message
86
- end
87
- end
88
111
 
89
- def real_exception(exception)
90
- if exception.respond_to? :original_exception
91
- exception.original_exception
92
- else
93
- exception
94
- end
95
- end
96
-
97
112
  def request_path
98
113
  env["PATH_INFO"]
99
114
  end
100
-
115
+
101
116
  def html_formatted_code_block(frame)
102
117
  CodeFormatter::HTML.new(frame.filename, frame.line).output
103
118
  end
104
-
119
+
105
120
  def text_formatted_code_block(frame)
106
121
  CodeFormatter::Text.new(frame.filename, frame.line).output
107
122
  end
@@ -111,11 +126,30 @@ module BetterErrors
111
126
  end
112
127
 
113
128
  def inspect_value(obj)
114
- CGI.escapeHTML(obj.inspect)
115
- rescue NoMethodError
116
- "<span class='unsupported'>(object doesn't support inspect)</span>"
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>"
117
140
  rescue Exception => e
118
- "<span class='unsupported'>(exception was raised in inspect)</span>"
141
+ "<span class='unsupported'>(exception #{CGI.escapeHTML(e.class.to_s)} was raised in inspect)</span>"
142
+ end
143
+
144
+ def eval_and_respond(index, code)
145
+ result, prompt, prefilled_input = @repls[index].send_input(code)
146
+
147
+ {
148
+ highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil),
149
+ prefilled_input: prefilled_input,
150
+ prompt: prompt,
151
+ result: result
152
+ }
119
153
  end
120
154
  end
121
155
  end
@@ -0,0 +1,17 @@
1
+ module BetterErrors
2
+ module ExceptionExtension
3
+ prepend_features Exception
4
+
5
+ def set_backtrace(*)
6
+ if caller_locations.none? { |loc| loc.path == __FILE__ }
7
+ @__better_errors_bindings_stack = ::Kernel.binding.callers.drop(1)
8
+ end
9
+
10
+ super
11
+ end
12
+
13
+ def __better_errors_bindings_stack
14
+ @__better_errors_bindings_stack || []
15
+ end
16
+ end
17
+ end
@@ -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