did_you_mean 0.9.10-java → 0.10.0-java

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.travis.yml +13 -19
  4. data/CHANGELOG.md +229 -0
  5. data/Gemfile +4 -7
  6. data/README.md +5 -16
  7. data/Rakefile +35 -33
  8. data/benchmark/jaro_winkler/memory_usage.rb +12 -0
  9. data/benchmark/jaro_winkler/speed.rb +14 -0
  10. data/benchmark/levenshtein/memory_usage.rb +12 -0
  11. data/benchmark/levenshtein/speed.rb +17 -0
  12. data/benchmark/memory_usage.rb +31 -0
  13. data/did_you_mean.gemspec +9 -2
  14. data/evaluation/calculator.rb +122 -0
  15. data/evaluation/dictionary_generator.rb +36 -0
  16. data/evaluation/incorrect_words.yaml +1159 -0
  17. data/ext/did_you_mean/extconf.rb +1 -1
  18. data/ext/did_you_mean/{method_missing.c → method_receiver.c} +1 -1
  19. data/lib/did_you_mean.rb +22 -3
  20. data/lib/did_you_mean/core_ext/name_error.rb +30 -22
  21. data/lib/did_you_mean/core_ext/no_method_error.rb +13 -2
  22. data/lib/did_you_mean/core_ext/rubinius.rb +16 -0
  23. data/lib/did_you_mean/finders.rb +48 -17
  24. data/lib/did_you_mean/finders/method_finder.rb +62 -0
  25. data/lib/did_you_mean/finders/name_error_finders.rb +8 -11
  26. data/lib/did_you_mean/finders/name_error_finders/class_finder.rb +77 -0
  27. data/lib/did_you_mean/finders/name_error_finders/name_finder.rb +18 -0
  28. data/lib/did_you_mean/formatter.rb +20 -0
  29. data/lib/did_you_mean/jaro_winkler.rb +87 -0
  30. data/lib/did_you_mean/test_helper.rb +1 -1
  31. data/lib/did_you_mean/version.rb +1 -1
  32. data/test/core_ext/name_error_extension_test.rb +74 -0
  33. data/test/{no_method_error_extension_test.rb → core_ext/no_method_error_extension_test.rb} +1 -1
  34. data/test/correctable/class_name_test.rb +65 -0
  35. data/test/correctable/method_name_test.rb +84 -0
  36. data/test/{null_finder_test.rb → correctable/uncorrectable_name_test.rb} +3 -7
  37. data/test/correctable/variable_name_test.rb +79 -0
  38. data/test/edit_distance/jaro_winkler_test.rb +30 -0
  39. data/test/test_helper.rb +2 -20
  40. data/test/word_collection_test.rb +76 -12
  41. metadata +58 -56
  42. data/Appraisals +0 -23
  43. data/gemfiles/activerecord_32.gemfile +0 -21
  44. data/gemfiles/activerecord_40.gemfile +0 -21
  45. data/gemfiles/activerecord_41.gemfile +0 -21
  46. data/gemfiles/activerecord_42.gemfile +0 -21
  47. data/gemfiles/activerecord_edge.gemfile +0 -25
  48. data/lib/did_you_mean/finders/name_error_finders/similar_class_finder.rb +0 -50
  49. data/lib/did_you_mean/finders/name_error_finders/similar_name_finder.rb +0 -61
  50. data/lib/did_you_mean/finders/similar_attribute_finder.rb +0 -26
  51. data/lib/did_you_mean/finders/similar_method_finder.rb +0 -34
  52. data/lib/did_you_mean/word_collection.rb +0 -30
  53. data/test/all_test.rb +0 -18
  54. data/test/name_error_extension_test.rb +0 -73
  55. data/test/similar_attribute_finder_test.rb +0 -17
  56. data/test/similar_class_finder_test.rb +0 -85
  57. data/test/similar_method_finder_test.rb +0 -60
  58. data/test/similar_name_finder_test.rb +0 -62
