better_errors 2.8.2 → 2.10.0.beta2

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +142 -0
  3. data/.github/workflows/release.yml +68 -0
  4. data/.gitignore +4 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +6 -1
  7. data/README.md +2 -2
  8. data/better_errors.gemspec +5 -4
  9. data/gemfiles/pry010.gemfile +2 -1
  10. data/gemfiles/pry011.gemfile +2 -1
  11. data/gemfiles/pry09.gemfile +2 -1
  12. data/gemfiles/rack.gemfile +2 -1
  13. data/gemfiles/rack_boc.gemfile +2 -1
  14. data/gemfiles/rails42.gemfile +2 -1
  15. data/gemfiles/rails42_boc.gemfile +2 -1
  16. data/gemfiles/rails42_haml.gemfile +2 -1
  17. data/gemfiles/rails50.gemfile +2 -1
  18. data/gemfiles/rails50_boc.gemfile +2 -1
  19. data/gemfiles/rails50_haml.gemfile +2 -1
  20. data/gemfiles/rails51.gemfile +2 -1
  21. data/gemfiles/rails51_boc.gemfile +2 -1
  22. data/gemfiles/rails51_haml.gemfile +2 -1
  23. data/gemfiles/rails52.gemfile +2 -1
  24. data/gemfiles/rails52_boc.gemfile +2 -1
  25. data/gemfiles/rails52_haml.gemfile +2 -1
  26. data/gemfiles/rails60.gemfile +2 -1
  27. data/gemfiles/rails60_boc.gemfile +2 -1
  28. data/gemfiles/rails60_haml.gemfile +2 -1
  29. data/gemfiles/rails61.gemfile +8 -0
  30. data/gemfiles/rails61_boc.gemfile +9 -0
  31. data/gemfiles/rails61_haml.gemfile +9 -0
  32. data/lib/better_errors.rb +15 -35
  33. data/lib/better_errors/code_formatter.rb +16 -27
  34. data/lib/better_errors/code_formatter/html.rb +15 -1
  35. data/lib/better_errors/editor.rb +103 -0
  36. data/lib/better_errors/error_page.rb +33 -15
  37. data/lib/better_errors/error_page_style.rb +30 -0
  38. data/lib/better_errors/exception_hint.rb +29 -0
  39. data/lib/better_errors/middleware.rb +20 -4
  40. data/lib/better_errors/raised_exception.rb +8 -1
  41. data/lib/better_errors/templates/main.erb +80 -717
  42. data/lib/better_errors/templates/text.erb +6 -3
  43. data/lib/better_errors/templates/variable_info.erb +18 -15
  44. data/lib/better_errors/version.rb +2 -1
  45. metadata +30 -7
  46. data/.travis.yml +0 -111
@@ -2,6 +2,7 @@ source "https://rubygems.org"
2
2
 
3
3
  gem "rails", "~> 6.0.0"
4
4
 
5
- gem 'coveralls', require: false
5
+ gem 'simplecov', require: false
6
+ gem 'simplecov-lcov', require: false
6
7
 
7
8
  gemspec path: "../"
@@ -3,6 +3,7 @@ source "https://rubygems.org"
3
3
  gem "rails", "~> 6.0.0"
4
4
  gem "binding_of_caller"
5
5
 
6
- gem 'coveralls', require: false
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
7
8
 
8
9
  gemspec path: "../"
@@ -3,6 +3,7 @@ source "https://rubygems.org"
3
3
  gem "rails", "~> 6.0.0"
4
4
  gem "haml"
5
5
 
6
- gem 'coveralls', require: false
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
7
8
 
8
9
  gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 6.1.0rc"
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.1.0rc"
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.1.0rc"
4
+ gem "haml"
5
+
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
8
+
9
+ gemspec path: "../"
@@ -1,8 +1,8 @@
1
1
  require "pp"
2
2
  require "erubi"
3
- require "coderay"
4
3
  require "uri"
5
4
 
5
+ require "better_errors/version"
6
6
  require "better_errors/code_formatter"
7
7
  require "better_errors/inspectable_value"
8
8
  require "better_errors/error_page"
@@ -10,21 +10,9 @@ require "better_errors/middleware"
10
10
  require "better_errors/raised_exception"
11
11
  require "better_errors/repl"
12
12
  require "better_errors/stack_frame"
13
- require "better_errors/version"
13
+ require "better_errors/editor"
14
14
 
15
15
  module BetterErrors
