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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +39 -0
  3. data/CHANGELOG.md +172 -57
  4. data/Gemfile +2 -0
  5. data/README.md +79 -28
  6. data/Rakefile +38 -12
  7. data/appveyor.yml +25 -0
  8. data/benchmark/jaro_winkler/memory_usage.rb +4 -4
  9. data/benchmark/jaro_winkler/speed.rb +14 -6
  10. data/benchmark/levenshtein/memory_usage.rb +4 -4
  11. data/benchmark/levenshtein/speed.rb +13 -8
  12. data/benchmark/require_path_checker.rb +47 -0
  13. data/benchmark/speed.yml +13 -0
  14. data/did_you_mean.gemspec +6 -4
  15. data/documentation/CHANGELOG.md.erb +8 -0
  16. data/documentation/changelog_generator.rb +34 -0
  17. data/documentation/human_typo_api.md +20 -0
  18. data/documentation/tree_spell_algorithm.md +82 -0
  19. data/documentation/tree_spell_checker_api.md +24 -0
  20. data/lib/did_you_mean.rb +95 -52
  21. data/lib/did_you_mean/core_ext/name_error.rb +4 -6
  22. data/lib/did_you_mean/experimental.rb +2 -2
  23. data/lib/did_you_mean/formatters/plain_formatter.rb +24 -0
  24. data/lib/did_you_mean/formatters/verbose_formatter.rb +36 -0
  25. data/lib/did_you_mean/levenshtein.rb +1 -1
  26. data/lib/did_you_mean/spell_checker.rb +8 -14
  27. data/lib/did_you_mean/spell_checkers/key_error_checker.rb +8 -2
  28. data/lib/did_you_mean/spell_checkers/method_name_checker.rb +48 -6
  29. data/lib/did_you_mean/spell_checkers/name_error_checkers.rb +2 -2
  30. data/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb +5 -5
  31. data/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb +56 -4
  32. data/lib/did_you_mean/spell_checkers/require_path_checker.rb +35 -0
  33. data/lib/did_you_mean/tree_spell_checker.rb +109 -0
  34. data/lib/did_you_mean/verbose.rb +2 -2
  35. data/lib/did_you_mean/version.rb +1 -1
  36. data/test/core_ext/test_name_error_extension.rb +48 -0
  37. data/test/edit_distance/{jaro_winkler_test.rb → test_jaro_winkler.rb} +2 -2
  38. data/test/fixtures/book.rb +1 -1
  39. data/test/fixtures/mini_dir.yml +15 -0
  40. data/test/fixtures/rspec_dir.yml +112 -0
  41. data/test/helper.rb +29 -0
  42. data/test/spell_checking/{class_name_check_test.rb → test_class_name_check.rb} +15 -11
  43. data/test/spell_checking/{key_name_check_test.rb → test_key_name_check.rb} +18 -8
  44. data/test/spell_checking/{method_name_check_test.rb → test_method_name_check.rb} +41 -13
  45. data/test/spell_checking/test_require_path_check.rb +32 -0
  46. data/test/spell_checking/{uncorrectable_name_check_test.rb → test_uncorrectable_name_check.rb} +3 -3
  47. data/test/spell_checking/{variable_name_check_test.rb → test_variable_name_check.rb} +36 -18
  48. data/test/{spell_checker_test.rb → test_spell_checker.rb} +2 -2
  49. data/test/test_tree_spell_checker.rb +170 -0
  50. data/test/test_verbose_formatter.rb +23 -0
  51. data/test/tree_spell/change_word.rb +61 -0
  52. data/test/tree_spell/human_typo.rb +69 -0
  53. data/test/tree_spell/test_change_word.rb +38 -0
  54. data/test/tree_spell/test_explore.rb +128 -0
  55. data/test/tree_spell/test_human_typo.rb +24 -0
  56. metadata +53 -66
  57. data/.ruby-version +0 -1
  58. data/.travis.yml +0 -14
  59. data/lib/did_you_mean/experimental/initializer_name_correction.rb +0 -20
  60. data/lib/did_you_mean/experimental/ivar_name_correction.rb +0 -74
  61. data/lib/did_you_mean/verbose_formatter.rb +0 -7
  62. data/test/core_ext/name_error_extension_test.rb +0 -47
  63. data/test/deprecated_formatter_test.rb +0 -9
  64. data/test/experimental/initializer_name_correction_test.rb +0 -15
  65. data/test/experimental/method_name_checker_test.rb +0 -13
  66. data/test/test_helper.rb +0 -13
  67. data/test/verbose_formatter_test.rb +0 -23
@@ -1,4 +1,4 @@
1
- require "did_you_mean/spell_checker"
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
- require "did_you_mean/spell_checker"
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 ||= SpellChecker.new(dictionary: method_names).correct(method_name) - NAMES_TO_EXCLUDE[@receiver.class]
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
- method_names = receiver.methods + receiver.singleton_methods
22
- method_names += receiver.private_methods if @private_call
23
- method_names.uniq!
24
- method_names
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
- require 'did_you_mean/spell_checkers/name_error_checkers/class_name_checker'
2
- require 'did_you_mean/spell_checkers/name_error_checkers/variable_name_checker'
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
- require 'delegate'
3
- require "did_you_mean/spell_checker"
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 < SimpleDelegator
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}#{__getobj__}")
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
- require "did_you_mean/spell_checker"
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
- RB_PREDEFINED_OBJECTS = [:false, :true, :nil]
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: (RB_PREDEFINED_OBJECTS + lvar_names + method_names + ivar_names + cvar_names))
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
@@ -1,4 +1,4 @@
1
- require 'did_you_mean'
2
- require 'did_you_mean/formatters/verbose_formatter'
1
+ require_relative '../did_you_mean'
2
+ require_relative 'formatters/verbose_formatter'
3
3
 
4
4
  DidYouMean.formatter = DidYouMean::VerboseFormatter.new
@@ -1,3 +1,3 @@
1
1
  module DidYouMean
2
- VERSION = "1.2.1"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -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
- require 'test_helper'
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 < Minitest::Test
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'
@@ -1,4 +1,4 @@
1
1
  class Book
2
- class Cover
2
+ class Spine
3
3
  end
4
4
  end
@@ -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