@@ -1,2 +1,2 @@
1
1
  require 'mkmf'
2
- create_makefile 'did_you_mean/method_missing'
2
+ create_makefile 'did_you_mean/method_receiver'
@@ -11,7 +11,7 @@ name_err_receiver(VALUE self)
11
11
  }
12
12
 
13
13
  void
14
- Init_method_missing()
14
+ Init_method_receiver()
15
15
  {
16
16
  VALUE err_mesg = rb_funcall(rb_cNameErrorMesg, '!', 3, Qnil, Qnil, Qnil);
17
17
  type = RTYPEDDATA(err_mesg)->type;
data/lib/did_you_mean.rb CHANGED
@@ -4,14 +4,33 @@ require "did_you_mean/version"
4
4
  require "did_you_mean/core_ext/name_error"
5
5
  require "did_you_mean/core_ext/no_method_error"
6
6
  require "did_you_mean/finders"
7
+ require "did_you_mean/formatter"
7
8
 
8
9
  module DidYouMean
9
- Interception.listen(->(exception, binding) {
10
+ Interception.listen do |exception, binding|
10
11
  # On IRB/pry console, this event is called twice. In the second event,
11
12
  # we get IRB/pry binding. So it shouldn't override @frame_binding if
12
13
  # it's already defined.
13
- if exception.is_a?(NameError) && !exception.instance_variable_defined?(:@frame_binding)
14
+ if DidYouMean.finders.include?(exception.class.to_s) && !exception.instance_variable_defined?(:@frame_binding)
14
15
  exception.instance_variable_set(:@frame_binding, binding)
15
16
  end
16
- })
17
+ end
18
+
19
+ def self.finders
20
+ @@finders ||= Hash.new(NullFinder)
21
+ end
22
+
23
+ finders.merge!("NameError" => NameErrorFinders)
24
+
25
+ case RUBY_ENGINE
26
+ when 'ruby', 'jruby'
27
+ finders["NoMethodError"] = MethodFinder
28
+ when 'rbx'
29
+ finders["NoMethodError"] =
30
+ if (___ rescue $!).class.to_s == "NameError" # For rbx > 2.5.0
31
+ MethodFinder
32
+ else
33
+ MethodFinder::RubiniusSupport # For rbx < 2.5.0
34
+ end
35
+ end
17
36
  end
@@ -1,30 +1,38 @@
1
- class NameError
2
- attr_reader :frame_binding
1
+ module DidYouMean
2
+ module Correctable
3
+ attr_reader :frame_binding
3
4
 
4
- IGNORED_CALLERS = [
5
- /( |`)missing_name'/,
6
- /( |`)safe_constantize'/
7
- ].freeze
8
- private_constant :IGNORED_CALLERS
5
+ IGNORED_CALLERS = [
6
+ /( |`)missing_name'/,
7
+ /( |`)safe_constantize'/
8
+ ].freeze
9
+ private_constant :IGNORED_CALLERS
9
10
 
10
- def to_s_with_did_you_mean
11
- msg = original_message.dup
12
- bt = caller.first(6)
11
+ def self.included(klass)
12
+ klass.class_eval do
13
+ __to_s__ = klass.instance_method(:to_s)
14
+ define_method(:original_message){ __to_s__.bind(self).call }
13
15
 
14
- msg << did_you_mean?.to_s if IGNORED_CALLERS.all? {|ignored| bt.grep(ignored).empty? }
15
- msg
16
- rescue
17
- original_message
18
- end
16
+ def to_s
17
+ msg = original_message.dup
18
+ bt = caller.first(6)
19
19
 
20
- alias original_message to_s
21
- alias to_s to_s_with_did_you_mean
20
+ msg << Formatter.new(suggestions).to_s if IGNORED_CALLERS.all? {|ignored| bt.grep(ignored).empty? }
21
+ msg
22
+ rescue
23
+ original_message
24
+ end
25
+ end
26
+ end
22
27
 
23
- def did_you_mean?
24
- finder.did_you_mean?
25
- end
28
+ def suggestions
29
+ finder.suggestions
30
+ end
26
31
 
27
- def finder
28
- @finder ||= DidYouMean.finders[self.class.to_s].new(self)
32
+ def finder
33
+ @finder ||= DidYouMean.finders[self.class.to_s].new(self)
34
+ end
29
35
  end
30
36
  end
37
+
38
+ NameError.send(:include, DidYouMean::Correctable)
@@ -1,7 +1,14 @@
1
1
  case RUBY_ENGINE
2
2
  when 'ruby'
3
- require 'did_you_mean/method_missing'
3
+ name_error = begin
4
+ raise_name_error
5
+ rescue NameError => e
6
+ e
7
+ end
4
8
 
9
+ unless name_error.respond_to?(:receiver)
10
+ require 'did_you_mean/method_receiver'
11
+ end
5
12
  when 'jruby'
6
13
  NoMethodError.class_eval do
7
14
  def to_s
@@ -15,7 +22,7 @@ when 'jruby'
15
22
  field.setAccessible(true)
16
23
  field.get(__message__)
17
24
  rescue
18
- super
25
+ nil
19
26
  end
20
27
  end
21
28
 
@@ -32,4 +39,8 @@ when 'jruby'
32
39
  end
33
40
  end
34
41
  end
42
+
43
+ when 'rbx'
44
+ require 'did_you_mean/core_ext/rubinius'
45
+ NoMethodError.class_eval { attr_reader :receiver }
35
46
  end
@@ -0,0 +1,16 @@
1
+ if defined?(Rubinius)
2
+ class << Rubinius
3
+ alias raise_with_no_receiver_capturer raise_exception
4
+
5
+ def raise_exception(exc)
6
+ if exc.is_a?(NoMethodError)
7
+ bt = Rubinius::VM.backtrace(0, true).detect do |x|
8
+ x.method.name == :method_missing
9
+ end
10
+ exc.instance_variable_set(:@receiver, bt.variables.self) if bt
11
+ end
12
+
13
+ raise_with_no_receiver_capturer(exc)
14
+ end
15
+ end
16
+ end
@@ -1,31 +1,62 @@
1
- require "did_you_mean/word_collection"
1
+ require "did_you_mean/levenshtein"
2
+ require "did_you_mean/jaro_winkler"
2
3
 
3
4
  module DidYouMean
4
5
  module BaseFinder
5
- def did_you_mean?
6
- return if similar_words.empty?
6
+ AT = "@".freeze
7
+ EMPTY = "".freeze
7
8
 
8
- output = "\n\n"
9
- output << " Did you mean? #{format(similar_words.first)}\n"
10
- output << similar_words.drop(1).map{|word| "#{' ' * 18}#{format(word)}\n" }.join
11
- output << " " # for pry
9
+ def suggestions
10
+ @suggestions ||= searches.flat_map do |input, candidates|
11
+ input = normalize(input)
12
+ threshold = input.length > 3 ? 0.834 : 0.77
13
+
14
+ seed = candidates.select {|candidate| JaroWinkler.distance(normalize(candidate), input) >= threshold }
15
+ .sort_by! {|candidate| JaroWinkler.distance(candidate.to_s, input) }
16
+ .reverse!
17
+
18
+ # Correct mistypes
19
+ threshold = (input.length * 0.25).ceil
20
+ corrections = seed.select {|c| Levenshtein.distance(normalize(c), input) <= threshold }
21
+
22
+ # Correct misspells
23
+ if corrections.empty?
24
+ corrections = seed.select do |candidate|
25
+ candidate = normalize(candidate)
26
+ length = input.length < candidate.length ? input.length : candidate.length
27
+
28
+ Levenshtein.distance(candidate, input) < length
29
+ end.first(1)
30
+ end
31
+
32
+ corrections
33
+ end
12
34
  end
13
35
 
14
- def similar_words
15
- @similar_words ||= WordCollection.new(words).similar_to(target_word)
36
+ def searches
37
+ raise NotImplementedError
16
38
  end
17
- end
18
39
 
19
- class NullFinder
20
- def initialize(*); end
21
- def did_you_mean?; end
40
+ private
41
+
42
+ def normalize(str_or_symbol) #:nodoc:
43
+ str = if str_or_symbol.is_a?(String)
44
+ str_or_symbol.dup
45
+ else
46
+ str_or_symbol.to_s
47
+ end
48
+
49
+ str.downcase!
50
+ str.tr!(AT, EMPTY)
51
+ str
52
+ end
22
53
  end
23
54
 
24
- def self.finders
25
- @@finders ||= Hash.new(NullFinder)
55
+ class NullFinder
56
+ def initialize(*); end
57
+ def suggestions; [] end
26
58
  end
27
59
  end
28
60
 
29
61
  require 'did_you_mean/finders/name_error_finders'
30
- require 'did_you_mean/finders/similar_attribute_finder'
31
- require 'did_you_mean/finders/similar_method_finder'
62
+ require 'did_you_mean/finders/method_finder'
@@ -0,0 +1,62 @@
1
+ module DidYouMean
2
+ class MethodFinder
3
+ include BaseFinder
4
+ attr_reader :method_name, :receiver
5
+
6
+ def initialize(exception)
7
+ @method_name = exception.name
8
+ @receiver = exception.receiver
9
+ @binding = exception.frame_binding
10
+ @location = exception.backtrace.first
11
+ @ivar_names = NameFinder.new(exception).ivar_names
12
+ end
13
+
14
+ def searches
15
+ {
16
+ method_name => method_names,
17
+ receiver_name.to_s => @ivar_names
18
+ }
19
+ end
20
+
21
+ def method_names
22
+ method_names = receiver.methods + receiver.singleton_methods
23
+ method_names += receiver.private_methods if receiver.equal?(@binding.eval("self"))
24
+ method_names.delete(method_name)
25
+ method_names.uniq!
26
+ method_names
27
+ end
28
+
29
+ def receiver_name
30
+ return unless @receiver.nil?
31
+
32
+ abs_path, lineno, label =
33
+ /(.*):(.*):in `(.*)'/ =~ @location && [$1, $2.to_i, $3]
34
+
35
+ line =
36
+ case abs_path
37
+ when "(irb)"
38
+ Readline::HISTORY.to_a.last
39
+ when "(pry)"
40
+ ::Pry.history.to_a.last
41
+ else
42
+ File.open(abs_path) do |file|
43
+ file.detect { file.lineno == lineno }
44
+ end if File.exist?(abs_path)
45
+ end
46
+
47
+ /@(\w+)*\.#{@method_name}/ =~ line.to_s && $1
48
+ end
49
+ end
50
+
51
+ if RUBY_ENGINE == 'rbx'
52
+ module MethodFinder::RubiniusSupport
53
+ def self.new(exception)
54
+ if exception.receiver === exception.frame_binding.eval("self")
55
+ NameErrorFinders.new(exception)
56
+ else
57
+ MethodFinder.new(exception)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -5,20 +5,17 @@ module DidYouMean
5
5
  end
6
6
 
7
7
  def self.new(exception)
8
- klass = if /uninitialized constant/ =~ exception.original_message
9
- SimilarClassFinder
10
- elsif /undefined local variable or method/ =~ exception.original_message
11
- SimilarNameFinder
8
+ case exception.original_message
9
+ when /uninitialized constant/
10
+ ClassFinder
11
+ when /undefined local variable or method/, /undefined method/, /uninitialized class variable/
12
+ NameFinder
12
13
  else
13
14
  NullFinder
14
- end
15
-
16
- klass.new(exception)
15
+ end.new(exception)
17
16
  end
18
17
  end
19
-
20
- finders["NameError"] = NameErrorFinders
21
18
  end
22
19
 
23
- require 'did_you_mean/finders/name_error_finders/similar_name_finder'
24
- require 'did_you_mean/finders/name_error_finders/similar_class_finder'
20
+ require 'did_you_mean/finders/name_error_finders/name_finder'
21
+ require 'did_you_mean/finders/name_error_finders/class_finder'
@@ -0,0 +1,77 @@
1
+ require 'delegate'
2
+
3
+ module DidYouMean
4
+ class ClassFinder
5
+ include BaseFinder
6
+ attr_reader :class_name, :original_message
7
+
8
+ def initialize(exception)
9
+ @class_name, @original_message = exception.name, exception.original_message
10
+ end
11
+
12
+ def searches
13
+ {name_from_message => class_names}
14
+ end
15
+
16
+ def class_names
17
+ scopes.flat_map do |scope|
18
+ scope.constants.map do |c|
19
+ ClassName.new(c, scope == Object ? EMPTY : "#{scope}::")
20
+ end
21
+ end
22
+ end
23
+
24
+ if RUBY_ENGINE == 'jruby'
25
+ # Always use the original error message to retrieve the user
26
+ # input since JRuby 1.7 behaves differently from MRI/Rubinius.
27
+ #
28
+ # class Name; end
29
+ # error = (Name::DoesNotExist rescue $!)
30
+ #
31
+ # # on MRI/Rubinius
32
+ # error.name #=> :DoesNotExist
33
+ #
34
+ # # on JRuby <= 1.7
35
+ # error.name #=> :'Name::DoesNotExist'
36
+ #
37
+ def name_from_message
38
+ /([A-Z]\w*$)/.match(original_message)[0]
39
+ end
40
+ else
41
+ def name_from_message
42
+ class_name || /([A-Z]\w*$)/.match(original_message)[0]
43
+ end
44
+ end
45
+
46
+ def suggestions
47
+ super.map(&:full_name)
48
+ end
49
+
50
+ def scopes
51
+ @scopes ||= scope_base.inject([Object]) do |_scopes, scope|
52
+ _scopes << _scopes.last.const_get(scope)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def scope_base
59
+ @scope_base ||= (/(([A-Z]\w*::)*)([A-Z]\w*)$/ =~ original_message ? $1 : "").split("::")
60
+ end
61
+
62
+ class ClassName < SimpleDelegator
63
+ attr :namespace
64
+
65
+ def initialize(name, namespace = '')
66
+ super(name)
67
+ @namespace = namespace
68
+ end
69
+
70
+ def full_name
71
+ self.class.new("#{namespace}#{__getobj__}")
72
+ end
73
+ end
74
+
75
+ private_constant :ClassName
76
+ end
77
+ end
@@ -0,0 +1,18 @@
1
+ module DidYouMean
2
+ class NameFinder
3
+ include BaseFinder
4
+ attr_reader :name, :method_names, :lvar_names, :ivar_names, :cvar_names
5
+
6
+ def initialize(exception)
7
+ @name = exception.name.to_s.tr(AT, EMPTY)
8
+ @lvar_names = exception.frame_binding.eval("local_variables")
9
+ @method_names = exception.frame_binding.eval("methods + private_methods")
10
+ @cvar_names = exception.frame_binding.eval("self.class.class_variables")
11
+ @ivar_names = exception.frame_binding.eval("instance_variables")
12
+ end
13
+
14
+ def searches
15
+ {name => (lvar_names + method_names + ivar_names + cvar_names)}
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module DidYouMean
2
+ class Formatter
3
+ def initialize(suggestions = [])
4
+ @suggestions = suggestions
5
+ end
6
+
7
+ def to_s
8
+ return "" if @suggestions.empty?
9
+
10
+ output = "\n\n"
11
+ output << " Did you mean? #{format(@suggestions.first)}\n"
12
+ output << @suggestions.drop(1).map{|word| "#{' ' * 18}#{format(word)}\n" }.join
13
+ output << " " # for rspec
14
+ end
15
+
16
+ def format(name)
17
+ name
18
+ end
19
+ end
20
+ end