16
- POSSIBLE_EDITOR_PRESETS = [
17
- { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
18
- { symbols: [:macvim, :mvim], sniff: /vim/i, url: proc { |file, line| "mvim://open?url=file://#{file}&line=#{line}" } },
19
- { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
20
- { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
21
- { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
22
- { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
23
- { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
24
- { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
25
- { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
26
- ]
27
-
28
16
  class << self
29
17
  # The path to the root of the application. Better Errors uses this property
30
18
  # to determine if a file in a backtrace should be considered an application
@@ -64,17 +52,18 @@ module BetterErrors
64
52
  @maximum_variable_inspect_size = 100_000
65
53
  @ignored_classes = ['ActionDispatch::Request', 'ActionDispatch::Response']
66
54
 
67
- # Returns a proc, which when called with a filename and line number argument,
55
+ # Returns an object which responds to #url, which when called with
56
+ # a filename and line number argument,
68
57
  # returns a URL to open the filename and line in the selected editor.
69
58
  #
70
59
  # Generates TextMate URLs by default.
71
60
  #
72
- # BetterErrors.editor["/some/file", 123]
61
+ # BetterErrors.editor.url("/some/file", 123)
73
62
  # # => txmt://open?url=file:///some/file&line=123
74
63
  #
75
64
  # @return [Proc]
76
65
  def self.editor
77
- @editor
66
+ @editor ||= default_editor
78
67
  end
79
68
 
80
69
  # Configures how Better Errors generates open-in-editor URLs.
@@ -115,20 +104,15 @@ module BetterErrors
115
104
  # @param [Proc] proc
116
105
  #
117
106
  def self.editor=(editor)
118
- POSSIBLE_EDITOR_PRESETS.each do |config|
119
- if config[:symbols].include?(editor)
120
- return self.editor = config[:url]
121
- end
122
- end
123
-
124
- if editor.is_a? String
125
- self.editor = proc { |file, line| editor % { file: URI.encode_www_form_component(file), line: line } }
107
+ if editor.is_a? Symbol
108
+ @editor = Editor.editor_from_symbol(editor)
109
+ raise(ArgumentError, "Symbol #{editor} is not a symbol in the list of supported errors.") unless editor
110
+ elsif editor.is_a? String
111
+ @editor = Editor.for_formatting_string(editor)
112
+ elsif editor.respond_to? :call
113
+ @editor = Editor.for_proc(editor)
126
114
  else
127
- if editor.respond_to? :call
128
- @editor = editor
129
- else
130
- raise TypeError, "Expected editor to be a valid editor key, a format string or a callable."
131
- end
115
+ raise ArgumentError, "Expected editor to be a valid editor key, a format string or a callable."
132
116
  end
133
117
  end
134
118
 
@@ -145,12 +129,8 @@ module BetterErrors
145
129
  #
146
130
  # @return [Symbol]
147
131
  def self.default_editor
148
- POSSIBLE_EDITOR_PRESETS.detect(-> { {} }) { |config|
149
- ENV["EDITOR"] =~ config[:sniff]
150
- }[:url] || :textmate
132
+ Editor.default_editor
151
133
  end
152
-
153
- BetterErrors.editor = default_editor
154
134
  end
155
135
 
156
136
  begin
@@ -4,14 +4,6 @@ module BetterErrors
4
4
  require "better_errors/code_formatter/html"
5
5
  require "better_errors/code_formatter/text"
6
6
 
7
- FILE_TYPES = {
8
- ".rb" => :ruby,
9
- "" => :ruby,
10
- ".html" => :html,
11
- ".erb" => :erb,
12
- ".haml" => :haml
13
- }
14
-
15
7
  attr_reader :filename, :line, :context
16
8
 
17
9
  def initialize(filename, line, context = 5)
@@ -26,13 +18,21 @@ module BetterErrors
26
18
  source_unavailable
27
19
  end
28
20
 
29
- def formatted_code
30
- formatted_lines.join
21
+ def line_range
22
+ min = [line - context, 1].max
23
+ max = [line + context, source_lines.count].min
24
+ min..max
31
25
  end
32
26
 
33
- def coderay_scanner
34
- ext = File.extname(filename)
35
- FILE_TYPES[ext] || :text
27
+ def context_lines
28
+ range = line_range
29
+ source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
30
+ end
31
+
32
+ private
33
+
34
+ def formatted_code
35
+ formatted_lines.join
36
36
  end
37
37
 
38
38
  def each_line_of(lines, &blk)
@@ -41,23 +41,12 @@ module BetterErrors
41
41
  }
42
42
  end
43
43
 
44
- def highlighted_lines
45
- CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
46
- end
47
-
48
- def context_lines
49
- range = line_range
50
- source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
44
+ def source
45
+ @source ||= File.read(filename)
51
46
  end
52
47
 
53
48
  def source_lines
54
- @source_lines ||= File.readlines(filename)
55
- end
56
-
57
- def line_range
58
- min = [line - context, 1].max
59
- max = [line + context, source_lines.count].min
60
- min..max
49
+ @source_lines ||= source.lines
61
50
  end
62
51
  end
63
52
  end
@@ -1,3 +1,5 @@
1
+ require "rouge"
2
+
1
3
  module BetterErrors
2
4
  # @private
3
5
  class CodeFormatter::HTML < CodeFormatter
@@ -20,7 +22,19 @@ module BetterErrors
20
22
  end
21
23
 
22
24
  def formatted_code
23
- %{<div class="code_linenums">#{formatted_nums.join}</div><div class="code">#{super}</div>}
25
+ %{
26
+ <div class="code_linenums">#{formatted_nums.join}</div>
27
+ <div class="code"><div class='code-wrapper'>#{super}</div></div>
28
+ }
29
+ end
30
+
31
+ def rouge_lexer
32
+ Rouge::Lexer.guess(filename: filename, source: source) { Rouge::Lexers::Ruby }
24
33
  end
34
+
35
+ def highlighted_lines
36
+ Rouge::Formatters::HTML.new.format(rouge_lexer.lex(context_lines.join)).lines
37
+ end
38
+
25
39
  end
26
40
  end
@@ -0,0 +1,103 @@
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
+ def scheme
88
+ url('/fake', 42).sub(/:.*/, ':')
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :url_proc
94
+
95
+ def virtual_path
96
+ @virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH']
97
+ end
98
+
99
+ def host_path
100
+ @host_path ||= ENV['BETTER_ERRORS_HOST_PATH']
101
+ end
102
+ end
103
+ end
@@ -1,10 +1,14 @@
1
1
  require "cgi"
2
2
  require "json"
3
3
  require "securerandom"
4
+ require "rouge"
5
+ require "better_errors/error_page_style"
4
6
 
5
7
  module BetterErrors
6
8
  # @private
7
9
  class ErrorPage
10
+ VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time)
11
+
8
12
  def self.template_path(template_name)
9
13
  File.expand_path("../templates/#{template_name}.erb", __FILE__)
10
14
  end
@@ -13,6 +17,15 @@ module BetterErrors
13
17
  Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
14
18
  end
15
19
 
20
+ def self.render_template(template_name, locals)
21
+ locals.send(:eval, self.template(template_name).src)
22
+ rescue => e
23
+ # Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
24
+ # We don't know the line number, so just injecting the template path has to be enough.
25
+ e.backtrace.unshift "#{self.template_path(template_name)}:0"
26
+ raise
27
+ end
28
+
16
29
  attr_reader :exception, :env, :repls
17
30
 
18
31
  def initialize(exception, env)
@@ -26,20 +39,21 @@ module BetterErrors
26
39
  @id ||= SecureRandom.hex(8)
27
40
  end
28
41
 
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
42
+ def render_main(csrf_token, csp_nonce)
43
+ frame = backtrace_frames[0]
44
+ first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
45
+ self.class.render_template('main', binding)
46
+ end
47
+
48
+ def render_text
49
+ self.class.render_template('text', binding)
36
50
  end
37
51
 
38
52
  def do_variables(opts)
39
53
  index = opts["index"].to_i
40
- @frame = backtrace_frames[index]
41
- @var_start_time = Time.now.to_f
42
- { html: render("variable_info") }
54
+ frame = backtrace_frames[index]
55
+ variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
56
+ { html: self.class.render_template("variable_info", variable_info) }
43
57
  end
44
58
 
45
59
  def do_eval(opts)
@@ -67,6 +81,10 @@ module BetterErrors
67
81
  exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
68
82
  end
69
83
 
84
+ def exception_hint
85
+ exception.hint
86
+ end
87
+
70
88
  def active_support_actions
71
89
  return [] unless defined?(ActiveSupport::ActionableError)
72
90
 
@@ -90,7 +108,7 @@ module BetterErrors
90
108
  private
91
109
 
92
110
  def editor_url(frame)
93
- BetterErrors.editor[frame.filename, frame.line]
111
+ BetterErrors.editor.url(frame.filename, frame.line)
94
112
  end
95
113
 
96
114
  def rack_session
@@ -109,11 +127,11 @@ module BetterErrors
109
127
  env["PATH_INFO"]
110
128
  end
111
129
 
112
- def html_formatted_code_block(frame)
130
+ def self.html_formatted_code_block(frame)
113
131
  CodeFormatter::HTML.new(frame.filename, frame.line).output
114
132
  end
115
133
 
116
- def text_formatted_code_block(frame)
134
+ def self.text_formatted_code_block(frame)
117
135
  CodeFormatter::Text.new(frame.filename, frame.line).output
118
136
  end
119
137
 
@@ -121,7 +139,7 @@ module BetterErrors
121
139
  str + "\n" + char*str.size
122
140
  end
123
141
 
124
- def inspect_value(obj)
142
+ def self.inspect_value(obj)
125
143
  if BetterErrors.ignored_classes.include? obj.class.name
126
144
  "<span class='unsupported'>(Instance of ignored class. "\
127
145
  "#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
@@ -141,7 +159,7 @@ module BetterErrors
141
159
  result, prompt, prefilled_input = @repls[index].send_input(code)
142
160
 
143
161
  {
144
- highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil),
162
+ highlighted_input: Rouge::Formatters::HTML.new.format(Rouge::Lexers::Ruby.lex(code)),
145
163
  prefilled_input: prefilled_input,
146
164
  prompt: prompt,
147
165
  result: result