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.
- 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
|