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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +2 -0
  6. data/Rakefile +6 -0
  7. data/Readme.md +34 -0
  8. data/acceptance +29 -0
  9. data/experiments/formatting/5_potential_structure_dsls.rb +88 -0
  10. data/experiments/formatting/half_thoughtout_dsl_for_toplevel_structure_of_argument_error.rb +43 -0
  11. data/experiments/formatting/haml_like_structure.rb +139 -0
  12. data/experiments/formatting/other_structures +156 -0
  13. data/lib/error_to_communicate.rb +3 -0
  14. data/lib/error_to_communicate/at_exit.rb +11 -0
  15. data/lib/error_to_communicate/config.rb +32 -0
  16. data/lib/error_to_communicate/exception_info.rb +41 -0
  17. data/lib/error_to_communicate/format.rb +132 -0
  18. data/lib/error_to_communicate/format/terminal_helpers.rb +97 -0
  19. data/lib/error_to_communicate/parse/backtrace.rb +34 -0
  20. data/lib/error_to_communicate/parse/exception.rb +21 -0
  21. data/lib/error_to_communicate/parse/no_method_error.rb +27 -0
  22. data/lib/error_to_communicate/parse/registry.rb +30 -0
  23. data/lib/error_to_communicate/parse/wrong_number_of_arguments.rb +35 -0
  24. data/lib/error_to_communicate/rspec_formatter.rb +46 -0
  25. data/lib/error_to_communicate/version.rb +3 -0
  26. data/lib/what_weve_got_here_is_an_error_to_communicate.rb +3 -0
  27. data/screenshot.png +0 -0
  28. data/spec/acceptance/argument_error_spec.rb +55 -0
  29. data/spec/acceptance/exception_spec.rb +29 -0
  30. data/spec/acceptance/no_error_spec.rb +44 -0
  31. data/spec/acceptance/no_methood_error_spec.rb +50 -0
  32. data/spec/acceptance/spec_helper.rb +41 -0
  33. data/spec/parse/backtrace_spec.rb +101 -0
  34. data/spec/parse/exception_spec.rb +14 -0
  35. data/spec/parse/no_method_error_spec.rb +23 -0
  36. data/spec/parse/registered_parsers_spec.rb +68 -0
  37. data/spec/parse/spec_helper.rb +23 -0
  38. data/spec/parse/wrong_number_of_arguments_spec.rb +77 -0
  39. data/spec/rspec_formatter_spec.rb +95 -0
  40. data/spec/spec_helper.rb +20 -0
  41. data/what_weve_got_here_is_an_error_to_communicate.gemspec +19 -0
  42. metadata +168 -0
@@ -0,0 +1,3 @@
1
+ require 'error_to_communicate/config'
2
+ ErrorToCommunicate = WhatWeveGotHereIsAnErrorToCommunicate
3
+ ErrorToCommunicate::CONFIG = ErrorToCommunicate::Config.new
@@ -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