did_you_mean 0.9.10-java → 0.10.0-java

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