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
data/Gemfile CHANGED
@@ -4,5 +4,7 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem 'benchmark-ips'
7
+ gem 'benchmark_driver'
7
8
  gem 'memory_profiler'
8
9
  gem 'jaro_winkler', '>= 1.4.0'
10
+ gem 'test-unit'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # did_you_mean [![Gem Version](https://badge.fury.io/rb/did_you_mean.svg)](https://rubygems.org/gems/did_you_mean) [![Build Status](https://travis-ci.org/yuki24/did_you_mean.svg?branch=master)](https://travis-ci.org/yuki24/did_you_mean)
1
+ # did_you_mean [![Gem Version](https://badge.fury.io/rb/did_you_mean.svg)](https://rubygems.org/gems/did_you_mean) [![Build Status](https://travis-ci.org/ruby/did_you_mean.svg?branch=master)](https://travis-ci.org/ruby/did_you_mean)
2
2
 
3
3
  ## Installation
4
4
 
@@ -61,34 +61,12 @@ hash.fetch(:fooo)
61
61
  # Did you mean? :foo
62
62
  ```
63
63
 
64
- ## Experimental Features
65
-
66
- Aside from the basic features above, the `did_you_mean` gem comes with experimental features. They can be enabled by calling `require 'did_you_mean/experimental'`.
67
-
68
- Note that **these experimental features should never be enabled in production as they would impact Ruby's performance and use some unstable Ruby APIs.**
69
-
70
- ### Correcting an Instance Variable When It's Incorrectly Spelled
71
-
72
- ```ruby
73
- require 'did_you_mean/experimental'
74
-
75
- @full_name = "Yuki Nishijima"
76
- @full_anme.split(" ")
77
- # => NoMethodError: undefined method `split' for nil:NilClass
78
- # Did you mean? @full_name
79
- ```
80
-
81
- ### Displaying a Warning When `initialize` is Incorrectly Spelled
64
+ ### LoadError
82
65
 
83
66
  ```ruby
84
- require 'did_you_mean/experimental'
85
-
86
- class Person
87
- def intialize
88
- ...
89
- end
90
- end
91
- # => warning: intialize might be misspelled, perhaps you meant initialize?
67
+ require 'net-http'
68
+ # => LoadError (cannot load such file -- net-http)
69
+ # Did you mean? net/http
92
70
  ```
93
71
 
94
72
  ## Verbose Formatter
@@ -108,9 +86,82 @@ OBject
108
86
  #
109
87
  ```
110
88
 
