did_you_mean 1.2.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +39 -0
- data/CHANGELOG.md +172 -57
- data/Gemfile +2 -0
- data/README.md +79 -28
- data/Rakefile +38 -12
- data/appveyor.yml +25 -0
- data/benchmark/jaro_winkler/memory_usage.rb +4 -4
- data/benchmark/jaro_winkler/speed.rb +14 -6
- data/benchmark/levenshtein/memory_usage.rb +4 -4
- data/benchmark/levenshtein/speed.rb +13 -8
- data/benchmark/require_path_checker.rb +47 -0
- data/benchmark/speed.yml +13 -0
- data/did_you_mean.gemspec +6 -4
- data/documentation/CHANGELOG.md.erb +8 -0
- data/documentation/changelog_generator.rb +34 -0
- data/documentation/human_typo_api.md +20 -0
- data/documentation/tree_spell_algorithm.md +82 -0
- data/documentation/tree_spell_checker_api.md +24 -0
- data/lib/did_you_mean.rb +95 -52
- data/lib/did_you_mean/core_ext/name_error.rb +4 -6
- data/lib/did_you_mean/experimental.rb +2 -2
- data/lib/did_you_mean/formatters/plain_formatter.rb +24 -0
- data/lib/did_you_mean/formatters/verbose_formatter.rb +36 -0
- data/lib/did_you_mean/levenshtein.rb +1 -1
- data/lib/did_you_mean/spell_checker.rb +8 -14
- data/lib/did_you_mean/spell_checkers/key_error_checker.rb +8 -2
- data/lib/did_you_mean/spell_checkers/method_name_checker.rb +48 -6
- data/lib/did_you_mean/spell_checkers/name_error_checkers.rb +2 -2
- data/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb +5 -5
- data/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb +56 -4
- data/lib/did_you_mean/spell_checkers/require_path_checker.rb +35 -0
- data/lib/did_you_mean/tree_spell_checker.rb +109 -0
- data/lib/did_you_mean/verbose.rb +2 -2
- data/lib/did_you_mean/version.rb +1 -1
- data/test/core_ext/test_name_error_extension.rb +48 -0
- data/test/edit_distance/{jaro_winkler_test.rb → test_jaro_winkler.rb} +2 -2
- data/test/fixtures/book.rb +1 -1
- data/test/fixtures/mini_dir.yml +15 -0
- data/test/fixtures/rspec_dir.yml +112 -0
- data/test/helper.rb +29 -0
- data/test/spell_checking/{class_name_check_test.rb → test_class_name_check.rb} +15 -11
- data/test/spell_checking/{key_name_check_test.rb → test_key_name_check.rb} +18 -8
- data/test/spell_checking/{method_name_check_test.rb → test_method_name_check.rb} +41 -13
- data/test/spell_checking/test_require_path_check.rb +32 -0
- data/test/spell_checking/{uncorrectable_name_check_test.rb → test_uncorrectable_name_check.rb} +3 -3
- data/test/spell_checking/{variable_name_check_test.rb → test_variable_name_check.rb} +36 -18
- data/test/{spell_checker_test.rb → test_spell_checker.rb} +2 -2
- data/test/test_tree_spell_checker.rb +170 -0
- data/test/test_verbose_formatter.rb +23 -0
- data/test/tree_spell/change_word.rb +61 -0
- data/test/tree_spell/human_typo.rb +69 -0
- data/test/tree_spell/test_change_word.rb +38 -0
- data/test/tree_spell/test_explore.rb +128 -0
- data/test/tree_spell/test_human_typo.rb +24 -0
- metadata +53 -66
- data/.ruby-version +0 -1
- data/.travis.yml +0 -14
- data/lib/did_you_mean/experimental/initializer_name_correction.rb +0 -20
- data/lib/did_you_mean/experimental/ivar_name_correction.rb +0 -74
- data/lib/did_you_mean/verbose_formatter.rb +0 -7
- data/test/core_ext/name_error_extension_test.rb +0 -47
- data/test/deprecated_formatter_test.rb +0 -9
- data/test/experimental/initializer_name_correction_test.rb +0 -15
- data/test/experimental/method_name_checker_test.rb +0 -13
- data/test/test_helper.rb +0 -13
- data/test/verbose_formatter_test.rb +0 -23
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative "../spell_checker"
|
2
2
|
|
3
3
|
module DidYouMean
|
4
4
|
class KeyErrorChecker
|
@@ -8,7 +8,13 @@ module DidYouMean
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def corrections
|
11
|
-
@corrections ||= SpellChecker.new(dictionary: @keys).correct(@key).map(&:inspect)
|
11
|
+
@corrections ||= exact_matches.empty? ? SpellChecker.new(dictionary: @keys).correct(@key).map(&:inspect) : exact_matches
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def exact_matches
|
17
|
+
@exact_matches ||= @keys.select { |word| @key == word.to_s }.map(&:inspect)
|
12
18
|
end
|
13
19
|
end
|
14
20
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative "../spell_checker"
|
2
2
|
|
3
3
|
module DidYouMean
|
4
4
|
class MethodNameChecker
|
@@ -7,6 +7,35 @@ module DidYouMean
|
|
7
7
|
NAMES_TO_EXCLUDE = { NilClass => nil.methods }
|
8
8
|
NAMES_TO_EXCLUDE.default = []
|
9
9
|
|
10
|
+
# +MethodNameChecker::RB_RESERVED_WORDS+ is the list of reserved words in
|
11
|
+
# Ruby that take an argument. Unlike
|
12
|
+
# +VariableNameChecker::RB_RESERVED_WORDS+, these reserved words require
|
13
|
+
# an argument, and a +NoMethodError+ is raised due to the presence of the
|
14
|
+
# argument.
|
15
|
+
#
|
16
|
+
# The +MethodNameChecker+ will use this list to suggest a reversed word if
|
17
|
+
# a +NoMethodError+ is raised and found closest matches.
|
18
|
+
#
|
19
|
+
# Also see +VariableNameChecker::RB_RESERVED_WORDS+.
|
20
|
+
RB_RESERVED_WORDS = %i(
|
21
|
+
alias
|
22
|
+
case
|
23
|
+
def
|
24
|
+
defined?
|
25
|
+
elsif
|
26
|
+
end
|
27
|
+
ensure
|
28
|
+
for
|
29
|
+
rescue
|
30
|
+
super
|
31
|
+
undef
|
32
|
+
unless
|
33
|
+
until
|
34
|
+
when
|
35
|
+
while
|
36
|
+
yield
|
37
|
+
)
|
38
|
+
|
10
39
|
def initialize(exception)
|
11
40
|
@method_name = exception.name
|
12
41
|
@receiver = exception.receiver
|
@@ -14,14 +43,27 @@ module DidYouMean
|
|
14
43
|
end
|
15
44
|
|
16
45
|
def corrections
|
17
|
-
@corrections ||=
|
46
|
+
@corrections ||= begin
|
47
|
+
dictionary = method_names
|
48
|
+
dictionary = RB_RESERVED_WORDS + dictionary if @private_call
|
49
|
+
|
50
|
+
SpellChecker.new(dictionary: dictionary).correct(method_name) - names_to_exclude
|
51
|
+
end
|
18
52
|
end
|
19
53
|
|
20
54
|
def method_names
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
55
|
+
if Object === receiver
|
56
|
+
method_names = receiver.methods + receiver.singleton_methods
|
57
|
+
method_names += receiver.private_methods if @private_call
|
58
|
+
method_names.uniq!
|
59
|
+
method_names
|
60
|
+
else
|
61
|
+
[]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def names_to_exclude
|
66
|
+
Object === receiver ? NAMES_TO_EXCLUDE[receiver.class] : []
|
25
67
|
end
|
26
68
|
end
|
27
69
|
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require_relative 'name_error_checkers/class_name_checker'
|
2
|
+
require_relative 'name_error_checkers/variable_name_checker'
|
3
3
|
|
4
4
|
module DidYouMean
|
5
5
|
class << (NameErrorCheckers = Object.new)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
require_relative "../../spell_checker"
|
4
4
|
|
5
5
|
module DidYouMean
|
6
6
|
class ClassNameChecker
|
@@ -31,16 +31,16 @@ module DidYouMean
|
|
31
31
|
end.uniq
|
32
32
|
end
|
33
33
|
|
34
|
-
class ClassName <
|
34
|
+
class ClassName < String
|
35
35
|
attr :namespace
|
36
36
|
|
37
37
|
def initialize(name, namespace = '')
|
38
|
-
super(name)
|
38
|
+
super(name.to_s)
|
39
39
|
@namespace = namespace
|
40
40
|
end
|
41
41
|
|
42
42
|
def full_name
|
43
|
-
self.class.new("#{namespace}#{
|
43
|
+
self.class.new("#{namespace}#{self}")
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -1,14 +1,66 @@
|
|
1
1
|
# frozen-string-literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "../../spell_checker"
|
4
4
|
|
5
5
|
module DidYouMean
|
6
6
|
class VariableNameChecker
|
7
7
|
attr_reader :name, :method_names, :lvar_names, :ivar_names, :cvar_names
|
8
8
|
|
9
|
-
NAMES_TO_EXCLUDE = { 'foo' => [:fork] }
|
9
|
+
NAMES_TO_EXCLUDE = { 'foo' => [:fork, :for] }
|
10
10
|
NAMES_TO_EXCLUDE.default = []
|
11
|
-
|
11
|
+
|
12
|
+
# +VariableNameChecker::RB_RESERVED_WORDS+ is the list of all reserved
|
13
|
+
# words in Ruby. They could be declared like methods are, and a typo would
|
14
|
+
# cause Ruby to raise a +NameError+ because of the way they are declared.
|
15
|
+
#
|
16
|
+
# The +:VariableNameChecker+ will use this list to suggest a reversed word
|
17
|
+
# if a +NameError+ is raised and found closest matches, excluding:
|
18
|
+
#
|
19
|
+
# * +do+
|
20
|
+
# * +if+
|
21
|
+
# * +in+
|
22
|
+
# * +or+
|
23
|
+
#
|
24
|
+
# Also see +MethodNameChecker::RB_RESERVED_WORDS+.
|
25
|
+
RB_RESERVED_WORDS = %i(
|
26
|
+
BEGIN
|
27
|
+
END
|
28
|
+
alias
|
29
|
+
and
|
30
|
+
begin
|
31
|
+
break
|
32
|
+
case
|
33
|
+
class
|
34
|
+
def
|
35
|
+
defined?
|
36
|
+
else
|
37
|
+
elsif
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
false
|
41
|
+
for
|
42
|
+
module
|
43
|
+
next
|
44
|
+
nil
|
45
|
+
not
|
46
|
+
redo
|
47
|
+
rescue
|
48
|
+
retry
|
49
|
+
return
|
50
|
+
self
|
51
|
+
super
|
52
|
+
then
|
53
|
+
true
|
54
|
+
undef
|
55
|
+
unless
|
56
|
+
until
|
57
|
+
when
|
58
|
+
while
|
59
|
+
yield
|
60
|
+
__LINE__
|
61
|
+
__FILE__
|
62
|
+
__ENCODING__
|
63
|
+
)
|
12
64
|
|
13
65
|
def initialize(exception)
|
14
66
|
@name = exception.name.to_s.tr("@", "")
|
@@ -23,7 +75,7 @@ module DidYouMean
|
|
23
75
|
|
24
76
|
def corrections
|
25
77
|
@corrections ||= SpellChecker
|
26
|
-
.new(dictionary: (
|
78
|
+
.new(dictionary: (RB_RESERVED_WORDS + lvar_names + method_names + ivar_names + cvar_names))
|
27
79
|
.correct(name) - NAMES_TO_EXCLUDE[@name]
|
28
80
|
end
|
29
81
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require_relative "../spell_checker"
|
4
|
+
require_relative "../tree_spell_checker"
|
5
|
+
|
6
|
+
module DidYouMean
|
7
|
+
class RequirePathChecker
|
8
|
+
attr_reader :path
|
9
|
+
|
10
|
+
INITIAL_LOAD_PATH = $LOAD_PATH.dup.freeze
|
11
|
+
ENV_SPECIFIC_EXT = ".#{RbConfig::CONFIG["DLEXT"]}"
|
12
|
+
|
13
|
+
private_constant :INITIAL_LOAD_PATH, :ENV_SPECIFIC_EXT
|
14
|
+
|
15
|
+
def self.requireables
|
16
|
+
@requireables ||= INITIAL_LOAD_PATH
|
17
|
+
.flat_map {|path| Dir.glob("**/???*{.rb,#{ENV_SPECIFIC_EXT}}", base: path) }
|
18
|
+
.map {|path| path.chomp!(".rb") || path.chomp!(ENV_SPECIFIC_EXT) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(exception)
|
22
|
+
@path = exception.path
|
23
|
+
end
|
24
|
+
|
25
|
+
def corrections
|
26
|
+
@corrections ||= begin
|
27
|
+
threshold = path.size * 2
|
28
|
+
dictionary = self.class.requireables.reject {|str| str.size >= threshold }
|
29
|
+
spell_checker = path.include?("/") ? TreeSpellChecker : SpellChecker
|
30
|
+
|
31
|
+
spell_checker.new(dictionary: dictionary).correct(path).uniq
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DidYouMean
|
4
|
+
# spell checker for a dictionary that has a tree
|
5
|
+
# structure, see doc/tree_spell_checker_api.md
|
6
|
+
class TreeSpellChecker
|
7
|
+
attr_reader :dictionary, :separator, :augment
|
8
|
+
|
9
|
+
def initialize(dictionary:, separator: '/', augment: nil)
|
10
|
+
@dictionary = dictionary
|
11
|
+
@separator = separator
|
12
|
+
@augment = augment
|
13
|
+
end
|
14
|
+
|
15
|
+
def correct(input)
|
16
|
+
plausibles = plausible_dimensions(input)
|
17
|
+
return fall_back_to_normal_spell_check(input) if plausibles.empty?
|
18
|
+
|
19
|
+
suggestions = find_suggestions(input, plausibles)
|
20
|
+
return fall_back_to_normal_spell_check(input) if suggestions.empty?
|
21
|
+
|
22
|
+
suggestions
|
23
|
+
end
|
24
|
+
|
25
|
+
def dictionary_without_leaves
|
26
|
+
@dictionary_without_leaves ||= dictionary.map { |word| word.split(separator)[0..-2] }.uniq
|
27
|
+
end
|
28
|
+
|
29
|
+
def tree_depth
|
30
|
+
@tree_depth ||= dictionary_without_leaves.max { |a, b| a.size <=> b.size }.size
|
31
|
+
end
|
32
|
+
|
33
|
+
def dimensions
|
34
|
+
@dimensions ||= tree_depth.times.map do |index|
|
35
|
+
dictionary_without_leaves.map { |element| element[index] }.compact.uniq
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_leaves(path)
|
40
|
+
path_with_separator = "#{path}#{separator}"
|
41
|
+
|
42
|
+
dictionary
|
43
|
+
.select {|str| str.include?(path_with_separator) }
|
44
|
+
.map {|str| str.gsub(path_with_separator, '') }
|
45
|
+
end
|
46
|
+
|
47
|
+
def plausible_dimensions(input)
|
48
|
+
input.split(separator)[0..-2]
|
49
|
+
.map
|
50
|
+
.with_index { |element, index| correct_element(dimensions[index], element) if dimensions[index] }
|
51
|
+
.compact
|
52
|
+
end
|
53
|
+
|
54
|
+
def possible_paths(states)
|
55
|
+
states.map { |state| state.join(separator) }
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def find_suggestions(input, plausibles)
|
61
|
+
states = plausibles[0].product(*plausibles[1..-1])
|
62
|
+
paths = possible_paths(states)
|
63
|
+
leaf = input.split(separator).last
|
64
|
+
|
65
|
+
find_ideas(paths, leaf)
|
66
|
+
end
|
67
|
+
|
68
|
+
def fall_back_to_normal_spell_check(input)
|
69
|
+
return [] unless augment
|
70
|
+
|
71
|
+
::DidYouMean::SpellChecker.new(dictionary: dictionary).correct(input)
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_ideas(paths, leaf)
|
75
|
+
paths.flat_map do |path|
|
76
|
+
names = find_leaves(path)
|
77
|
+
ideas = correct_element(names, leaf)
|
78
|
+
|
79
|
+
ideas_to_paths(ideas, leaf, names, path)
|
80
|
+
end.compact
|
81
|
+
end
|
82
|
+
|
83
|
+
def ideas_to_paths(ideas, leaf, names, path)
|
84
|
+
if ideas.empty?
|
85
|
+
nil
|
86
|
+
elsif names.include?(leaf)
|
87
|
+
["#{path}#{separator}#{leaf}"]
|
88
|
+
else
|
89
|
+
ideas.map {|str| "#{path}#{separator}#{str}" }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def correct_element(names, element)
|
94
|
+
return names if names.size == 1
|
95
|
+
|
96
|
+
str = normalize(element)
|
97
|
+
|
98
|
+
return [str] if names.include?(str)
|
99
|
+
|
100
|
+
::DidYouMean::SpellChecker.new(dictionary: names).correct(str)
|
101
|
+
end
|
102
|
+
|
103
|
+
def normalize(str)
|
104
|
+
str.downcase!
|
105
|
+
str.tr!('@', ' ') if str.include?('@')
|
106
|
+
str
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/did_you_mean/verbose.rb
CHANGED
data/lib/did_you_mean/version.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
|
3
|
+
class NameErrorExtensionTest < Test::Unit::TestCase
|
4
|
+
SPELL_CHECKERS = DidYouMean::SPELL_CHECKERS
|
5
|
+
|
6
|
+
class TestSpellChecker
|
7
|
+
def initialize(*); end
|
8
|
+
def corrections; ["does_exist"]; end
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@org, SPELL_CHECKERS['NameError'] = SPELL_CHECKERS['NameError'], TestSpellChecker
|
13
|
+
|
14
|
+
@error = assert_raise(NameError){ doesnt_exist }
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
SPELL_CHECKERS['NameError'] = @org
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_message
|
22
|
+
assert_match(/Did you mean\? does_exist/, @error.to_s)
|
23
|
+
assert_match(/Did you mean\? does_exist/, @error.message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_to_s_does_not_make_disruptive_changes_to_error_message
|
27
|
+
error = assert_raise(NameError) do
|
28
|
+
raise NameError, "uninitialized constant Object"
|
29
|
+
end
|
30
|
+
|
31
|
+
error.to_s
|
32
|
+
assert_equal 1, error.to_s.scan("Did you mean?").count
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_correctable_error_objects_are_dumpable
|
36
|
+
error =
|
37
|
+
begin
|
38
|
+
Dir.chdir(__dir__) { File.open('test_name_error_extension.rb') { |f| f.sizee } }
|
39
|
+
rescue NoMethodError => e
|
40
|
+
e
|
41
|
+
end
|
42
|
+
|
43
|
+
error.to_s
|
44
|
+
|
45
|
+
assert_equal "undefined method `sizee' for #<File:test_name_error_extension.rb (closed)>",
|
46
|
+
Marshal.load(Marshal.dump(error)).original_message
|
47
|
+
end
|
48
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative '../helper'
|
2
2
|
|
3
3
|
# These tests were originally written by Jian Weihang (簡煒航) as part of his work
|
4
4
|
# on the jaro_winkler gem. The original code could be found here:
|
@@ -6,7 +6,7 @@ require 'test_helper'
|
|
6
6
|
#
|
7
7
|
# Copyright (c) 2014 Jian Weihang
|
8
8
|
|
9
|
-
class JaroWinklerTest <
|
9
|
+
class JaroWinklerTest < Test::Unit::TestCase
|
10
10
|
def test_jaro_winkler_distance
|
11
11
|
assert_distance 0.9667, 'henka', 'henkan'
|
12
12
|
assert_distance 1.0, 'al', 'al'
|
data/test/fixtures/book.rb
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
- test/core_ext/name_error_extension_test.rb
|
3
|
+
- test/edit_distance/jaro_winkler_test.rb
|
4
|
+
- test/fixtures/book.rb
|
5
|
+
- test/spell_checker_test.rb
|
6
|
+
- test/spell_checking/class_name_check_test.rb
|
7
|
+
- test/spell_checking/key_name_check_test.rb
|
8
|
+
- test/spell_checking/method_name_check_test.rb
|
9
|
+
- test/spell_checking/uncorrectable_name_check_test.rb
|
10
|
+
- test/spell_checking/variable_name_check_test.rb
|
11
|
+
- test/test_helper.rb
|
12
|
+
- test/tree_spell_checker_test.rb
|
13
|
+
- test/tree_spell_explore_test.rb
|
14
|
+
- test/tree_spell_human_typo_test.rb
|
15
|
+
- test/verbose_formatter_test.rb
|