what_weve_got_here_is_an_error_to_communicate 0.0.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.
- 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
|