what_weve_got_here_is_an_error_to_communicate 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
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