what_weve_got_here_is_an_error_to_communicate 0.0.6 → 0.0.7

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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NzJkODUwYWU1ZGNiZGU2YjdmY2IyMzhlYmZkN2E4NzczMjZlMmYwOA==
5
- data.tar.gz: !binary |-
6
- ODY5NzA5MDJiNWEzOWM2OGUwZmI1NDcwOTk0YjIxYTc2MzIyYjVkYw==
2
+ SHA1:
3
+ metadata.gz: f9ff0e4e8188c71874623d5b7f91c3b1628c733c
4
+ data.tar.gz: aa5acc32a411201dc00d52ebf551b533a36d85fc
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- ZTQ1YWQzYzA4Y2JlYTkzNmY5ODlmMjEyMjdiYTlmMjc2MmRmMWVhZmJjYWRi
10
- MTU3ZGNkODY2NThhMmQ4Yzc2MmE4ODliODRmY2NiNTZhYjUwNDA5N2QxMGVl
11
- NmM3YjUzNWZkMjk4ZWQ2MTA0NTA2OWJhMDYyNjg5MjU5NDc5YTA=
12
- data.tar.gz: !binary |-
13
- YzQzZjBkZjhjNzgyNjEzM2JiN2RiZWQ4ZjU1ZGJiZjAzNTFlNTU1ZDUwNWZl
14
- ZDRkYzc4YmEzYTcxMzViZWM2NGVkYjI3YTUxMWIyNjA5Y2JhNWYyYjg1NGVh
15
- ZmEyYTlkZmIxODA5NzY1NTBhMGM2YWY0NGZhN2RhM2I0YTc3YTQ=
6
+ metadata.gz: ad7c22761d89cce4c92264aadd62b3c1c1609d5713ed97c76d06d50cb6c7e5057b8856a3ac2240cb0f14122401cc51c10453bff5ff1f3385a1292cc1124205af
7
+ data.tar.gz: 67ced68e105811bb22d0a1382eced38ae2830e1c8ebcb06f62852aeb9c0bde436b654217b31c3641452b2a674eee06c2eb45b2a63741a1c8461373cdada82fae
@@ -1,13 +1,23 @@
1
+ require 'interception'
1
2
  require 'error_to_communicate'
2
3
 
4
+ error_binding = nil
5
+ recording_code = lambda { |_exc, binding|
6
+ error_binding = binding
7
+ }
8
+
9
+ Interception.listen &recording_code
10
+
3
11
  # Deal with global deps and console knowledge here
4
12
  at_exit do
13
+ Interception.unlisten recording_code
5
14
  exception = $!
6
15
  config = ErrorToCommunicate::Config.default
7
16
 
8
- next unless config.accept? exception
17
+ next unless config.accept? exception, error_binding
18
+
19
+ heuristic = config.heuristic_for exception, error_binding
9
20
 
10
- heuristic = config.heuristic_for exception
11
21
  formatted = config.format heuristic, Dir.pwd
12
22
  $stderr.puts formatted
13
23
 
@@ -50,15 +50,15 @@ module ErrorToCommunicate
50
50
  self.project = Project.new root: root, loaded_features: loaded_features
51
51
  end
52
52
 
53
- def accept?(exception)
54
- return false unless ExceptionInfo.parseable? exception
55
- einfo = ExceptionInfo.parse(exception)
53
+ def accept?(exception, binding)
54
+ return false unless ExceptionInfo.parseable? exception, binding
55
+ einfo = ExceptionInfo.parse(exception, binding)
56
56
  !blacklist.call(einfo) && !!heuristics.find { |h| h.for? einfo }
57
57
  end
58
58
 
59
- def heuristic_for(exception)
60
- accept?(exception) || raise(ArgumentError, "Asked for a heuristic on an object we don't accept: #{exception.inspect}")
61
- einfo = ExceptionInfo.parse(exception)
59
+ def heuristic_for(exception, binding)
60
+ accept?(exception, binding) || raise(ArgumentError, "Asked for a heuristic on an object we don't accept: #{exception.inspect}")
61
+ einfo = ExceptionInfo.parse(exception, binding)
62
62
  heuristics.find { |heuristic| heuristic.for? einfo }
63
63
  .new(einfo: einfo, project: project)
64
64
  end
@@ -6,7 +6,7 @@ module ErrorToCommunicate
6
6
  end
7
7
 
8
8
  class ErrorToCommunicate::ExceptionInfo::Location
9
- attr_accessor :path, :linenum, :label, :pred, :succ
9
+ attr_accessor :path, :linenum, :label, :pred, :succ, :binding
10
10
 
11
11
  def initialize(attributes)
12
12
  self.path = Pathname.new attributes.fetch(:path)
@@ -14,17 +14,19 @@ class ErrorToCommunicate::ExceptionInfo::Location
14
14
  self.label = attributes.fetch :label
15
15
  self.pred = attributes.fetch :pred, nil
16
16
  self.succ = attributes.fetch :succ, nil
17
+ self.binding = attributes.fetch :binding
17
18
  end
18
19
 
19
20
  # What if the line doesn't match for some reason?
20
21
  # Raise an exception?
