what_weve_got_here_is_an_error_to_communicate 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.travis.yml +7 -0
- data/Gemfile +2 -0
- data/Rakefile +6 -0
- data/Readme.md +34 -0
- data/acceptance +29 -0
- data/experiments/formatting/5_potential_structure_dsls.rb +88 -0
- data/experiments/formatting/half_thoughtout_dsl_for_toplevel_structure_of_argument_error.rb +43 -0
- data/experiments/formatting/haml_like_structure.rb +139 -0
- data/experiments/formatting/other_structures +156 -0
- data/lib/error_to_communicate.rb +3 -0
- data/lib/error_to_communicate/at_exit.rb +11 -0
- data/lib/error_to_communicate/config.rb +32 -0
- data/lib/error_to_communicate/exception_info.rb +41 -0
- data/lib/error_to_communicate/format.rb +132 -0
- data/lib/error_to_communicate/format/terminal_helpers.rb +97 -0
- data/lib/error_to_communicate/parse/backtrace.rb +34 -0
- data/lib/error_to_communicate/parse/exception.rb +21 -0
- data/lib/error_to_communicate/parse/no_method_error.rb +27 -0
- data/lib/error_to_communicate/parse/registry.rb +30 -0
- data/lib/error_to_communicate/parse/wrong_number_of_arguments.rb +35 -0
- data/lib/error_to_communicate/rspec_formatter.rb +46 -0
- data/lib/error_to_communicate/version.rb +3 -0
- data/lib/what_weve_got_here_is_an_error_to_communicate.rb +3 -0
- data/screenshot.png +0 -0
- data/spec/acceptance/argument_error_spec.rb +55 -0
- data/spec/acceptance/exception_spec.rb +29 -0
- data/spec/acceptance/no_error_spec.rb +44 -0
- data/spec/acceptance/no_methood_error_spec.rb +50 -0
- data/spec/acceptance/spec_helper.rb +41 -0
- data/spec/parse/backtrace_spec.rb +101 -0
- data/spec/parse/exception_spec.rb +14 -0
- data/spec/parse/no_method_error_spec.rb +23 -0
- data/spec/parse/registered_parsers_spec.rb +68 -0
- data/spec/parse/spec_helper.rb +23 -0
- data/spec/parse/wrong_number_of_arguments_spec.rb +77 -0
- data/spec/rspec_formatter_spec.rb +95 -0
- data/spec/spec_helper.rb +20 -0
- data/what_weve_got_here_is_an_error_to_communicate.gemspec +19 -0
- metadata +168 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'error_to_communicate'
|
2
|
+
require 'error_to_communicate/format'
|
3
|
+
|
4
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
5
|
+
at_exit do
|
6
|
+
exception = $!
|
7
|
+
next unless CONFIG.parse? exception
|
8
|
+
$stderr.puts format CONFIG.parse exception
|
9
|
+
exit! 1 # there has got to be a better way to clear an exception, this could break other at_exit hooks -.^
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'error_to_communicate/version'
|
2
|
+
require 'error_to_communicate/parse/registry'
|
3
|
+
require 'error_to_communicate/parse/exception'
|
4
|
+
require 'error_to_communicate/parse/no_method_error'
|
5
|
+
require 'error_to_communicate/parse/wrong_number_of_arguments'
|
6
|
+
|
7
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
8
|
+
class Config
|
9
|
+
attr_accessor :registry
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
self.registry = Parse::Registry.new(
|
13
|
+
dont_parse: lambda { |exception|
|
14
|
+
exception.kind_of? SystemExit
|
15
|
+
},
|
16
|
+
parsers: [
|
17
|
+
Parse::WrongNumberOfArguments,
|
18
|
+
Parse::NoMethodError,
|
19
|
+
Parse::Exception,
|
20
|
+
]
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse?(exception)
|
25
|
+
registry.parse?(exception)
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse(exception)
|
29
|
+
registry.parse(exception)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
2
|
+
class ExceptionInfo
|
3
|
+
attr_accessor :classname, :explanation, :backtrace
|
4
|
+
attr_accessor :exception # for dev info only, parse out additional info rather than interacting with it direclty
|
5
|
+
def initialize(attributes)
|
6
|
+
self.exception = attributes.fetch :exception, nil
|
7
|
+
self.classname = attributes.fetch :classname
|
8
|
+
self.explanation = attributes.fetch :explanation
|
9
|
+
self.backtrace = attributes.fetch :backtrace
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ExceptionInfo::Location
|
14
|
+
# TODO: rename linenum -> line_number
|
15
|
+
attr_accessor :path, :linenum, :label, :pred, :succ
|
16
|
+
def initialize(attributes)
|
17
|
+
self.path = attributes.fetch :path
|
18
|
+
self.linenum = attributes.fetch :linenum
|
19
|
+
self.label = attributes.fetch :label
|
20
|
+
self.pred = attributes.fetch :pred, nil
|
21
|
+
self.succ = attributes.fetch :succ, nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ExceptionInfo::WrongNumberOfArguments < ExceptionInfo
|
26
|
+
attr_accessor :num_expected, :num_received
|
27
|
+
def initialize(attributes)
|
28
|
+
self.num_expected = attributes.fetch :num_expected
|
29
|
+
self.num_received = attributes.fetch :num_received
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ExceptionInfo::NoMethodError < ExceptionInfo
|
35
|
+
attr_accessor :undefined_method_name
|
36
|
+
def initialize(attributes)
|
37
|
+
self.undefined_method_name = attributes.fetch :undefined_method_name
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'coderay'
|
2
|
+
require 'pathname'
|
3
|
+
require 'error_to_communicate/format/terminal_helpers'
|
4
|
+
|
5
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
6
|
+
extend Format::TerminalHelpers
|
7
|
+
def self.format(info)
|
8
|
+
cwd = Dir.pwd
|
9
|
+
|
10
|
+
# FIXME:
|
11
|
+
# Something else should set this?
|
12
|
+
# I'd say heuristic, but fact is that it needs formatting info.
|
13
|
+
# Maybe initially, heuristic contains both extracted info and formatting info?
|
14
|
+
# Or maybe we want polymorphism at the formatter level?
|
15
|
+
display_class_and_message = lambda do |info|
|
16
|
+
if info.classname == 'ArgumentError'
|
17
|
+
"#{white}#{info.classname} | "\
|
18
|
+
"#{bri_red}#{info.explanation} "\
|
19
|
+
"#{dim_red}(expected #{white}#{info.num_expected},"\
|
20
|
+
"#{dim_red} sent #{white}#{info.num_received}"\
|
21
|
+
"#{dim_red})"\
|
22
|
+
"#{none}"
|
23
|
+
else
|
24
|
+
"#{white}#{info.classname} | "\
|
25
|
+
"#{bri_red}#{info.explanation} "\
|
26
|
+
"#{none}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
display_location = lambda do |attributes|
|
31
|
+
location = attributes.fetch :location
|
32
|
+
cwd = Pathname.new attributes.fetch(:cwd)
|
33
|
+
path = Pathname.new location.path
|
34
|
+
line_index = location.linenum - 1
|
35
|
+
highlight = attributes.fetch :highlight, location.label
|
36
|
+
end_index = bound_num min: 0, num: line_index+attributes.fetch(:context).end
|
37
|
+
start_index = bound_num min: 0, num: line_index+attributes.fetch(:context).begin
|
38
|
+
message = attributes.fetch :message, ''
|
39
|
+
message_offset = line_index - start_index
|
40
|
+
|
41
|
+
# first line gives the path
|
42
|
+
path_line = ""
|
43
|
+
path_line << color_path("#{path_to_dir cwd, path}/")
|
44
|
+
path_line << color_filename(path.basename)
|
45
|
+
path_line << ":" << color_linenum(location.linenum)
|
46
|
+
|
47
|
+
# then display the code
|
48
|
+
if path.exist?
|
49
|
+
code = File.read(path).lines[start_index..end_index].join("")
|
50
|
+
code = remove_indentation code
|
51
|
+
code = CodeRay.encode code, :ruby, :terminal
|
52
|
+
code = prefix_linenos_to code, start_index.next
|
53
|
+
code = indent code, " "
|
54
|
+
code = add_message_to code, message_offset, screaming_red(message)
|
55
|
+
code = highlight_text code, message_offset, highlight
|
56
|
+
else
|
57
|
+
code = "Can't find code\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
# adjust for emphasization
|
61
|
+
if attributes.fetch(:emphasisis) == :path
|
62
|
+
path_line = underline path_line
|
63
|
+
code = indent code, " "
|
64
|
+
code = desaturate code
|
65
|
+
code = highlight_text code, message_offset, highlight # b/c desaturate really strips color
|
66
|
+
end
|
67
|
+
|
68
|
+
# all together
|
69
|
+
path_line << "\n" << code
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# Display the ArgumentError
|
74
|
+
display = ""
|
75
|
+
display << separator
|
76
|
+
display << display_class_and_message.call(info) << "\n"
|
77
|
+
|
78
|
+
# Display the Heuristic
|
79
|
+
display << separator
|
80
|
+
|
81
|
+
# FIXME: Some sort of polymorphism or normalization would be way better here, too
|
82
|
+
# And, at the very least, not switching on classname, but some more abstract piece of info,
|
83
|
+
# b/c classnames are not completely consistent across the implementations
|
84
|
+
# (eg: https://github.com/JoshCheek/seeing_is_believing/blob/cc93b4ee3a83145509c235f64d9454dc3e12d8c9/lib/seeing_is_believing/event_stream/producer.rb#L54-55)
|
85
|
+
if info.classname == 'ArgumentError'
|
86
|
+
display << display_location.call(location: info.backtrace[0],
|
87
|
+
highlight: info.backtrace[0].label,
|
88
|
+
context: 0..5,
|
89
|
+
message: "EXPECTED #{info.num_expected}",
|
90
|
+
emphasisis: :code,
|
91
|
+
cwd: cwd)
|
92
|
+
display << "\n"
|
93
|
+
display << display_location.call(location: info.backtrace[1],
|
94
|
+
highlight: info.backtrace[0].label,
|
95
|
+
context: -5..5,
|
96
|
+
message: "SENT #{info.num_received}",
|
97
|
+
emphasisis: :code,
|
98
|
+
cwd: cwd)
|
99
|
+
elsif info.classname == 'NoMethodError'
|
100
|
+
display << display_location.call(location: info.backtrace[0],
|
101
|
+
highlight: info.backtrace[0].label,
|
102
|
+
context: -5..5,
|
103
|
+
message: "#{info.undefined_method_name} is undefined",
|
104
|
+
emphasisis: :code,
|
105
|
+
cwd: cwd)
|
106
|
+
else
|
107
|
+
display << display_location.call(location: info.backtrace[0],
|
108
|
+
highlight: info.backtrace[0].label,
|
109
|
+
context: -5..5,
|
110
|
+
emphasisis: :code,
|
111
|
+
cwd: cwd)
|
112
|
+
end
|
113
|
+
|
114
|
+
# display the backtrace
|
115
|
+
display << separator
|
116
|
+
display << display_location.call(location: info.backtrace[0],
|
117
|
+
highlight: info.backtrace[0].label,
|
118
|
+
context: 0..0,
|
119
|
+
emphasisis: :path,
|
120
|
+
cwd: cwd)
|
121
|
+
|
122
|
+
display << info.backtrace.each_cons(2).map { |next_loc, crnt_loc|
|
123
|
+
display_location.call location: crnt_loc,
|
124
|
+
highlight: next_loc.label,
|
125
|
+
context: 0..0,
|
126
|
+
emphasisis: :path,
|
127
|
+
cwd: cwd
|
128
|
+
}.join("")
|
129
|
+
|
130
|
+
display
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
2
|
+
module Format
|
3
|
+
module TerminalHelpers
|
4
|
+
def separator
|
5
|
+
("="*70) << "\n"
|
6
|
+
end
|
7
|
+
|
8
|
+
def color_path(str)
|
9
|
+
"\e[38;5;36m#{str}\e[39m" # fg r:0, g:3, b:2 (out of 0..5)
|
10
|
+
end
|
11
|
+
|
12
|
+
def color_linenum(linenum)
|
13
|
+
"\e[34m#{linenum}\e[39m"
|
14
|
+
end
|
15
|
+
|
16
|
+
def path_to_dir(from, to)
|
17
|
+
to.relative_path_from(from).dirname
|
18
|
+
rescue ArgumentError
|
19
|
+
return to # eg rbx's core code
|
20
|
+
end
|
21
|
+
|
22
|
+
def white
|
23
|
+
"\e[38;5;255m"
|
24
|
+
end
|
25
|
+
|
26
|
+
def bri_red
|
27
|
+
"\e[38;5;196m"
|
28
|
+
end
|
29
|
+
|
30
|
+
def dim_red
|
31
|
+
"\e[38;5;124m"
|
32
|
+
end
|
33
|
+
|
34
|
+
def none
|
35
|
+
"\e[39m"
|
36
|
+
end
|
37
|
+
|
38
|
+
def bound_num(attributes)
|
39
|
+
num = attributes.fetch :num
|
40
|
+
min = attributes.fetch :min
|
41
|
+
num < min ? min : num
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_indentation(code)
|
45
|
+
indentation = code.scan(/^\s*/).min_by(&:length)
|
46
|
+
code.gsub(/^#{indentation}/, "")
|
47
|
+
end
|
48
|
+
|
49
|
+
def prefix_linenos_to(code, start_linenum)
|
50
|
+
lines = code.lines
|
51
|
+
max_linenum = lines.count + start_linenum - 1 # 1 to translate to indexes
|
52
|
+
linenum_width = max_linenum.to_s.length + 1 # 1 for the colon
|
53
|
+
lines.zip(start_linenum..max_linenum)
|
54
|
+
.map { |line, num|
|
55
|
+
formatted_num = "#{num}:".ljust(linenum_width)
|
56
|
+
color_linenum(formatted_num) << " " << line
|
57
|
+
}.join("")
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_message_to(code, offset, message)
|
61
|
+
lines = code.lines
|
62
|
+
lines[offset].chomp! << " " << message << "\n"
|
63
|
+
lines.join("")
|
64
|
+
end
|
65
|
+
|
66
|
+
def highlight_text(code, index, text)
|
67
|
+
lines = code.lines
|
68
|
+
return code unless lines[index]
|
69
|
+
lines[index].gsub!(text, "\e[7m#{text}\e[27m") # invert
|
70
|
+
lines.join("")
|
71
|
+
end
|
72
|
+
|
73
|
+
def indent(str, indentation_str)
|
74
|
+
str.gsub /^/, indentation_str
|
75
|
+
end
|
76
|
+
|
77
|
+
def screaming_red(text)
|
78
|
+
return "" if text.empty?
|
79
|
+
"\e[38;5;255;48;5;88m #{text} \e[39;49m" # bright white on medium red
|
80
|
+
end
|
81
|
+
|
82
|
+
def underline(str)
|
83
|
+
"\e[4m#{str}\e[24m"
|
84
|
+
end
|
85
|
+
|
86
|
+
def color_filename(str)
|
87
|
+
"\e[38;5;49;1m#{str}\e[39m" # fg r:0, g:5, b:3 (out of 0..5)
|
88
|
+
end
|
89
|
+
|
90
|
+
def desaturate(str)
|
91
|
+
nocolor = str.gsub(/\e\[[\d;]+?m/, "")
|
92
|
+
allgray = nocolor.gsub(/^(.*?)\n?$/, "\e[38;5;240m\\1\e[39m\n")
|
93
|
+
allgray
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'error_to_communicate/exception_info'
|
2
|
+
|
3
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
4
|
+
module Parse
|
5
|
+
module Backtrace
|
6
|
+
def self.parse?(exception)
|
7
|
+
# Really, there are better methods, e.g. backtrace_locations,
|
8
|
+
# but they're unevenly implemented across versions and implementations
|
9
|
+
exception.respond_to? :backtrace
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.parse(exception)
|
13
|
+
locations = exception.backtrace.map &method(:parse_backtrace_line)
|
14
|
+
locations.each_cons(2) do |crnt, succ|
|
15
|
+
succ.pred = crnt
|
16
|
+
crnt.succ = succ
|
17
|
+
end
|
18
|
+
locations
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO: What if the line doesn't match for some reason?
|
22
|
+
# Raise an exception?
|
23
|
+
# Use some reasonable default? (is there one?)
|
24
|
+
def self.parse_backtrace_line(line)
|
25
|
+
line =~ /^(.*?):(\d+):in `(.*?)'$/ # Are ^ and $ sufficient? Should be \A and (\Z or \z)?
|
26
|
+
ExceptionInfo::Location.new(
|
27
|
+
path: $1,
|
28
|
+
linenum: $2.to_i,
|
29
|
+
label: $3,
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'error_to_communicate/exception_info'
|
2
|
+
require 'error_to_communicate/parse/backtrace'
|
3
|
+
|
4
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
5
|
+
module Parse
|
6
|
+
module Exception
|
7
|
+
def self.parse?(exception)
|
8
|
+
exception.respond_to?(:message) && Backtrace.parse?(exception)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.parse(exception)
|
12
|
+
ExceptionInfo.new(
|
13
|
+
exception: exception,
|
14
|
+
classname: exception.class.to_s,
|
15
|
+
explanation: exception.message,
|
16
|
+
backtrace: Backtrace.parse(exception),
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'error_to_communicate/exception_info'
|
2
|
+
require 'error_to_communicate/parse/backtrace'
|
3
|
+
|
4
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
5
|
+
module Parse
|
6
|
+
module NoMethodError
|
7
|
+
def self.parse?(exception)
|
8
|
+
exception.kind_of? ::NoMethodError
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.parse(exception)
|
12
|
+
ExceptionInfo::NoMethodError.new(
|
13
|
+
exception: exception,
|
14
|
+
classname: exception.class.to_s,
|
15
|
+
explanation: exception.message[/^[^\(]*/].strip,
|
16
|
+
backtrace: Backtrace.parse(exception),
|
17
|
+
undefined_method_name: extract_method_name(exception.message),
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.extract_method_name(message)
|
22
|
+
words = message.split(/\s+/)
|
23
|
+
words[2][1...-1]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
2
|
+
module Parse
|
3
|
+
class Registry
|
4
|
+
def initialize(options)
|
5
|
+
@dont_parse = options.fetch :dont_parse
|
6
|
+
@parsers = options.fetch :parsers
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(parser)
|
10
|
+
@parsers << parser
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def parser_for(exception)
|
15
|
+
return nil if @dont_parse.call exception
|
16
|
+
@parsers.find { |parser| parser.parse? exception }
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse?(exception)
|
20
|
+
!!parser_for(exception)
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse(exception)
|
24
|
+
parser = @parsers.find { |parser| parser.parse? exception }
|
25
|
+
return parser.parse exception if parser
|
26
|
+
raise ::ArgumentError.new, "No parser found for #{exception.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'error_to_communicate/exception_info'
|
2
|
+
require 'error_to_communicate/parse/backtrace'
|
3
|
+
|
4
|
+
module WhatWeveGotHereIsAnErrorToCommunicate
|
5
|
+
module Parse
|
6
|
+
module WrongNumberOfArguments
|
7
|
+
def self.parse?(exception)
|
8
|
+
exception.respond_to?(:message) && extract_from(exception)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.parse(exception)
|
12
|
+
num_received, num_expected = extract_from(exception)
|
13
|
+
ExceptionInfo::WrongNumberOfArguments.new(
|
14
|
+
exception: exception,
|
15
|
+
classname: exception.class.to_s,
|
16
|
+
explanation: 'Wrong number of arguments',
|
17
|
+
backtrace: Backtrace.parse(exception),
|
18
|
+
num_expected: num_expected,
|
19
|
+
num_received: num_received,
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.extract_from(exception)
|
26
|
+
case exception.message
|
27
|
+
when /^wrong number of arguments.*?\((\d+) for (\d+)\)$/ # MRI / JRuby
|
28
|
+
num_received, num_expected = $1.to_i, $2.to_i
|
29
|
+
when /^method '.*?': given (\d+).*? expected (\d+)$/ # RBX
|
30
|
+
num_received, num_expected = $1.to_i, $2.to_i
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|