89
+ ## Using the `DidYouMean::SpellChecker`
90
+
91
+ If you need to programmatically find the closest matches to the user input, you could do so by re-using the `DidYouMean::SpellChecker` object.
92
+
93
+ ```ruby
94
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: ['email', 'fail', 'eval'])
95
+
96
+ spell_checker.correct('meail') # => ['email']
97
+ spell_checker.correct('afil') # => ['fail']
98
+ ```
99
+
100
+ ## Disabling `did_you_mean`
101
+
102
+ Occasionally, you may want to disable the `did_you_mean` gem for e.g. debugging issues in the error object itself. You
103
+ can disable it entirely by specifying `--disable-did_you_mean` option to the `ruby` command:
104
+
105
+ ```bash
106
+ $ ruby --disable-did_you_mean -e "1.zeor?"
107
+ -e:1:in `<main>': undefined method `zeor?' for 1:Integer (NameError)
108
+ ```
109
+
110
+ When you do not have direct access to the `ruby` command (e.g. `rails console`, `irb`), you could applyoptions using the
111
+ `RUBYOPT` environment variable:
112
+
113
+ ```bash
114
+ $ RUBYOPT='--disable-did_you_mean' irb
115
+ irb:0> 1.zeor?
116
+ # => NoMethodError (undefined method `zeor?' for 1:Integer)
117
+ ```
118
+
119
+ ### Getting the original error message
120
+
121
+ Sometimes, you do not want to disable the gem entirely, but need to get the original error message without suggestions
122
+ (e.g. testing). In this case, you could use the `#original_message` method on the error object:
123
+
124
+ ```ruby
125
+ no_method_error = begin
126
+ 1.zeor?
127
+ rescue NoMethodError => error
128
+ error
129
+ end
130
+
131
+ no_method_error.message
132
+ # => NoMethodError (undefined method `zeor?' for 1:Integer)
133
+ # Did you mean? zero?
134
+
135
+ no_method_error.original_message
136
+ # => NoMethodError (undefined method `zeor?' for 1:Integer)
137
+ ```
138
+
139
+ ## Benchmarking
140
+
141
+ Performance is very important as the `did_you_mean` gem attempts to find the closest matches on the fly right after an exception
142
+ is thrown. You could use the following rake tasks to get insights into how the gem performs:
143
+
144
+ ```bash
145
+ bundle exec rake benchmark:ips:jaro
146
+ bundle exec rake benchmark:ips:levenshtein
147
+ bundle exec rake benchmark:memory
148
+ bundle exec rake benchmark:memory:jaro
149
+ bundle exec rake benchmark:memory:levenshtein
150
+ ```
151
+
152
+ **Be sure to always use `bundle exec` otherwise it will activate the pre-installed version of the `did_you_mean`
153
+ gem rather than using what's in the `lib/`.**
154
+
155
+ You could also use the [`benchmark-driver`](https://github.com/benchmark-driver/benchmark-driver) gem to know how each
156
+ Ruby performs differently.
157
+
158
+ ```bash
159
+ bundle exec benchmark-driver benchmark/speed.yml --rbenv '2.6.0 --jit;2.6.0;2.5.3;truffleruby-1.0.0-rc10' --run-duration 30
160
+ ```
161
+
111
162
  ## Contributing
112
163
 
113
- 1. Fork it (http://github.com/yuki24/did_you_mean/fork)
164
+ 1. Fork it (https://github.com/ruby/did_you_mean/fork)
114
165
  2. Create your feature branch (`git checkout -b my-new-feature`)
115
166
  3. Commit your changes (`git commit -am 'Add some feature'`)
116
167
  4. Make sure all tests pass (`bundle exec rake`)
data/Rakefile CHANGED
@@ -4,30 +4,32 @@ require 'rake/testtask'
4
4
  Rake::TestTask.new do |task|
5
5
  task.libs << "test"
6
6
 
7
- task.test_files = Dir['test/**/*_test.rb'].reject {|path| /(experimental)/ =~ path }
7
+ task.test_files = Dir['test/**/test_*.rb'].reject {|path| path.end_with?("test_explore.rb") }
8
8
  task.verbose = true
9
9
  task.warning = true
10
+ task.ruby_opts = %w[ --disable-did_you_mean ]
10
11
  end
11
12
 
12
- Rake::TestTask.new("test:experimental") do |task|
13
+ Rake::TestTask.new("test:explore") do |task|
13
14
  task.libs << "test"
14
- task.pattern = 'test/experimental/**/*_test.rb'
15
- task.verbose = true
16
- task.warning = true
17
- task.ruby_opts << "-rdid_you_mean/experimental"
15
+
16
+ task.pattern = 'test/tree_spell/test_explore.rb'
17
+ task.verbose = true
18
+ task.warning = true
19
+ task.ruby_opts = %w[ --disable-did_you_mean ]
18
20
  end
19
21
 
20
- task default: %i(test test:experimental)
22
+ task default: %i(test)
21
23
 
22
24
  namespace :test do
23
25
  namespace :accuracy do
24
26
  desc "Download Wiktionary's Simple English data and save it as a dictionary"
25
27
  task :prepare do
26
- sh 'ruby evaluation/dictionary_generator.rb'
28
+ sh "RUBYOPT='--disable-did_you_mean' ruby evaluation/dictionary_generator.rb"
27
29
  end
28
30
  end
29
31
 
30
- desc "Calculate accuracy of the gems' spell checker"
32
+ desc "Calculate accuracy of the gem's spell checker"
31
33
  task :accuracy do
32
34
  if !File.exist?("evaluation/dictionary.yml")
33
35
  puts 'Generating dictionary for evaluation:'
@@ -35,13 +37,37 @@ namespace :test do
35
37
  puts "\n"
36
38
  end
37
39
 
38
- sh 'ruby evaluation/calculator.rb'
40
+ sh "RUBYOPT='--disable-did_you_mean' ruby evaluation/calculator.rb"
39
41
  end
40
42
  end
41
43
 
42
44
  namespace :benchmark do
43
- desc "Measure memory usage by the did_you_mean gem"
45
+ namespace :ips do
46
+ desc "Measure performance of the gem's Jaro distance implementation"
47
+ task :jaro do
48
+ sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/jaro_winkler/speed.rb"
49
+ end
50
+
51
+ desc "Benchmark performance of the gem's Levenshtein distance implementation"
52
+ task :levenshtein do
53
+ sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/levenshtein/speed.rb"
54
+ end
55
+ end
56
+
57
+ desc "Benchmark memory usage in the gem's spell checker"
44
58
  task :memory do
45
- sh 'ruby benchmark/memory_usage.rb'
59
+ sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/memory_usage.rb"
60
+ end
61
+
62
+ namespace :memory do
63
+ desc "Benchmark memory usage in the gem's Jaro distance implementation"
64
+ task :jaro do
65
+ sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/jaro_winkler/memory_usage.rb"
66
+ end
67
+
68
+ desc "Benchmark memory usage in the gem's Levenshtein distance implementation"
69
+ task :levenshtein do
70
+ sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/levenshtein/memory_usage.rb"
71
+ end
46
72
  end
47
73
  end
@@ -0,0 +1,25 @@
1
+ install:
2
+ - set PATH=C:\Ruby26-x64\bin;%PATH%
3
+
4
+ build: off
5
+
6
+ branches:
7
+ only:
8
+ - master
9
+
10
+ environment:
11
+ ruby_version: "24-%Platform%"
12
+ zlib_version: "1.2.11"
13
+ matrix:
14
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
15
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
16
+ - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
17
+
18
+ before_test:
19
+ - ruby -v
20
+ - gem -v
21
+ - bundle -v
22
+ - gem uni did_you_mean
23
+
24
+ test_script:
25
+ - rake
@@ -1,11 +1,11 @@
1
+ # frozen-string-literal: true
2
+
1
3
  require 'memory_profiler'
2
4
  require 'did_you_mean/jaro_winkler'
3
5
 
4
- str1, str2 = "user_signed_in?", "user_logged_in?"
5
-
6
6
  report = MemoryProfiler.report do
7
- 80.times do
8
- DidYouMean::Jaro.distance str1, str2
7
+ 1000.times do
8
+ DidYouMean::Jaro.distance "user_signed_in?", "user_logged_in?"
9
9
  end
10
10
  end
11
11
 
@@ -1,14 +1,22 @@
1
+ # frozen-string-literal: true
2
+
1
3
  require 'benchmark/ips'
2
- require 'did_you_mean'
4
+ require 'did_you_mean/jaro_winkler'
3
5
 
4
6
  Benchmark.ips do |x|
5
- x.report "before" do
6
- DidYouMean::Jaro.before_distance "user_signed_in?", "user_logged_in?"
7
+ x.report "original" do
8
+ DidYouMean::Jaro.distance "user_signed_in?", "user_logged_in?"
7
9
  end
8
10
 
9
- x.report "after" do
10
- DidYouMean::Jaro.after_distance "user_signed_in?", "user_logged_in?"
11
- end
11
+ # This #proposed method is not defined. Write your own method using this
12
+ # name so we can reliably run the benchmark and measure the difference.
13
+ #
14
+ # Alternatively, you could directly update the #distance method and remove
15
+ # this completely.
16
+ #
17
+ # x.report "proposed" do
18
+ # DidYouMean::Jaro.proposed "user_signed_in?", "user_logged_in?"
19
+ # end
12
20
 
13
21
  x.compare!
14
22
  end
@@ -1,11 +1,11 @@
1
+ # frozen-string-literal: true
2
+
1
3
  require 'memory_profiler'
2
4
  require 'did_you_mean/levenshtein'
3
5
 
4
- str1, str2 = "user_signed_in?", "user_logged_in?"
5
-
6
6
  report = MemoryProfiler.report do
7
- 80.times do
8
- DidYouMean::Levenshtein.distance str1, str2
7
+ 1000.times do
8
+ DidYouMean::Levenshtein.distance "user_signed_in?", "user_logged_in?"
9
9
  end
10
10
  end
11
11
 
@@ -1,17 +1,22 @@
1
+ # frozen-string-literal: true
2
+
1
3
  require 'benchmark/ips'
2
- require 'did_you_mean'
3
4
  require 'did_you_mean/levenshtein'
4
5
 
5
- STR1, STR2 = "user_signed_in?", "user_logged_in?"
6
-
7
6
  Benchmark.ips do |x|
8
- x.report "enumerable" do
9
- DidYouMean::Levenshtein.before_distance STR1, STR2
7
+ x.report "original" do
8
+ DidYouMean::Levenshtein.distance "user_signed_in?", "user_logged_in?"
10
9
  end
11
10
 
12
- x.report "while" do
13
- DidYouMean::Levenshtein.after_distance STR1, STR2
14
- end
11
+ # This #proposed method is not defined. Write your own method using this
12
+ # name so we can reliably run the benchmark and measure the difference.
13
+ #
14
+ # Alternatively, you could directly update the #distance method and remove
15
+ # this completely.
16
+ #
17
+ # x.report "proposed" do
18
+ # DidYouMean::Levenshtein.proposed "user_signed_in?", "user_logged_in?"
19
+ # end
15
20
 
16
21
  x.compare!
17
22
  end
@@ -0,0 +1,47 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # Run the following command to run this script:
4
+ #
5
+ # $ ruby --disable-did_you_mean benchmark/require_path_checker.rb
6
+ #
7
+
8
+ require_relative '../lib/did_you_mean/spell_checkers/require_path_checker'
9
+
10
+ require 'benchmark/ips'
11
+
12
+ Benchmark.ips do |x|
13
+ x.config(time: 10, warmup: 10)
14
+
15
+ exception_with_slash = begin
16
+ require 'net/htto'
17
+ rescue LoadError => error
18
+ error
19
+ end
20
+
21
+ exception_without_slash = begin
22
+ require 'net-http'
23
+ rescue LoadError => error
24
+ error
25
+ end
26
+
27
+ checker_for_path = DidYouMean::RequirePathChecker.new(exception_with_slash)
28
+ checker_for_file = DidYouMean::RequirePathChecker.new(exception_without_slash)
29
+
30
+ x.report "original (with a /)" do
31
+ checker_for_path.corrections
32
+ end
33
+
34
+ x.report "original (without /)" do
35
+ checker_for_file.corrections
36
+ end
37
+
38
+ #x.report "proposed (with a /)" do
39
+ # checker_for_path.experiment
40
+ #end
41
+ #
42
+ #x.report "proposed (without /)" do
43
+ # checker_for_file.experiment
44
+ #end
45
+
46
+ x.compare!
47
+ end
@@ -0,0 +1,13 @@
1
+ prelude: |
2
+ require 'did_you_mean/spell_checker'
3
+
4
+ str1, str2 = "user_signed_in?", "user_logged_in?"
5
+ METHODS = ''.methods
6
+ INPUT = 'starts_with?'
7
+ collection = DidYouMean::SpellChecker.new(dictionary: METHODS)
8
+
9
+ benchmark:
10
+ Jaro: DidYouMean::Jaro.distance(str1, str2)
11
+ Jaro Winkler: DidYouMean::JaroWinkler.distance(str1, str2)
12
+ Levenshtein: DidYouMean::Levenshtein.distance(str1, str2)
13
+ Spell checker: collection.correct(INPUT)
@@ -1,7 +1,11 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'did_you_mean/version'
4
+ begin
5
+ require_relative "lib/did_you_mean/version"
6
+ rescue LoadError # Fallback to load version file in ruby core repository
7
+ require_relative "version"
8
+ end
5
9
 
6
10
  Gem::Specification.new do |spec|
7
11
  spec.name = "did_you_mean"
@@ -10,7 +14,7 @@ Gem::Specification.new do |spec|
10
14
  spec.email = ["mail@yukinishijima.net"]
11
15
  spec.summary = '"Did you mean?" experience in Ruby'
12
16
  spec.description = 'The gem that has been saving people from typos since 2014.'
13
- spec.homepage = "https://github.com/yuki24/did_you_mean"
17
+ spec.homepage = "https://github.com/ruby/did_you_mean"
14
18
  spec.license = "MIT"
15
19
 
16
20
  spec.files = `git ls-files`.split($/).reject{|path| path.start_with?('evaluation/') }
@@ -19,7 +23,5 @@ Gem::Specification.new do |spec|
19
23
 
20
24
  spec.required_ruby_version = '>= 2.5.0'
21
25
 
22
- spec.add_development_dependency "bundler"
23
26
  spec.add_development_dependency "rake"
24
- spec.add_development_dependency "minitest"
25
27
  end
@@ -0,0 +1,8 @@
1
+ <% releases.each do |release| %>
2
+ ## [<%= release.name %>](https://github.com/<%= repository %>/tree/<%= release.tag_name %>)
3
+
4
+ _<sup>released at <%= release.published_at %></sup>_
5
+
6
+ <%= release.body.gsub(/\r\n/, "\n") %>
7
+
8
+ <% end %>
@@ -0,0 +1,34 @@
1
+ require 'octokit'
2
+ require 'reverse_markdown'
3
+ require 'erb'
4
+
5
+ class ChangeLogGenerator
6
+ attr :repository, :template_path, :changelog_path
7
+
8
+ def initialize(repository, template_path: "CHANGELOG.md.erb", changelog_path: "CHANGELOG.md")
9
+ @repository = repository
10
+ @template_path = template_path
11
+ @changelog_path = changelog_path
12
+ end
13
+
14
+ def generate_and_save!
15
+ changelog_in_md = ERB.new(template).result(binding)
16
+ changelog_in_html = Octokit.markdown(changelog_in_md, context: repository, mode: "gfm")
17
+
18
+ File.open(changelog_path, 'w') do |file|
19
+ file.write ReverseMarkdown.convert(changelog_in_html, github_flavored: true)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def template
26
+ open("#{__dir__}/#{template_path}").read
27
+ end
28
+
29
+ def releases
30
+ @releases ||= Octokit.releases(repository)
31
+ end
32
+ end
33
+
34
+ ChangeLogGenerator.new("ruby/did_you_mean").generate_and_save!