21
22
  # Use some reasonable default? (is there one?)
22
- def self.parse(line)
23
+ def self.parse(line, binding)
23
24
  line =~ /^(.*?):(\d+):in `(.*?)'$/ # Are ^ and $ sufficient? Should be \A and (\Z or \z)?
24
25
  ErrorToCommunicate::ExceptionInfo::Location.new(
25
26
  path: ($1||""),
26
27
  linenum: ($2||"-1").to_i,
27
28
  label: ($3||line),
29
+ binding: binding
28
30
  )
29
31
  end
30
32
 
@@ -55,6 +57,7 @@ class ErrorToCommunicate::ExceptionInfo
55
57
  attr_accessor :classname
56
58
  attr_accessor :message
57
59
  attr_accessor :backtrace
60
+ attr_accessor :binding
58
61
 
59
62
  def initialize(attributes)
60
63
  self.exception = attributes.fetch :exception, nil
@@ -74,22 +77,22 @@ class ErrorToCommunicate::ExceptionInfo
74
77
  @exception
75
78
  end
76
79
 
77
- def self.parseable?(exception)
80
+ def self.parseable?(exception, binding)
78
81
  exception.respond_to?(:message) && exception.respond_to?(:backtrace)
79
82
  end
80
83
 
81
- def self.parse(exception)
84
+ def self.parse(exception, binding)
82
85
  return exception if exception.kind_of? self
83
86
  new exception: exception,
84
87
  classname: exception.class.name,
85
88
  message: exception.message,
86
- backtrace: parse_backtrace(exception.backtrace)
89
+ backtrace: parse_backtrace(exception.backtrace, binding)
87
90
  end
88
91
 
89
- def self.parse_backtrace(backtrace)
92
+ def self.parse_backtrace(backtrace, binding)
90
93
  # Really, there are better methods, e.g. backtrace_locations,
91
94
  # but they're unevenly implemented across versions and implementations
92
- backtrace = (backtrace||[]).map { |line| Location.parse line }
95
+ backtrace = (backtrace||[]).map { |line| Location.parse line, binding }
93
96
  backtrace.each_cons(2) { |crnt, pred| crnt.pred, pred.succ = pred, crnt }
94
97
  backtrace
95
98
  end
@@ -38,7 +38,12 @@ module ErrorToCommunicate
38
38
  when Array then semantic_content.map { |c| format c }.join
39
39
  when :summary then format([:separator]) + format(content)
40
40
  when :heuristic then format([:separator]) + format(content)
41
- when :backtrace then format([:separator]) + format(content)
41
+ when :backtrace then
42
+ if content.any?
43
+ format([:separator]) + format(content)
44
+ else
45
+ format([:separator]) + format([:message, "No backtrace available"]) # TODO: Not tested, I hit this with capybara, when RSpec filtered everything out of the backtrace
46
+ end
42
47
  when :separator then theme.separator_line
43
48
  when :columns then theme.columns [content, *rest].map { |c| format c }
44
49
  when :classname then theme.classname format content
@@ -1,4 +1,6 @@
1
+ require 'rouge'
1
2
  require 'error_to_communicate/heuristic'
3
+ require 'error_to_communicate/levenshtein'
2
4
 
3
5
  module ErrorToCommunicate
4
6
  class Heuristic
@@ -25,8 +27,57 @@ module ErrorToCommunicate
25
27
  ]
26
28
  end
27
29
 
30
+ def semantic_explanation
31
+ if misspelling?
32
+ "You called the method `#{undefined_method_name}' on `#{name_of_ivar}', which is nil\nPossible misspelling of `#{closest_name}'"
33
+ else
34
+ super
35
+ end
36
+ end
37
+
28
38
  private
29
39
 
40
+ # FIXME:
41
+ # Needs to be able to deal with situations like
42
+ # the line number being within a multiline expression
43
+ def name_of_ivar
44
+ return @name_of_ivar if defined? @name_of_ivar
45
+ file = File.read(einfo.backtrace.first.path)
46
+ line = file.lines.to_a[einfo.backtrace.first.linenum-1]
47
+
48
+ tokens = Rouge::Lexers::Ruby.lex(line).to_a
49
+ index = tokens.index { |token, text| text == undefined_method_name }
50
+
51
+ while 0 <= index
52
+ token, text = tokens[index]
53
+ break if token.qualname == "Name.Variable.Instance"
54
+ index -= 1
55
+ end
56
+
57
+ @name_of_ivar = if index == -1
58
+ nil
59
+ else
60
+ token, text = tokens[index]
61
+ text
62
+ end
63
+ end
64
+
65
+ def existing_ivars
66
+ # TODO: this should get pushed into the location
67
+ binding = einfo.backtrace[0].binding
68
+ return [] unless binding
69
+ binding.eval('self').instance_variables
70
+ end
71
+
72
+ def misspelling?
73
+ name_of_ivar && closest_name &&
74
+ Levenshtein.call(closest_name, name_of_ivar) <= 2
75
+ end
76
+
77
+ def closest_name
78
+ @closest_name ||= existing_ivars.min_by { |varname| Levenshtein.call varname, name_of_ivar }
79
+ end
80
+
30
81
  def self.parse_undefined_name(message)
