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