better_errors 0.0.2 → 0.2.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.
data/README.md CHANGED
@@ -1,28 +1,33 @@
1
1
  # Better Errors
2
2
 
3
- Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails.
3
+ Better Errors replaces the standard Rails error page with a much better and more useful error page. It is also usable outside of Rails in any Rack app as Rack middleware.
4
4
 
5
- ![image](http://i.imgur.com/O9anD.png)
5
+ ![image](http://i.imgur.com/zYOXF.png)
6
6
 
7
7
  ## Features
8
8
 
9
9
  * Full stack trace
10
10
  * Source code inspection for all stack frames (with highlighting)
11
11
  * Local and instance variable inspection
12
+ * Live REPL on every stack frame
12
13
 
13
14
  ## Installation
14
15
 
15
16
  Add this line to your application's Gemfile (under the **development** group):
16
17
 
17
- gem 'better_errors'
18
-
19
- And then execute:
18
+ ```ruby
19
+ group :development do
20
+ gem "better_errors"
21
+ end
22
+ ```
20
23
 
21
- $ bundle
24
+ If you would like to use Better Errors' **advanced features** (REPL, local/instance variable inspection, pretty stack frame names), you need to add the [`binding_of_caller`](https://github.com/banister/binding_of_caller) gem to your Gemfile:
22
25
 
23
- Or install it yourself as:
26
+ ```ruby
27
+ gem "binding_of_caller"
28
+ ```
24
29
 
25
- $ gem install better_errors
30
+ This is an optional dependency however, and Better Errors will work without it.
26
31
 
27
32
  ## Usage
28
33
 
@@ -44,6 +49,19 @@ get "/" do
44
49
  end
45
50
  ```
46
51
 
52
+ ## Compatibility
53
+
54
+ * **Supported**
55
+ * MRI 1.9.2, 1.9.3
56
+ * JRuby (1.9 mode) - *advanced features unsupported*
57
+ * Rubinius (1.9 mode) - *advanced features unsupported*
58
+ * **Coming soon**
59
+ * MRI 2.0.0 - the official API for grabbing caller bindings is slated for MRI 2.0.0, but it has not been implemented yet
60
+
61
+ ## Known issues
62
+
63
+ * Calling `yield` from the REPL segfaults MRI 1.9.x.
64
+
47
65
  ## Contributing
48
66
 
49
67
  1. Fork it
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.version = BetterErrors::VERSION
9
9
  s.authors = ["Charlie Somerville"]
10
10
  s.email = ["charlie@charliesomerville.com"]
11
- s.description = %q{Better Errors gives Rails a better error page.}
12
- s.summary = %q{Better Errors gives Rails a better error page}
11
+ s.description = %q{Provides a better error page for Rails and other Rack apps. Includes source code inspection, a live REPL and local/instance variable inspection for all stack frames.}
12
+ s.summary = %q{Better error page for Rails and other Rack apps}
13
13
  s.homepage = "https://github.com/charliesome/better_errors"
14
14
  s.license = "MIT"
15
15
 
@@ -19,8 +19,10 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_development_dependency "rake"
21
21
 
22
- s.add_dependency "erubis"
23
- s.add_dependency "coderay"
24
- # optional dependency:
22
+ s.add_dependency "erubis", ">= 2.7.0"
23
+ s.add_dependency "coderay", ">= 1.0.0"
24
+
25
+ # optional dependencies:
25
26
  # s.add_dependency "binding_of_caller"
27
+ # s.add_dependency "pry"
26
28
  end
@@ -0,0 +1,60 @@
1
+ module BetterErrors
2
+ class CodeFormatter
3
+ FILE_TYPES = {
4
+ ".rb" => :ruby,
5
+ "" => :ruby,
6
+ ".html" => :html,
7
+ ".erb" => :erb,
8
+ ".haml" => :haml
9
+ }
10
+
11
+ attr_reader :filename, :line, :context
12
+
13
+ def initialize(filename, line, context = 5)
14
+ @filename = filename
15
+ @line = line
16
+ @context = context
17
+ end
18
+
19
+ def html
20
+ %{<div class="code">#{formatted_lines.join}</div>}
21
+ rescue Errno::ENOENT, Errno::EINVAL
22
+ source_unavailable
23
+ end
24
+
25
+ def source_unavailable
26
+ "<p class='unavailable'>Source unavailable</p>"
27
+ end
28
+
29
+ def coderay_scanner
30
+ ext = File.extname(filename)
31
+ FILE_TYPES[ext] || :text
32
+ end
33
+
34
+ def formatted_lines
35
+ line_range.zip(highlighted_lines).map do |current_line, str|
36
+ class_name = current_line == line ? "highlight" : ""
37
+ sprintf '<pre class="%s">%5d %s</pre>', class_name, current_line, str
38
+ end
39
+ end
40
+
41
+ def highlighted_lines
42
+ CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
43
+ end
44
+
45
+ def context_lines
46
+ range = line_range
47
+ source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
48
+ end
49
+
50
+ def source_lines
51
+ @source_lines ||= File.readlines(filename)
52
+ end
53
+
54
+ def line_range
55
+ min = [line - context, 1].max
56
+ max = [line + context, source_lines.count].min
57
+ min..max
58
+ end
59
+ end
60
+ end
@@ -1,20 +1,21 @@
1
1
  class Exception
2
- attr_reader :__better_errors_bindings_stack
3
-
4
2
  original_initialize = instance_method(:initialize)
5
3
 
6
- define_method :initialize do |*args|
7
- unless Thread.current[:__better_errors_exception_lock]
8
- Thread.current[:__better_errors_exception_lock] = true
9
- begin
10
- @__better_errors_bindings_stack = []
11
- 2.upto(caller.size) do |index|
12
- @__better_errors_bindings_stack << binding.of_caller(index) rescue break
4
+ if BetterErrors.binding_of_caller_available?
5
+ define_method :initialize do |*args|
6
+ unless Thread.current[:__better_errors_exception_lock]
7
+ Thread.current[:__better_errors_exception_lock] = true
8
+ begin
9
+ @__better_errors_bindings_stack = binding.callers.drop(1)
10
+ ensure
11
+ Thread.current[:__better_errors_exception_lock] = false
13
12
  end
14
- ensure
15
- Thread.current[:__better_errors_exception_lock] = false
16
13
  end
14
+ original_initialize.bind(self).call(*args)
17
15
  end
18
- original_initialize.bind(self).call(*args)
19
16
  end
20
- end
17
+
18
+ def __better_errors_bindings_stack
19
+ @__better_errors_bindings_stack || []
20
+ end
21
+ end
@@ -2,26 +2,62 @@ require "json"
2
2
 
3
3
  module BetterErrors
4
4
  class ErrorPage
5
- def self.template_path
6
- __FILE__.gsub(/\.rb$/, ".erb")
5
+ def self.template_path(template_name)
6
+ File.expand_path("../templates/#{template_name}.erb", __FILE__)
7
7
  end
8
8
 
9
- def self.template
10
- Erubis::EscapedEruby.new(File.read(template_path))
9
+ def self.template(template_name)
10
+ Erubis::EscapedEruby.new(File.read(template_path(template_name)))
11
11
  end
12
12
 
13
- attr_reader :exception, :env
13
+ attr_reader :exception, :env, :repls
14
14
 
15
15
  def initialize(exception, env)
16
16
  @exception = real_exception(exception)
17
17
  @env = env
18
+ @start_time = Time.now.to_f
19
+ @repls = []
18
20
  end
19
21
 
20
- def render
21
- self.class.template.result binding
22
+ def render(template_name = "main")
23
+ self.class.template(template_name).result binding
24
+ end
25
+
26
+ def do_variables(opts)
27
+ index = opts["index"].to_i
28
+ @frame = backtrace_frames[index]
29
+ { html: render("variable_info") }
30
+ end
31
+
32
+ def do_eval(opts)
33
+ index = opts["index"].to_i
34
+ code = opts["source"]
35
+
36
+ unless binding = backtrace_frames[index].frame_binding
37
+ return { error: "REPL unavailable in this stack frame" }
38
+ end
39
+
40
+ result, prompt =
41
+ (@repls[index] ||= REPL.provider.new(binding)).send_input(code)
42
+
43
+ { result: result,
44
+ prompt: prompt,
45
+ highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil) }
46
+ end
47
+
48
+ def backtrace_frames
49
+ @backtrace_frames ||= StackFrame.from_exception(exception)
22
50
  end
23
51
 
24
52
  private
53
+ def exception_message
54
+ if exception.is_a?(SyntaxError) && exception.message =~ /\A.*:\d*: (.*)$/
55
+ $1
56
+ else
57
+ exception.message
58
+ end
59
+ end
60
+
25
61
  def real_exception(exception)
26
62
  if exception.respond_to? :original_exception
27
63
  exception.original_exception
@@ -33,48 +69,9 @@ module BetterErrors
33
69
  def request_path
34
70
  env["REQUEST_PATH"]
35
71
  end
36
-
37
- def backtrace_frames
38
- @backtrace_frames ||= ErrorFrame.from_exception(exception)
39
- end
40
-
41
- def coderay_scanner_for_ext(ext)
42
- case ext
43
- when "rb"; :ruby
44
- when "html"; :html
45
- when "erb"; :erb
46
- when "haml"; :haml
47
- end
48
- end
49
-
50
- def file_extension(filename)
51
- filename.split(".").last
52
- end
53
-
54
- def code_extract(frame, lines_of_context = 5)
55
- lines = File.readlines(frame.filename)
56
- min_line = [1, frame.line - lines_of_context].max - 1
57
- max_line = [frame.line + lines_of_context, lines.count + 1].min - 1
58
- [min_line, max_line, lines[min_line..max_line].join]
59
- end
60
72
 
61
73
  def highlighted_code_block(frame)
62
- ext = file_extension(frame.filename)
63
- scanner = coderay_scanner_for_ext(ext)
64
- min_line, max_line, code = code_extract(frame)
65
- highlighted_code = CodeRay.scan(code, scanner).div wrap: nil
66
- "".tap do |html|
67
- html << "<div class='code'>"
68
- highlighted_code.each_line.each_with_index do |str, index|
69
- if min_line + index + 1 == frame.line
70
- html << "<pre class='highlight'>"
71
- else
72
- html << "<pre>"
73
- end
74
- html << sprintf("%5d", min_line + index + 1) << " " << str << "</pre>"
75
- end
76
- html << "</div>"
77
- end
74
+ CodeFormatter.new(frame.filename, frame.line).html
78
75
  end
79
76
  end
80
77
  end
@@ -1,3 +1,5 @@
1
+ require "json"
2
+
1
3
  module BetterErrors
2
4
  class Middleware
3
5
  def initialize(app, handler = ErrorPage)
@@ -6,10 +8,47 @@ module BetterErrors
6
8
  end
7
9
 
8
10
  def call(env)
11
+ case env["PATH_INFO"]
12
+ when %r{\A/__better_errors/(?<oid>\d+)/(?<method>\w+)\z}
13
+ internal_call env, $~
14
+ when %r{\A/__better_errors/?\z}
15
+ show_error_page env
16
+ else
17
+ app_call env
18
+ end
19
+ end
20
+
21
+ private
22
+ def app_call(env)
9
23
  @app.call env
10
24
  rescue Exception => ex
11
- error_page = @handler.new ex, env
12
- [500, { "Content-Type" => "text/html; charset=utf-8" }, [error_page.render]]
25
+ @error_page = @handler.new ex, env
26
+ log_exception
27
+ show_error_page(env)
28
+ end
29
+
30
+ def show_error_page(env)
31
+ [500, { "Content-Type" => "text/html; charset=utf-8" }, [@error_page.render]]
32
+ end
33
+
34
+ def log_exception
35
+ return unless BetterErrors.logger
36
+
37
+ message = "\n#{@error_page.exception.class} - #{@error_page.exception.message}:\n"
38
+ @error_page.backtrace_frames.each do |frame|
39
+ message << " #{frame}\n"
40
+ end
41
+
42
+ BetterErrors.logger.fatal message
43
+ end
44
+
45
+ def internal_call(env, opts)
46
+ if opts[:oid].to_i != @error_page.object_id
47
+ return [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(error: "Session expired")]]
48
+ end
49
+
50
+ response = @error_page.send("do_#{opts[:method]}", JSON.parse(env["rack.input"].read))
51
+ [200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
13
52
  end
14
53
  end
15
54
  end
@@ -1,10 +1,11 @@
1
1
  module BetterErrors
2
2
  class Railtie < Rails::Railtie
3
3
  initializer "better_errors.configure_rails_initialization" do
4
- middleware = Rails.application.middleware
5
- middleware.use BetterErrors::Middleware
6
-
7
- BetterErrors.application_root = Rails.root.to_s
4
+ unless Rails.env.production?
5
+ Rails.application.middleware.use BetterErrors::Middleware
6
+ BetterErrors.logger = Rails.logger
7
+ BetterErrors.application_root = Rails.root.to_s
8
+ end
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,20 @@
1
+ module BetterErrors
2
+ module REPL
3
+ class Basic
4
+ def initialize(binding)
5
+ @binding = binding
6
+ end
7
+
8
+ def send_input(str)
9
+ [execute(str), ">>"]
10
+ end
11
+
12
+ private
13
+ def execute(str)
14
+ "=> #{@binding.eval(str).inspect}\n"
15
+ rescue Exception => e
16
+ "!! #{e.inspect rescue e.class.to_s rescue "Exception"}\n"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,67 @@
1
+ require "fiber"
2
+ require "pry"
3
+
4
+ module BetterErrors
5
+ module REPL
6
+ class Pry
7
+ class Input
8
+ def readline
9
+ Fiber.yield
10
+ end
11
+ end
12
+
13
+ class Output
14
+ def initialize
15
+ @buffer = ""
16
+ end
17
+
18
+ def puts(*args)
19
+ args.each do |arg|
20
+ @buffer << "#{arg.chomp}\n"
21
+ end
22
+ end
23
+
24
+ def tty?
25
+ false
26
+ end
27
+
28
+ def read_buffer
29
+ @buffer
30
+ ensure
31
+ @buffer = ""
32
+ end
33
+ end
34
+
35
+ def initialize(binding)
36
+ @fiber = Fiber.new do
37
+ @pry.repl binding
38
+ end
39
+ @input = Input.new
40
+ @output = Output.new
41
+ @pry = ::Pry.new input: @input, output: @output
42
+ @pry.hooks.clear_all
43
+ @continued_expression = false
44
+ @pry.hooks.add_hook :after_read, "better_errors hacky hook" do
45
+ @continued_expression = false
46
+ end
47
+ @fiber.resume
48
+ end
49
+
50
+ def pry_indent
51
+ @pry.instance_variable_get(:@indent)
52
+ end
53
+
54
+ def send_input(str)
55
+ old_pry_config_color = ::Pry.config.color
56
+ ::Pry.config.color = false
57
+ @continued_expression = true
58
+ @fiber.resume "#{str}\n"
59
+ # TODO - indent with `pry_indent.current_prefix`
60
+ # TODO - use proper pry prompt
61
+ [@output.read_buffer, @continued_expression ? ".." : ">>"]
62
+ ensure
63
+ ::Pry.config.color = old_pry_config_color
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ module BetterErrors
2
+ module REPL
3
+ PROVIDERS = [
4
+ { impl: "better_errors/repl/basic",
5
+ const: :Basic },
6
+ ]
7
+
8
+ def self.provider
9
+ @provider ||= const_get detect[:const]
10
+ end
11
+
12
+ def self.provider=(prov)
13
+ @provider = prov
14
+ end
15
+
16
+ def self.detect
17
+ PROVIDERS.find do |prov|
18
+ test_provider prov
19
+ end
20
+ end
21
+
22
+ def self.test_provider(provider)
23
+ require provider[:impl]
24
+ true
25
+ rescue LoadError
26
+ false
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,124 @@
1
+ module BetterErrors
2
+ class StackFrame
3
+ def self.from_exception(exception)
4
+ idx_offset = 0
5
+ list = exception.backtrace.each_with_index.map do |frame, idx|
6
+ frame_binding = exception.__better_errors_bindings_stack[idx - idx_offset]
7
+ md = /\A(?<file>.*):(?<line>\d*)(:in `(?<name>.*)')?\z/.match(frame)
8
+
9
+ # prevent mismatching frames in the backtrace with the binding stack
10
+ if frame_binding and frame_binding.eval("__FILE__") != md[:file]
11
+ idx_offset += 1
12
+ frame_binding = nil
13
+ end
14
+
15
+ StackFrame.new(md[:file], md[:line].to_i, md[:name], frame_binding)
16
+ end
17
+
18
+ if exception.is_a?(SyntaxError) && exception.to_s =~ /\A(.*):(\d*):/
19
+ list.unshift StackFrame.new($1, $2.to_i, "")
20
+ end
21
+
22
+ list
23
+ end
24
+
25
+ attr_reader :filename, :line, :name, :frame_binding
26
+
27
+ def initialize(filename, line, name, frame_binding = nil)
28
+ @filename = filename
29
+ @line = line
30
+ @name = name
31
+ @frame_binding = frame_binding
32
+
33
+ set_pretty_method_name if frame_binding
34
+ end
35
+
36
+ def application?
37
+ root = BetterErrors.application_root
38
+ starts_with? filename, root if root
39
+ end
40
+
41
+ def application_path
42
+ filename[(BetterErrors.application_root.length+1)..-1]
43
+ end
44
+
45
+ def gem?
46
+ Gem.path.any? { |path| starts_with? filename, path }
47
+ end
48
+
49
+ def gem_path
50
+ Gem.path.each do |path|
51
+ if starts_with? filename, path
52
+ return filename.gsub("#{path}/gems/", "(gem) ")
53
+ end
54
+ end
55
+ end
56
+
57
+ def class_name
58
+ @class_name
59
+ end
60
+
61
+ def method_name
62
+ @method_name || @name
63
+ end
64
+
65
+ def context
66
+ if application?
67
+ :application
68
+ elsif gem?
69
+ :gem
70
+ else
71
+ :dunno
72
+ end
73
+ end
74
+
75
+ def pretty_path
76
+ case context
77
+ when :application; application_path
78
+ when :gem; gem_path
79
+ else filename
80
+ end
81
+ end
82
+
83
+ def local_variables
84
+ return {} unless frame_binding
85
+ frame_binding.eval("local_variables").each_with_object({}) do |name, hash|
86
+ begin
87
+ hash[name] = frame_binding.eval(name.to_s)
88
+ rescue NameError => e
89
+ # local_variables sometimes returns broken variables.
90
+ # https://bugs.ruby-lang.org/issues/7536
91
+ end
92
+ end
93
+ end
94
+
95
+ def instance_variables
96
+ return {} unless frame_binding
97
+ Hash[frame_binding.eval("instance_variables").map { |x|
98
+ [x, frame_binding.eval(x.to_s)]
99
+ }]
100
+ end
101
+
102
+ def to_s
103
+ "#{pretty_path}:#{line}:in `#{name}'"
104
+ end
105
+
106
+ private
107
+ def set_pretty_method_name
108
+ name =~ /\A(block (\([^)]+\) )?in )?/
109
+ recv = frame_binding.eval("self")
110
+ return unless method_name = frame_binding.eval("__method__")
111
+ if recv.is_a? Module
112
+ @class_name = "#{$1}#{recv}"
113
+ @method_name = ".#{method_name}"
114
+ else
115
+ @class_name = "#{$1}#{recv.class}"
116
+ @method_name = "##{method_name}"
117
+ end
118
+ end
119
+
120
+ def starts_with?(haystack, needle)
121
+ haystack[0, needle.length] == needle
122
+ end
123
+ end
124
+ end