31
82
  message[/`(.*)'/, 1]
32
83
  end
@@ -20,7 +20,8 @@ module ErrorToCommunicate
20
20
  path: reported_file,
21
21
  linenum: reported_line,
22
22
  label: "unexpected #{unexpected}, expected: #{expected}",
23
- pred: backtrace[0]
23
+ pred: backtrace[0],
24
+ binding: nil
24
25
  end
25
26
 
26
27
  def semantic_info
@@ -0,0 +1,34 @@
1
+ module ErrorToCommunicate
2
+ class Levenshtein
3
+ def self.call(target, actual)
4
+ new(target, actual).distance
5
+ end
6
+
7
+ attr_accessor :target, :actual, :memoized
8
+ def initialize(target, actual)
9
+ self.target = target
10
+ self.actual = actual
11
+ self.memoized = {}
12
+ end
13
+
14
+ def distance
15
+ @distance ||= call target.length, actual.length
16
+ end
17
+
18
+ def call(target_length, actual_length)
19
+ memoized[[target_length, actual_length]] ||=
20
+ if target_length.zero?
21
+ actual_length
22
+ elsif actual_length.zero?
23
+ target_length
24
+ elsif target[target_length - 1] == actual[actual_length - 1]
25
+ call(target_length - 1, actual_length - 1)
26
+ else
27
+ [ call(target_length , actual_length - 1),
28
+ call(target_length - 1, actual_length ),
29
+ call(target_length - 1, actual_length - 1),
30
+ ].min + 1
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,4 @@
1
+ require 'interception'
1
2
  require 'error_to_communicate'
2
3
  require 'rspec/core/formatters/documentation_formatter'
3
4
 
@@ -17,10 +18,11 @@ module ErrorToCommunicate
17
18
  self.failure_number = attributes.fetch :failure_number
18
19
  self.failure = attributes.fetch :failure
19
20
  self.config = attributes.fetch :config
21
+ binding = attributes.fetch :binding
20
22
 
21
23
  # initialize the heuristic
22
- ExceptionInfo.parse(failure.exception).tap do |einfo|
23
- einfo.backtrace = ExceptionInfo.parse_backtrace failure.formatted_backtrace
24
+ ExceptionInfo.parse(failure.exception, binding).tap do |einfo|
25
+ einfo.backtrace = ExceptionInfo.parse_backtrace failure.formatted_backtrace, binding
24
26
  super einfo: einfo, project: config.project
25
27
  end
26
28
 
@@ -41,7 +43,7 @@ module ErrorToCommunicate
41
43
  [:classname, failure.description]]]] # TODO: not classname
42
44
  else
43
45
  # wrap the heuristic that would otherwise be chosen
44
- heuristic = config.heuristic_for einfo
46
+ heuristic = config.heuristic_for einfo, binding
45
47
  self.semantic_info = heuristic.semantic_info
46
48
  self.semantic_summary =
47
49
  [:summary, [
@@ -60,8 +62,18 @@ module ErrorToCommunicate
60
62
  end
61
63
  end
62
64
 
65
+ module ExceptionRecorder
66
+ extend self
67
+ def record_exception_bindings(config)
68
+ config.around do |spec|
69
+ Thread.current[:e2c_last_binding_seen] = nil
70
+ Interception.listen(spec) { |_exc, binding| Thread.current[:e2c_last_binding_seen] ||= binding }
71
+ end
72
+ end
73
+ ::RSpec.configure { |config| record_exception_bindings config }
74
+ end
63
75
 
64
- class RSpecFormatter < RSpec::Core::Formatters::DocumentationFormatter
76
+ class RSpecFormatter < ::RSpec::Core::Formatters::DocumentationFormatter
65
77
  # Register for notifications from our parent classes
66
78
  # http://rspec.info/documentation/3.2/rspec-core/RSpec/Core/Formatters.html
67
79
  #
@@ -71,6 +83,22 @@ module ErrorToCommunicate
71
83
  # }
72
84
  RSpec::Core::Formatters.register self
73
85
 
86
+ def initialize(*)
87
+ @num_failures = 0
88
+ super
89
+ end
90
+
91
+ def example_failed(failure_notification)
92
+ super
93
+ # we must create it here, because it won't have access to the callstack later
94
+ example = failure_notification.example
95
+ example.metadata[:heuristic] = Heuristic::RSpecFailure.new \
96
+ config: Config.default, # E2C heuristic, not RSpec's
97
+ failure: failure_notification,
98
+ failure_number: (@num_failures += 1),
99
+ binding: Thread.current[:e2c_last_binding_seen]
100
+ end
101
+
74
102
  # Use ErrorToCommunicate to print error info
75
103
  # rather than default DocumentationFormatter.
76
104
  #
@@ -82,11 +110,8 @@ module ErrorToCommunicate
82
110
  # but we can't currently turn colour off in our output
83
111
  def dump_failures(notification)
84
112
  output.puts "\nFailures:\n"
85
- notification.failure_notifications.each.with_index(1) do |failure, failure_number|
86
- heuristic = Heuristic::RSpecFailure.new \
87
- config: Config.default,
88
- failure: failure,
89
- failure_number: failure_number
113
+ notification.failure_notifications.each do |notification|
114
+ heuristic = notification.example.metadata.fetch :heuristic
90
115
  formatted = Config.default.format heuristic, Dir.pwd
91
116
  output.puts formatted.chomp.gsub(/^/, ' ')
92
117
  end
@@ -1,3 +1,3 @@
1
1
  module ErrorToCommunicate
2
- VERSION = '0.0.6'.freeze
2
+ VERSION = '0.0.7'.freeze
3
3
  end
@@ -0,0 +1,44 @@
1
+ require 'acceptance/spec_helper'
2
+
3
+ RSpec.context 'Exception', acceptance: true do
4
+ it 'Prints heuristics' do
5
+ write_file 'misspelled_ivar.rb', <<-BODY
6
+ require "what_weve_got_here_is_an_error_to_communicate"
7
+ User = Struct.new :name
8
+
9
+ class Email
10
+ def initialize(user)
11
+ @user = user
12
+ end
13
+
14
+ def body
15
+ "Dear, \#{@uesr.name}, <3 <3 <3"
16
+ end
17
+ end
18
+
19
+ Email.new(User.new 'Jorge').body
20
+ BODY
21
+
22
+ invocation = ruby 'misspelled_ivar.rb'
23
+ stderr = strip_color invocation.stderr
24
+
25
+ # sanity
26
+ expect(invocation.stdout).to eq ''
27
+ expect(invocation.exitstatus).to eq 1
28
+
29
+ # error: It prints the exception class and prints the message
30
+ expect(stderr).to match /NoMethodError/
31
+ expect(stderr).to match /You called the method `name' on `@uesr', which is nil/
32
+
33
+ # Suggests a fix
34
+ expect(stderr).to match /possible misspelling of `@user'/i
35
+
36
+ # Shows where the method was called
37
+ expect(stderr).to include '"Dear, #{@uesr.name}, <3 <3 <3"'
38
+
39
+ # MAYBE: shows where the correctly spelled variable was set
40
+
41
+ # typical backtrace
42
+ expect(stderr).to include 'misspelled_ivar.rb:10'
43
+ end
44
+ end
data/spec/config_spec.rb CHANGED
@@ -20,11 +20,13 @@ RSpec.describe 'configuration', config: true do
20
20
 
21
21
  # helper methods
22
22
  def yes_accept!(config, ex)
23
- expect(config.accept? ex).to eq true
23
+ binding = nil
24
+ expect(config.accept? ex, binding).to eq true
24
25
  end
25
26
 
26
27
  def no_accept!(config, ex)
27
- expect(config.accept? ex).to eq false
28
+ binding = nil
29
+ expect(config.accept? ex, binding).to eq false
28
30
  end
29
31
 
30
32
  def config_for(attrs)
@@ -72,18 +74,20 @@ RSpec.describe 'configuration', config: true do
72
74
  end
73
75
 
74
76
  describe 'finding the heuristic for an exception' do
75
- it 'raises an ArgumentError if given an acception that it won\'t accept' do
77
+ it 'raises an ArgumentError if given an ecception that it won\'t accept' do
76
78
  config = config_for blacklist: allow_none, heuristics: [match_all]
77
- expect { config.heuristic_for "not an error" }
79
+ binding = nil
80
+ expect { config.heuristic_for "not an error", binding }
78
81
  .to raise_error ArgumentError, /"not an error"/
79
82
  end
80
83
 
81
84
  it 'finds the first heuristic that is willing to accept it' do
85
+ binding = nil
82
86
  config = config_for blacklist: allow_all,
83
87
  heuristics: [match_no_method_error, match_all]
84
88
  exception = capture { sdfsdfsdf() }
85
- expect(config.heuristic_for exception).to be_a_kind_of match_no_method_error
86
- expect(config.heuristic_for exception).to_not be_a_kind_of match_all
89
+ expect(config.heuristic_for exception, binding).to be_a_kind_of match_no_method_error
90
+ expect(config.heuristic_for exception, binding).to_not be_a_kind_of match_all
87
91
  end
88
92
  end
89
93
 
@@ -92,11 +96,12 @@ RSpec.describe 'configuration', config: true do
92
96
 
93
97
  describe 'blacklist' do
94
98
  it 'doesn\'t accept a SystemExit' do
99
+ binding = nil
95
100
  system_exit = capture { exit 1 }
96
- expect(default_config.accept? system_exit).to eq false
101
+ expect(default_config.accept? system_exit, binding).to eq false
97
102
 
98
103
  generic_exception = capture { raise }
99
- expect(default_config.accept? generic_exception).to eq true
104
+ expect(default_config.accept? generic_exception, binding).to eq true
100
105
  end
101
106
  end
102
107
 
@@ -22,4 +22,54 @@ RSpec.describe 'Heuristic for a NoMethodError', heuristic: true do
22
22
  extracts_method_name! '<', "undefined method `<' for nil:NilClass"
23
23
  extracts_method_name! "ab `c' d", "undefined method `ab `c' d' for main:Object"
24
24
  end
25
+
26
+ describe 'on nil' do
27
+ describe 'for an instance variable' do
28
+ it 'suggest a closely spelled variable name if one exists' do
29
+ @abcd = 123
30
+ err = nil
31
+ begin
32
+ @abce.even?
33
+ rescue NoMethodError => no_method_error
34
+ err = no_method_error
35
+ end
36
+
37
+ heuristic = heuristic_class.new project: build_default_project,
38
+ einfo: einfo_for(err, binding)
39
+ expect(heuristic.semantic_explanation).to match /@abcd/
40
+ expect(heuristic.semantic_explanation).to match /@abce/
41
+ expect(heuristic.semantic_explanation).to match /spell/
42
+ end
43
+
44
+ it 'does not suggest a misspelling when there is no spelled variable' do
45
+ @abcd = 123
46
+ err = nil
47
+ begin
48
+ @ablol.even?
49
+ rescue NoMethodError => no_method_error
50
+ err = no_method_error
51
+ end
52
+
53
+ heuristic = heuristic_class.new project: build_default_project,
54
+ einfo: einfo_for(err, binding)
55
+ expect(heuristic.semantic_explanation).to_not match /spell/
56
+ expect(heuristic.semantic_explanation).to_not match /@abcd/
57
+ end
58
+
59
+ it 'doesn\'t suggest this when there is no binding provided' do
60
+ @abcd = 123
61
+ err = nil
62
+ begin
63
+ @abce.even?
64
+ rescue NoMethodError => no_method_error
65
+ err = no_method_error
66
+ end
67
+
68
+ heuristic = heuristic_class.new project: build_default_project,
69
+ einfo: einfo_for(err, nil)
70
+ expect(heuristic.semantic_explanation).to_not match /spell/
71
+ expect(heuristic.semantic_explanation).to_not match /@abcd/
72
+ end
73
+ end
74
+ end
25
75
  end
@@ -10,7 +10,8 @@ module HeuristicSpecHelpers
10
10
 
11
11
  def heuristic_for(attributes={})
12
12
  heuristic_class.new project: build_default_project(attributes),
13
- einfo: einfo_for(FakeException.new attributes)
13
+ einfo: einfo_for(FakeException.new(attributes),
14
+ attributes[:binding])
14
15
  end
15
16
 
16
17
  def build_default_project(attributes={})
@@ -78,7 +78,7 @@ RSpec.describe 'heuristics for the WrongNumberOfArguments', heuristic: true do
78
78
  context: (-5..5),
79
79
  emphasis: :code,
80
80
  location: ErrorToCommunicate::ExceptionInfo::Location.new(
81
- path: '/a', linenum: 1, label: 'b'
81
+ path: '/a', linenum: 1, label: 'b', binding: nil
82
82
  )
83
83
  end
84
84
 
@@ -4,7 +4,7 @@ require 'heuristic/spec_helper'
4
4
 
5
5
  RSpec.describe 'Heuristic', heuristic: true do
6
6
  let(:einfo) { ErrorToCommunicate::ExceptionInfo.new classname: 'the classname', message: 'the message', backtrace: [
7
- ErrorToCommunicate::ExceptionInfo::Location.new(path: 'file', linenum: 12, label: 'a')
7
+ ErrorToCommunicate::ExceptionInfo::Location.new(path: 'file', linenum: 12, label: 'a', binding: nil)
8
8
  ]
9
9
  }
10
10
  let(:subclass) { Class.new ErrorToCommunicate::Heuristic }
@@ -0,0 +1,64 @@
1
+ require 'error_to_communicate/levenshtein'
2
+
3
+ RSpec.describe ErrorToCommunicate::Levenshtein do
4
+ def assert_distance(distance, target, actual)
5
+ expect(described_class.call target, actual).to eq distance
6
+ end
7
+
8
+ specify 'count one distance for each substitution' do
9
+ assert_distance 1, "kitten", "sitten" # "s" for "k"
10
+ assert_distance 1, "sitten", "sittin" # "i" for "e"
11
+ assert_distance 2, "sitten", "sitxin" # "i" for "e", and "x" for "t"
12
+ end
13
+
14
+ example 'count one distance for each addition' do
15
+ assert_distance 1, "sittin", "sitting" # "g"
16
+ assert_distance 2, "sitin", "sitting" # "t", and "g"
17
+ assert_distance 2, "cat", "catch" # "ch"
18
+ end
19
+
20
+ example 'count one distance for each deletion' do
21
+ assert_distance 1, "sitting", "sittin" # "g"
22
+ assert_distance 2, "sitting", "sitin" # "t", and "g"
23
+ assert_distance 2, "catch", "cat" # "ch"
24
+ end
25
+
26
+ example 'add the various numbers' do
27
+ # add "a" at beginning
28
+ # delete "h" from end
29
+ # swap "X" to "d"
30
+ assert_distance 3, "abcdefg", "bcXefgh"
31
+ end
32
+
33
+ describe 'some edge cases' do
34
+ [ ['--a', 'a', 2],
35
+ ['a', '--a', 2],
36
+ ['-a-', 'a', 2],
37
+ ['a', '-a-', 2],
38
+ ['---', 'a', 3],
39
+ ['a', '---', 3],
40
+ ['aaa', 'bbb', 3],
41
+ ['bbb', 'aaa', 3],
42
+ ].each do |s1, s2, distance|
43
+ example "distance(#{s1.inspect}, #{s2.inspect}) # => #{distance.inspect}" do
44
+ assert_distance distance, s1, s2
45
+ end
46
+ end
47
+ end
48
+
49
+ specify 'it is not crazy stupid slow' do
50
+ # When comparing "aaa", "bbb",
51
+ # This algorithm compares 94 pairs of strings.
52
+ # When those pairs are uniqued, there are only 16 left,
53
+ # so it is doing 78 redundant steps.
54
+ #
55
+ # At length 4 ("aaaa", "bbbb"), it does 481 comparisons instead of 25
56
+ # At length 5, 2524 instead of 36
57
+ # At length 10, 12_146_179 instead of 121, and it took 52.585554 seconds
58
+ n = 100
59
+ start = Time.now
60
+ assert_distance n, "a"*n, "b"*n
61
+ time = Time.now - start
62
+ expect(time).to be < 1
63
+ end
64
+ end
@@ -63,7 +63,8 @@ RSpec.describe 'Parsing exceptions to ExceptionInfo', einfo: true do
63
63
  # it 'records the relative filepath if it cannot fild the file'
64
64
 
65
65
  def assert_parses_line(line, assertions)
66
- parsed = ErrorToCommunicate::ExceptionInfo::Location.parse(line)
66
+ binding = nil
67
+ parsed = ErrorToCommunicate::ExceptionInfo::Location.parse(line, binding)
67
68
  assertions.each do |method_name, expected|
68
69
  expected = Pathname.new expected if method_name == :path
69
70
  actual = parsed.__send__ method_name
@@ -85,11 +86,14 @@ RSpec.describe 'Parsing exceptions to ExceptionInfo', einfo: true do
85
86
 
86
87
  context 'fake backtraces (eg RSpec renders text in the `formatted_backtrace` to get it to print messages there)' do
87
88
  it 'has an empty path, linenum of -1, the entire string is the label' do
88
- a, b, c = parsed = ErrorToCommunicate::ExceptionInfo.parse_backtrace([
89
- "/Users/josh/.gem/ruby/2.1.1/gems/rspec-core-3.2.3/lib/rspec/core/runner.rb:29:in `block in autorun'",
90
- "",
91
- " Showing full backtrace because every line was filtered out.",
92
- ])
89
+ binding = nil
90
+ a, b, c = parsed = ErrorToCommunicate::ExceptionInfo.parse_backtrace(
91
+ [ "/Users/josh/.gem/ruby/2.1.1/gems/rspec-core-3.2.3/lib/rspec/core/runner.rb:29:in `block in autorun'",
92
+ "",
93
+ " Showing full backtrace because every line was filtered out.",
94
+ ],
95
+ binding
96
+ )
93
97
 
94
98
  expect(parsed.map(&:path).map(&:to_s)).to eq [
95
99
  "/Users/josh/.gem/ruby/2.1.1/gems/rspec-core-3.2.3/lib/rspec/core/runner.rb",
@@ -158,6 +162,7 @@ RSpec.describe 'Parsing exceptions to ExceptionInfo', einfo: true do
158
162
 
159
163
  describe 'ExceptionInfo::Location' do
160
164
  def location_for(attrs)
165
+ attrs[:binding] ||= nil
161
166
  ErrorToCommunicate::ExceptionInfo::Location.new attrs
162
167
  end
163
168
 
@@ -122,7 +122,8 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
122
122
  heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
123
123
  config: ErrorToCommunicate::Config.default,
124
124
  failure: failure_for(description: 'DESC', type: :assertion),
125
- failure_number: 999
125
+ failure_number: 999,
126
+ binding: binding
126
127
  summaryname, ((columnsname, *columns)) = heuristic.semantic_summary
127
128
  expect(summaryname).to eq :summary
128
129
  expect(columnsname).to eq :columns
@@ -132,9 +133,10 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
132
133
  specify 'the info is the formatted error message and first line from the backtrace with some context' do
133
134
  config = ErrorToCommunicate::Config.new
134
135
  heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
135
- config: ErrorToCommunicate::Config.default,
136
- failure: failure_for(type: :assertion, message: 'MESSAGE', formatted_backtrace: ["/file:123:in `method'"]),
137
- failure_number: 999
136
+ config: ErrorToCommunicate::Config.default,
137
+ failure: failure_for(type: :assertion, message: 'MESSAGE', formatted_backtrace: ["/file:123:in `method'"]),
138
+ failure_number: 999,
139
+ binding: binding
138
140
  heuristicname, ((messagename, message), (codename, codeattrs), *rest) = heuristic.semantic_info
139
141
  expect(heuristicname).to eq :heuristic
140
142
  expect(messagename ).to eq :message
@@ -151,7 +153,8 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
151
153
  heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
152
154
  config: ErrorToCommunicate::Config.default,
153
155
  failure_number: 999,
154
- failure: failure_for(type: :assertion, message: 'MESSAGE', formatted_backtrace: [])
156
+ failure: failure_for(type: :assertion, message: 'MESSAGE', formatted_backtrace: []),
157
+ binding: binding
155
158
  heuristicname, ((messagename, message), *rest) = heuristic.semantic_info
156
159
  expect(heuristicname).to eq :heuristic
157
160
  expect(messagename ).to eq :message
@@ -180,7 +183,8 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
180
183
  failure: failure_for(message: "wrong number of arguments (1 for 0)",
181
184
  description: 'DESC',
182
185
  type: :argument_error),
183
- failure_number: 999
186
+ failure_number: 999,
187
+ binding: binding
184
188
  summaryname, ((columnsname, *columns)) = heuristic.semantic_summary
185
189
  expect(summaryname).to eq :summary
186
190
  expect(columnsname).to eq :columns
@@ -193,12 +197,34 @@ RSpec.describe ErrorToCommunicate::RSpecFormatter, rspec_formatter: true do
193
197
  .to receive(:semantic_info).and_return("SEMANTICINFO")
194
198
 
195
199
  heuristic = ErrorToCommunicate::Heuristic::RSpecFailure.new \
196
- config: ErrorToCommunicate::Config.new,
197
- failure: failure_for(message: "wrong number of arguments (1 for 0)", type: :argument_error),
198
- failure_number: 999
200
+ config: ErrorToCommunicate::Config.new,
201
+ failure: failure_for(message: "wrong number of arguments (1 for 0)", type: :argument_error),
202
+ failure_number: 999,
203
+ binding: binding
199
204
 
200
205
  expect(heuristic.semantic_info).to eq "SEMANTICINFO"
201
206
  end
207
+
208
+ it 'provides the bindings needed for some of the advanced analysis' do
209
+ formatter = new_formatter
210
+ context_around_failure = this_line_of_code
211
+ run_specs_against formatter do
212
+ example('suggests better name1') {
213
+ @abc = 123
214
+ @abd.even? # misspelled
215
+ }
216
+ example('suggests better name2') {
217
+ @lol = 123
218
+ @lul.even? # misspelled
219
+ }
220
+ end
221
+
222
+
223
+ # Sigh the above whitespace is important, or it can pass because the assertion is in the code displayed for context
224
+ # It's stupid, I know
225
+ expect(get_printed formatter).to include "Possible misspelling of `@lol'"
226
+ expect(get_printed formatter).to include "Possible misspelling of `@abc'"
227
+ end
202
228
  end
203
229
 
204
230
  context 'fixing the message\'s whitespace' do
data/spec/spec_helper.rb CHANGED
@@ -12,8 +12,8 @@ class FakeException
12
12
  end
13
13
 
14
14
  module SpecHelpers
15
- def einfo_for(exception)
16
- ErrorToCommunicate::ExceptionInfo.parse exception
15
+ def einfo_for(exception, binding=nil)
16
+ ErrorToCommunicate::ExceptionInfo.parse exception, binding
17
17
  end
18
18
 
19
19
  def trap_warnings
@@ -11,8 +11,8 @@ Gem::Specification.new do |s|
11
11
  s.homepage = 'https://github.com/JoshCheek/what-we-ve-got-here-is-an-error-to-communicate'
12
12
 
13
13
  s.add_runtime_dependency 'rouge', '~> 1.8', '!= 1.9.1'
14
- # s.add_runtime_dependency 'interception', '~> 0.5'
15
- # s.add_runtime_dependency 'binding_of_caller', '~> 0.7.2'
14
+ s.add_runtime_dependency 'interception', '~> 0.5'
15
+ s.add_runtime_dependency 'binding_of_caller', '~> 0.7.2'
16
16
 
17
17
  s.add_development_dependency 'rspec', '~> 3.2'
18
18
  s.add_development_dependency 'haiti', '< 0.3', '>= 0.2.0'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: what_weve_got_here_is_an_error_to_communicate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Cheek
@@ -9,94 +9,122 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-08-17 00:00:00.000000000 Z
12
+ date: 2015-12-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rouge
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - ~>
18
+ - - "~>"
19
19
  - !ruby/object:Gem::Version
20
20
  version: '1.8'
21
- - - ! '!='
21
+ - - "!="
22
22
  - !ruby/object:Gem::Version
23
23
  version: 1.9.1
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
27
27
  requirements:
28
- - - ~>
28
+ - - "~>"
29
29
  - !ruby/object:Gem::Version
30
30
  version: '1.8'
31
- - - ! '!='
31
+ - - "!="
32
32
  - !ruby/object:Gem::Version
33
33
  version: 1.9.1
34
+ - !ruby/object:Gem::Dependency
35
+ name: interception
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.5'
48
+ - !ruby/object:Gem::Dependency
49
+ name: binding_of_caller
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.7.2
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.7.2
34
62
  - !ruby/object:Gem::Dependency
35
63
  name: rspec
36
64
  requirement: !ruby/object:Gem::Requirement
37
65
  requirements:
38
- - - ~>
66
+ - - "~>"
39
67
  - !ruby/object:Gem::Version
40
68
  version: '3.2'
41
69
  type: :development
42
70
  prerelease: false
43
71
  version_requirements: !ruby/object:Gem::Requirement
44
72
  requirements:
45
- - - ~>
73
+ - - "~>"
46
74
  - !ruby/object:Gem::Version
47
75
  version: '3.2'
48
76
  - !ruby/object:Gem::Dependency
49
77
  name: haiti
50
78
  requirement: !ruby/object:Gem::Requirement
51
79
  requirements:
52
- - - <
80
+ - - "<"
53
81
  - !ruby/object:Gem::Version
54
82
  version: '0.3'
55
- - - ! '>='
83
+ - - ">="
56
84
  - !ruby/object:Gem::Version
57
85
  version: 0.2.0
58
86
  type: :development
59
87
  prerelease: false
60
88
  version_requirements: !ruby/object:Gem::Requirement
61
89
  requirements:
62
- - - <
90
+ - - "<"
63
91
  - !ruby/object:Gem::Version
64
92
  version: '0.3'
65
- - - ! '>='
93
+ - - ">="
66
94
  - !ruby/object:Gem::Version
67
95
  version: 0.2.0
68
96
  - !ruby/object:Gem::Dependency
69
97
  name: pry
70
98
  requirement: !ruby/object:Gem::Requirement
71
99
  requirements:
72
- - - <
100
+ - - "<"
73
101
  - !ruby/object:Gem::Version
74
102
  version: '0.11'
75
- - - ! '>='
103
+ - - ">="
76
104
  - !ruby/object:Gem::Version
77
105
  version: 0.10.0
78
106
  type: :development
79
107
  prerelease: false
80
108
  version_requirements: !ruby/object:Gem::Requirement
81
109
  requirements:
82
- - - <
110
+ - - "<"
83
111
  - !ruby/object:Gem::Version
84
112
  version: '0.11'
85
- - - ! '>='
113
+ - - ">="
86
114
  - !ruby/object:Gem::Version
87
115
  version: 0.10.0
88
116
  - !ruby/object:Gem::Dependency
89
117
  name: rake
90
118
  requirement: !ruby/object:Gem::Requirement
91
119
  requirements:
92
- - - ~>
120
+ - - "~>"
93
121
  - !ruby/object:Gem::Version
94
122
  version: '10.4'
95
123
  type: :development
96
124
  prerelease: false
97
125
  version_requirements: !ruby/object:Gem::Requirement
98
126
  requirements:
99
- - - ~>
127
+ - - "~>"
100
128
  - !ruby/object:Gem::Version
101
129
  version: '10.4'
102
130
  description: Hooks into program lifecycle to display error messages to you in a helpufl
@@ -106,9 +134,9 @@ executables: []
106
134
  extensions: []
107
135
  extra_rdoc_files: []
108
136
  files:
109
- - .gitignore
110
- - .rspec
111
- - .travis.yml
137
+ - ".gitignore"
138
+ - ".rspec"
139
+ - ".travis.yml"
112
140
  - Gemfile
113
141
  - Rakefile
114
142
  - Readme.md
@@ -129,6 +157,7 @@ files:
129
157
  - lib/error_to_communicate/heuristic/no_method_error.rb
130
158
  - lib/error_to_communicate/heuristic/syntax_error.rb
131
159
  - lib/error_to_communicate/heuristic/wrong_number_of_arguments.rb
160
+ - lib/error_to_communicate/levenshtein.rb
132
161
  - lib/error_to_communicate/project.rb
133
162
  - lib/error_to_communicate/rspec_formatter.rb
134
163
  - lib/error_to_communicate/theme.rb
@@ -144,6 +173,7 @@ files:
144
173
  - spec/acceptance/short_and_long_require_spec.rb
145
174
  - spec/acceptance/spec_helper.rb
146
175
  - spec/acceptance/syntax_error_spec.rb
176
+ - spec/acceptance/unexpected_nil_spec.rb
147
177
  - spec/acceptance/wrong_number_of_arguments_spec.rb
148
178
  - spec/config_spec.rb
149
179
  - spec/heuristic/exception_spec.rb
@@ -152,6 +182,7 @@ files:
152
182
  - spec/heuristic/spec_helper.rb
153
183
  - spec/heuristic/wrong_number_of_arguments_spec.rb
154
184
  - spec/heuristic_spec.rb
185
+ - spec/levenshtein_spec.rb
155
186
  - spec/parsing_exception_info_spec.rb
156
187
  - spec/rspec_formatter_spec.rb
157
188
  - spec/spec_helper.rb
@@ -166,17 +197,17 @@ require_paths:
166
197
  - lib
167
198
  required_ruby_version: !ruby/object:Gem::Requirement
168
199
  requirements:
169
- - - ! '>='
200
+ - - ">="
170
201
  - !ruby/object:Gem::Version
171
202
  version: '0'
172
203
  required_rubygems_version: !ruby/object:Gem::Requirement
173
204
  requirements:
174
- - - ! '>='
205
+ - - ">="
175
206
  - !ruby/object:Gem::Version
176
207
  version: '0'
177
208
  requirements: []
178
209
  rubyforge_project:
179
- rubygems_version: 2.4.1
210
+ rubygems_version: 2.4.8
180
211
  signing_key:
181
212
  specification_version: 4
182
213
  summary: Readable, helpful error messages