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.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +13 -19
- data/CHANGELOG.md +229 -0
- data/Gemfile +4 -7
- data/README.md +5 -16
- data/Rakefile +35 -33
- data/benchmark/jaro_winkler/memory_usage.rb +12 -0
- data/benchmark/jaro_winkler/speed.rb +14 -0
- data/benchmark/levenshtein/memory_usage.rb +12 -0
- data/benchmark/levenshtein/speed.rb +17 -0
- data/benchmark/memory_usage.rb +31 -0
- data/did_you_mean.gemspec +9 -2
- data/evaluation/calculator.rb +122 -0
- data/evaluation/dictionary_generator.rb +36 -0
- data/evaluation/incorrect_words.yaml +1159 -0
- data/ext/did_you_mean/extconf.rb +1 -1
- data/ext/did_you_mean/{method_missing.c → method_receiver.c} +1 -1
- data/lib/did_you_mean.rb +22 -3
- data/lib/did_you_mean/core_ext/name_error.rb +30 -22
- data/lib/did_you_mean/core_ext/no_method_error.rb +13 -2
- data/lib/did_you_mean/core_ext/rubinius.rb +16 -0
- data/lib/did_you_mean/finders.rb +48 -17
- data/lib/did_you_mean/finders/method_finder.rb +62 -0
- data/lib/did_you_mean/finders/name_error_finders.rb +8 -11
- data/lib/did_you_mean/finders/name_error_finders/class_finder.rb +77 -0
- data/lib/did_you_mean/finders/name_error_finders/name_finder.rb +18 -0
- data/lib/did_you_mean/formatter.rb +20 -0
- data/lib/did_you_mean/jaro_winkler.rb +87 -0
- data/lib/did_you_mean/test_helper.rb +1 -1
- data/lib/did_you_mean/version.rb +1 -1
- data/test/core_ext/name_error_extension_test.rb +74 -0
- data/test/{no_method_error_extension_test.rb → core_ext/no_method_error_extension_test.rb} +1 -1
- data/test/correctable/class_name_test.rb +65 -0
- data/test/correctable/method_name_test.rb +84 -0
- data/test/{null_finder_test.rb → correctable/uncorrectable_name_test.rb} +3 -7
- data/test/correctable/variable_name_test.rb +79 -0
- data/test/edit_distance/jaro_winkler_test.rb +30 -0
- data/test/test_helper.rb +2 -20
- data/test/word_collection_test.rb +76 -12
- metadata +58 -56
- data/Appraisals +0 -23
- data/gemfiles/activerecord_32.gemfile +0 -21
- data/gemfiles/activerecord_40.gemfile +0 -21
- data/gemfiles/activerecord_41.gemfile +0 -21
- data/gemfiles/activerecord_42.gemfile +0 -21
- data/gemfiles/activerecord_edge.gemfile +0 -25
- data/lib/did_you_mean/finders/name_error_finders/similar_class_finder.rb +0 -50
- data/lib/did_you_mean/finders/name_error_finders/similar_name_finder.rb +0 -61
- data/lib/did_you_mean/finders/similar_attribute_finder.rb +0 -26
- data/lib/did_you_mean/finders/similar_method_finder.rb +0 -34
- data/lib/did_you_mean/word_collection.rb +0 -30
- data/test/all_test.rb +0 -18
- data/test/name_error_extension_test.rb +0 -73
- data/test/similar_attribute_finder_test.rb +0 -17
- data/test/similar_class_finder_test.rb +0 -85
- data/test/similar_method_finder_test.rb +0 -60
- data/test/similar_name_finder_test.rb +0 -62
data/ext/did_you_mean/extconf.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
require 'mkmf'
|
2
|
-
create_makefile 'did_you_mean/
|
2
|
+
create_makefile 'did_you_mean/method_receiver'
|
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
|
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
|
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
|
-
|
2
|
-
|
1
|
+
module DidYouMean
|
2
|
+
module Correctable
|
3
|
+
attr_reader :frame_binding
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
IGNORED_CALLERS = [
|
6
|
+
/( |`)missing_name'/,
|
7
|
+
/( |`)safe_constantize'/
|
8
|
+
].freeze
|
9
|
+
private_constant :IGNORED_CALLERS
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
original_message
|
18
|
-
end
|
16
|
+
def to_s
|
17
|
+
msg = original_message.dup
|
18
|
+
bt = caller.first(6)
|
19
19
|
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
28
|
+
def suggestions
|
29
|
+
finder.suggestions
|
30
|
+
end
|
26
31
|
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/did_you_mean/finders.rb
CHANGED
@@ -1,31 +1,62 @@
|
|
1
|
-
require "did_you_mean/
|
1
|
+
require "did_you_mean/levenshtein"
|
2
|
+
require "did_you_mean/jaro_winkler"
|
2
3
|
|
3
4
|
module DidYouMean
|
4
5
|
module BaseFinder
|
5
|
-
|
6
|
-
|
6
|
+
AT = "@".freeze
|
7
|
+
EMPTY = "".freeze
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
15
|
-
|
36
|
+
def searches
|
37
|
+
raise NotImplementedError
|
16
38
|
end
|
17
|
-
end
|
18
39
|
|
19
|
-
|
20
|
-
|
21
|
-
def
|
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
|
-
|
25
|
-
|
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/
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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/
|
24
|
-
require 'did_you_mean/finders/name_error_finders/
|
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
|