did_you_mean 1.5.0 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +6 -0
  3. data/.github/workflows/ruby.yml +3 -3
  4. data/CHANGELOG.md +37 -2
  5. data/README.md +7 -15
  6. data/Rakefile +9 -2
  7. data/documentation/tree_spell_checker_api.md +1 -1
  8. data/lib/did_you_mean/core_ext/name_error.rb +43 -11
  9. data/lib/did_you_mean/formatter.rb +44 -0
  10. data/lib/did_you_mean/formatters/plain_formatter.rb +3 -32
  11. data/lib/did_you_mean/formatters/verbose_formatter.rb +5 -44
  12. data/lib/did_you_mean/spell_checker.rb +9 -9
  13. data/lib/did_you_mean/spell_checkers/method_name_checker.rb +3 -0
  14. data/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb +4 -1
  15. data/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb +20 -0
  16. data/lib/did_you_mean/spell_checkers/require_path_checker.rb +5 -1
  17. data/lib/did_you_mean/verbose.rb +2 -4
  18. data/lib/did_you_mean/version.rb +1 -1
  19. data/lib/did_you_mean.rb +51 -8
  20. data/test/core_ext/test_name_error_extension.rb +15 -8
  21. data/test/helper.rb +14 -0
  22. data/test/lib/core_assertions.rb +757 -0
  23. data/test/lib/envutil.rb +372 -0
  24. data/test/lib/find_executable.rb +22 -0
  25. data/test/lib/helper.rb +3 -0
  26. data/test/spell_checking/test_key_name_check.rb +7 -7
  27. data/test/spell_checking/test_method_name_check.rb +10 -10
  28. data/test/spell_checking/test_pattern_key_name_check.rb +20 -0
  29. data/test/spell_checking/test_require_path_check.rb +3 -3
  30. data/test/spell_checking/test_uncorrectable_name_check.rb +1 -1
  31. data/test/spell_checking/test_variable_name_check.rb +24 -12
  32. data/test/test_ractor_compatibility.rb +117 -0
  33. data/test/test_spell_checker.rb +1 -0
  34. data/test/tree_spell/test_change_word.rb +1 -1
  35. metadata +21 -8
  36. data/test/test_verbose_formatter.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3befc8f677ec57e2975d04b2095fcecf7314ff207150aa994b197894dba883af
4
- data.tar.gz: dc5a44ad2f6058ef68913f4080a23141f39b23f3b30129fe753d5d88cca85cc0
3
+ metadata.gz: c8872d94ffdf1a826aa542768069af9a51087bc5156e54c277ccad671088724f
4
+ data.tar.gz: cd6c5001ef84b6618b488995b7c707dd88e9586df229067b25c6e4fb77c43882
5
5
  SHA512:
6
- metadata.gz: 4d60d78614d4a6650b2be87599d3c3d867ab74f4cdf7f6083594229e3c51588af57a5c9574ca35ced229558f4280cd3677cbd7944cc5cef55e34a4f7ff7addf3
7
- data.tar.gz: df5f07a5893afee4e03af416230ee828c7c2c2dfb8e5e35b5c525bebdff03710645b4af192563dc546abd4c210f8f2e8ce15e90124503488b74db19bd700f71c
6
+ metadata.gz: 5c18bc93c847891f6061325c0a2f1f6b067af698adbd49a6b96172c1a7cda5f4fd96fbf7e9d5b1aeb1d0851caf702c28351239edc4c62f76b2ba919e6e686f2d
7
+ data.tar.gz: 9c6f42e8c30a6a803c42409192757ecf13f03398771e682d2cf98976a391e7a5976af38a8d5eea972fcc3560ff16e8dffbfa5e3cc2eef96228c2a2fbd8bea030
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: 'github-actions'
4
+ directory: '/'
5
+ schedule:
6
+ interval: 'weekly'
@@ -13,9 +13,9 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
15
  matrix:
16
- ruby: [ '2.5.8', '2.6.6', '2.7.1', 'ruby-head', 'jruby-9.2.11.1', 'jruby-head' ]
16
+ ruby: [ '2.5', '2.6', '2.7', '3.0', 'ruby-head', 'jruby-9.2', 'jruby-head' ]
17
17
  steps:
18
- - uses: actions/checkout@v1
18
+ - uses: actions/checkout@v3
19
19
  - uses: ruby/setup-ruby@v1
20
20
  with:
21
21
  ruby-version: ${{ matrix.ruby }}
@@ -27,7 +27,7 @@ jobs:
27
27
  benchmark:
28
28
  runs-on: ubuntu-latest
29
29
  steps:
30
- - uses: actions/checkout@v1
30
+ - uses: actions/checkout@v3
31
31
  - uses: ruby/setup-ruby@v1
32
32
  with:
33
33
  ruby-version: 2.7.1
data/CHANGELOG.md CHANGED
@@ -1,6 +1,41 @@
1
- ## v1.5.0
1
+ ## v1.6.1
2
2
 
3
- - Suggest require paths on LoadError ([#143](https://github.com/ruby/did_you_mean/pull/143))
3
+ _<sup>released at 2020-12-22 13:22:35 UTC</sup>_
4
+
5
+ #### Deprecations
6
+
7
+ - Deprecate custom formatters to reduce complexity for Ractor support.
8
+ - Deprecate access to the `DidYouMean::SPELL_CHECKERS` constant for Ractor support.
9
+
10
+ #### Features
11
+
12
+ - The `did_you_mean` gem is now Ractor-compatible (`8faba54b`)
13
+ - Suggest keys on NoMatchingPatternKeyError (#159, @k-tsj)
14
+ - Make the same name check case-sensitive (#164, @pocke)
15
+
16
+ Before:
17
+ ```ruby
18
+ DidYouMean::SpellChecker.new(dictionary: ['Method', 'MEthod']).correct("MEthod")
19
+ # => ['Method', 'method']
20
+ ```
21
+
22
+ After:
23
+ ```ruby
24
+ DidYouMean::SpellChecker.new(dictionary: ['Method', 'MEthod']).correct("MEthod")
25
+ # => ['Method']
26
+ ```
27
+
28
+ ## v1.6.0 (yanked)
29
+
30
+ _This version has been yanked due to significant and unexpected breaking changes._
31
+
32
+ ## [v1.5.0](https://github.com/ruby/did_you_mean/tree/v1.5.0)
33
+
34
+ _<sup>released at 2020-12-22 05:47:21 UTC</sup>_
35
+
36
+ #### Features
37
+
38
+ - Suggest require paths on LoadError ([#143](https://github.com/ruby/did_you_mean/pull/143))
4
39
 
5
40
  ## [v1.4.0](https://github.com/ruby/did_you_mean/tree/v1.4.0)
6
41
 
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/ruby/did_you_mean.svg?branch=master)](https://travis-ci.org/ruby/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://github.com/ruby/did_you_mean/actions/workflows/ruby.yml/badge.svg)](https://github.com/ruby/did_you_mean/actions/workflows/ruby.yml)
2
2
 
3
3
  ## Installation
4
4
 
@@ -69,21 +69,13 @@ require 'net-http'
69
69
  # Did you mean? net/http
70
70
  ```
71
71
 
72
- ## Verbose Formatter
73
-
74
- This verbose formatter changes the error message format to take more lines/spaces so it'll be slightly easier to read the suggestions. This formatter can totally be used in any environment including production.
72
+ ### NoMatchingPatternKeyError
75
73
 
76
74
  ```ruby
77
- OBject
78
- # => NameError: uninitialized constant OBject
79
- # Did you mean? Object
80
-
81
- require 'did_you_mean/verbose'
82
- OBject
83
- # => NameError: uninitialized constant OBject
84
- #
85
- # Did you mean? Object
86
- #
75
+ hash = {foo: 1, bar: 2, baz: 3}
76
+ hash => {fooo:}
77
+ # => NoMatchingPatternKeyError: key not found: :fooo
78
+ # Did you mean? :foo
87
79
  ```
88
80
 
89
81
  ## Using the `DidYouMean::SpellChecker`
@@ -107,7 +99,7 @@ $ ruby --disable-did_you_mean -e "1.zeor?"
107
99
  -e:1:in `<main>': undefined method `zeor?' for 1:Integer (NameError)
108
100
  ```
109
101
 
110
- When you do not have direct access to the `ruby` command (e.g. `rails console`, `irb`), you could applyoptions using the
102
+ When you do not have direct access to the `ruby` command (e.g. `rails console`, `irb`), you could apply options using the
111
103
  `RUBYOPT` environment variable:
112
104
 
113
105
  ```bash
data/Rakefile CHANGED
@@ -2,12 +2,12 @@ require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new do |task|
5
- task.libs << "test"
5
+ task.libs << "test/lib" << "test"
6
6
 
7
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
+ task.ruby_opts = %w[ --disable-did_you_mean -rhelper ]
11
11
  end
12
12
 
13
13
  Rake::TestTask.new("test:explore") do |task|
@@ -21,6 +21,13 @@ end
21
21
 
22
22
  task default: %i(test)
23
23
 
24
+ task :sync_tool do
25
+ require 'fileutils'
26
+ FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib"
27
+ FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib"
28
+ FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib"
29
+ end
30
+
24
31
  namespace :test do
25
32
  namespace :accuracy do
26
33
  desc "Download Wiktionary's Simple English data and save it as a dictionary"
@@ -11,7 +11,7 @@ where
11
11
  * that are used to correct a misspelling
12
12
  * The dictionary must be tree structured with a single character separator
13
13
  * e.g 'spec/models/goals_spec_rb'.
14
- ####separator: A single charactor. Cannot be cannot be alphabetical, '@' or '.'.
14
+ ####separator: A single character. Cannot be cannot be alphabetical, '@' or '.'.
15
15
  ####augment: When set to true, the checker will used the standard ```SpellChecker``` to find possible suggestions.
16
16
  ## Methods
17
17
  ```
@@ -1,17 +1,49 @@
1
1
  module DidYouMean
2
2
  module Correctable
3
- def original_message
4
- method(:to_s).super_method.call
5
- end
3
+ if Exception.method_defined?(:detailed_message)
4
+ # just for compatibility
5
+ def original_message
6
+ # we cannot use alias here because
7
+ to_s
8
+ end
9
+
10
+ def detailed_message(highlight: true, did_you_mean: true, **)
11
+ msg = super.dup
12
+
13
+ return msg unless did_you_mean
14
+
15
+ suggestion = DidYouMean.formatter.message_for(corrections)
16
+
17
+ if highlight
18
+ suggestion = suggestion.gsub(/.+/) { "\e[1m" + $& + "\e[m" }
19
+ end
20
+
21
+ msg << suggestion
22
+ msg
23
+ rescue
24
+ super
25
+ end
26
+ else
27
+ SKIP_TO_S_FOR_SUPER_LOOKUP = true
28
+ private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP
29
+
30
+ def original_message
31
+ meth = method(:to_s)
32
+ while meth.owner.const_defined?(:SKIP_TO_S_FOR_SUPER_LOOKUP)
33
+ meth = meth.super_method
34
+ end
35
+ meth.call
36
+ end
6
37
 
7
- def to_s
8
- msg = super.dup
9
- suggestion = DidYouMean.formatter.message_for(corrections)
38
+ def to_s
39
+ msg = super.dup
40
+ suggestion = DidYouMean.formatter.message_for(corrections)
10
41
 
11
- msg << suggestion if !msg.end_with?(suggestion)
12
- msg
13
- rescue
14
- super
42
+ msg << suggestion if !msg.include?(suggestion)
43
+ msg
44
+ rescue
45
+ super
46
+ end
15
47
  end
16
48
 
17
49
  def corrections
@@ -19,7 +51,7 @@ module DidYouMean
19
51
  end
20
52
 
21
53
  def spell_checker
22
- SPELL_CHECKERS[self.class.to_s].new(self)
54
+ DidYouMean.spell_checkers[self.class.to_s].new(self)
23
55
  end
24
56
  end
25
57
  end
@@ -0,0 +1,44 @@
1
+ # frozen-string-literal: true
2
+
3
+ module DidYouMean
4
+ # The +DidYouMean::Formatter+ is the basic, default formatter for the
5
+ # gem. The formatter responds to the +message_for+ method and it returns a
6
+ # human readable string.
7
+ class Formatter
8
+
9
+ # Returns a human readable string that contains +corrections+. This
10
+ # formatter is designed to be less verbose to not take too much screen
11
+ # space while being helpful enough to the user.
12
+ #
13
+ # @example
14
+ #
15
+ # formatter = DidYouMean::Formatter.new
16
+ #
17
+ # # displays suggestions in two lines with the leading empty line
18
+ # puts formatter.message_for(["methods", "method"])
19
+ #
20
+ # Did you mean? methods
21
+ # method
22
+ # # => nil
23
+ #
24
+ # # displays an empty line
25
+ # puts formatter.message_for([])
26
+ #
27
+ # # => nil
28
+ #
29
+ def self.message_for(corrections)
30
+ corrections.empty? ? "" : "\nDid you mean? #{corrections.join("\n ")}"
31
+ end
32
+
33
+ def message_for(corrections)
34
+ warn "The instance method #message_for has been deprecated. Please use the class method " \
35
+ "DidYouMean::Formatter.message_for(...) instead."
36
+
37
+ self.class.message_for(corrections)
38
+ end
39
+ end
40
+
41
+ PlainFormatter = Formatter
42
+
43
+ deprecate_constant :PlainFormatter
44
+ end
@@ -1,33 +1,4 @@
1
- # frozen-string-literal: true
1
+ require_relative '../formatter'
2
2
 
3
- module DidYouMean
4
- # The +DidYouMean::PlainFormatter+ is the basic, default formatter for the
5
- # gem. The formatter responds to the +message_for+ method and it returns a
6
- # human readable string.
7
- class PlainFormatter
8
-
9
- # Returns a human readable string that contains +corrections+. This
10
- # formatter is designed to be less verbose to not take too much screen
11
- # space while being helpful enough to the user.
12
- #
13
- # @example
14
- #
15
- # formatter = DidYouMean::PlainFormatter.new
16
- #
17
- # # displays suggestions in two lines with the leading empty line
18
- # puts formatter.message_for(["methods", "method"])
19
- #
20
- # Did you mean? methods
21
- # method
22
- # # => nil
23
- #
24
- # # displays an empty line
25
- # puts formatter.message_for([])
26
- #
27
- # # => nil
28
- #
29
- def message_for(corrections)
30
- corrections.empty? ? "" : "\nDid you mean? #{corrections.join("\n ")}"
31
- end
32
- end
33
- end
3
+ warn "`require 'did_you_mean/formatters/plain_formatter'` is deprecated. Please `require 'did_you_mean/formatter'` " \
4
+ "instead."
@@ -1,49 +1,10 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- module DidYouMean
4
- # The +DidYouMean::VerboseFormatter+ uses extra empty lines to make the
5
- # suggestion stand out more in the error message.
6
- #
7
- # In order to activate the verbose formatter,
8
- #
9
- # @example
10
- #
11
- # OBject
12
- # # => NameError: uninitialized constant OBject
13
- # # Did you mean? Object
14
- #
15
- # require 'did_you_mean/verbose'
16
- #
17
- # OBject
18
- # # => NameError: uninitialized constant OBject
19
- # #
20
- # # Did you mean? Object
21
- # #
22
- #
23
- class VerboseFormatter
3
+ warn "`require 'did_you_mean/formatters/verbose_formatter'` is deprecated and falls back to the default formatter. "
24
4
 
25
- # Returns a human readable string that contains +corrections+. This
26
- # formatter is designed to be less verbose to not take too much screen
27
- # space while being helpful enough to the user.
28
- #
29
- # @example
30
- #
31
- # formatter = DidYouMean::PlainFormatter.new
32
- #
33
- # puts formatter.message_for(["methods", "method"])
34
- #
35
- #
36
- # Did you mean? methods
37
- # method
38
- #
39
- # # => nil
40
- #
41
- def message_for(corrections)
42
- return "" if corrections.empty?
5
+ require_relative '../formatter'
43
6
 
44
- output = "\n\n Did you mean? ".dup
45
- output << corrections.join("\n ")
46
- output << "\n "
47
- end
48
- end
7
+ module DidYouMean
8
+ # For compatibility:
9
+ VerboseFormatter = Formatter
49
10
  end
@@ -10,25 +10,25 @@ module DidYouMean
10
10
  end
11
11
 
12
12
  def correct(input)
13
- input = normalize(input)
14
- threshold = input.length > 3 ? 0.834 : 0.77
13
+ normalized_input = normalize(input)
14
+ threshold = normalized_input.length > 3 ? 0.834 : 0.77
15
15
 
16
- words = @dictionary.select { |word| JaroWinkler.distance(normalize(word), input) >= threshold }
17
- words.reject! { |word| input == word.to_s }
18
- words.sort_by! { |word| JaroWinkler.distance(word.to_s, input) }
16
+ words = @dictionary.select { |word| JaroWinkler.distance(normalize(word), normalized_input) >= threshold }
17
+ words.reject! { |word| input.to_s == word.to_s }
18
+ words.sort_by! { |word| JaroWinkler.distance(word.to_s, normalized_input) }
19
19
  words.reverse!
20
20
 
21
21
  # Correct mistypes
22
- threshold = (input.length * 0.25).ceil
23
- corrections = words.select { |c| Levenshtein.distance(normalize(c), input) <= threshold }
22
+ threshold = (normalized_input.length * 0.25).ceil
23
+ corrections = words.select { |c| Levenshtein.distance(normalize(c), normalized_input) <= threshold }
24
24
 
25
25
  # Correct misspells
26
26
  if corrections.empty?
27
27
  corrections = words.select do |word|
28
28
  word = normalize(word)
29
- length = input.length < word.length ? input.length : word.length
29
+ length = normalized_input.length < word.length ? normalized_input.length : word.length
30
30
 
31
- Levenshtein.distance(word, input) < length
31
+ Levenshtein.distance(word, normalized_input) < length
32
32
  end.first(1)
33
33
  end
34
34
 
@@ -6,6 +6,7 @@ module DidYouMean
6
6
 
7
7
  NAMES_TO_EXCLUDE = { NilClass => nil.methods }
8
8
  NAMES_TO_EXCLUDE.default = []
9
+ Ractor.make_shareable(NAMES_TO_EXCLUDE) if defined?(Ractor)
9
10
 
10
11
  # +MethodNameChecker::RB_RESERVED_WORDS+ is the list of reserved words in
11
12
  # Ruby that take an argument. Unlike
@@ -36,6 +37,8 @@ module DidYouMean
36
37
  yield
37
38
  )
38
39
 
40
+ Ractor.make_shareable(RB_RESERVED_WORDS) if defined?(Ractor)
41
+
39
42
  def initialize(exception)
40
43
  @method_name = exception.name
41
44
  @receiver = exception.receiver
@@ -8,6 +8,7 @@ module DidYouMean
8
8
 
9
9
  NAMES_TO_EXCLUDE = { 'foo' => [:fork, :for] }
10
10
  NAMES_TO_EXCLUDE.default = []
11
+ Ractor.make_shareable(NAMES_TO_EXCLUDE) if defined?(Ractor)
11
12
 
12
13
  # +VariableNameChecker::RB_RESERVED_WORDS+ is the list of all reserved
13
14
  # words in Ruby. They could be declared like methods are, and a typo would
@@ -62,6 +63,8 @@ module DidYouMean
62
63
  __ENCODING__
63
64
  )
64
65
 
66
+ Ractor.make_shareable(RB_RESERVED_WORDS) if defined?(Ractor)
67
+
65
68
  def initialize(exception)
66
69
  @name = exception.name.to_s.tr("@", "")
67
70
  @lvar_names = exception.respond_to?(:local_variables) ? exception.local_variables : []
@@ -76,7 +79,7 @@ module DidYouMean
76
79
  def corrections
77
80
  @corrections ||= SpellChecker
78
81
  .new(dictionary: (RB_RESERVED_WORDS + lvar_names + method_names + ivar_names + cvar_names))
79
- .correct(name) - NAMES_TO_EXCLUDE[@name]
82
+ .correct(name).uniq - NAMES_TO_EXCLUDE[@name]
80
83
  end
81
84
  end
82
85
  end
@@ -0,0 +1,20 @@
1
+ require_relative "../spell_checker"
2
+
3
+ module DidYouMean
4
+ class PatternKeyNameChecker
5
+ def initialize(no_matching_pattern_key_error)
6
+ @key = no_matching_pattern_key_error.key
7
+ @keys = no_matching_pattern_key_error.matchee.keys
8
+ end
9
+
10
+ def corrections
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)
18
+ end
19
+ end
20
+ end
@@ -2,13 +2,17 @@
2
2
 
3
3
  require_relative "../spell_checker"
4
4
  require_relative "../tree_spell_checker"
5
+ require "rbconfig"
5
6
 
6
7
  module DidYouMean
7
8
  class RequirePathChecker
8
9
  attr_reader :path
9
10
 
10
11
  INITIAL_LOAD_PATH = $LOAD_PATH.dup.freeze
11
- ENV_SPECIFIC_EXT = ".#{RbConfig::CONFIG["DLEXT"]}"
12
+ Ractor.make_shareable(INITIAL_LOAD_PATH) if defined?(Ractor)
13
+
14
+ ENV_SPECIFIC_EXT = ".#{RbConfig::CONFIG["DLEXT"]}"
15
+ Ractor.make_shareable(ENV_SPECIFIC_EXT) if defined?(Ractor)
12
16
 
13
17
  private_constant :INITIAL_LOAD_PATH, :ENV_SPECIFIC_EXT
14
18
 
@@ -1,4 +1,2 @@
1
- require_relative '../did_you_mean'
2
- require_relative 'formatters/verbose_formatter'
3
-
4
- DidYouMean.formatter = DidYouMean::VerboseFormatter.new
1
+ warn "The verbose formatter has been removed and now `require 'did_you_mean/verbose'` has no effect. Please " \
2
+ "remove this call."
@@ -1,3 +1,3 @@
1
1
  module DidYouMean
2
- VERSION = "1.5.0"
2
+ VERSION = "1.6.2".freeze
3
3
  end
data/lib/did_you_mean.rb CHANGED
@@ -7,7 +7,8 @@ require_relative 'did_you_mean/spell_checkers/method_name_checker'
7
7
  require_relative 'did_you_mean/spell_checkers/key_error_checker'
8
8
  require_relative 'did_you_mean/spell_checkers/null_checker'
9
9
  require_relative 'did_you_mean/spell_checkers/require_path_checker'
10
- require_relative 'did_you_mean/formatters/plain_formatter'
10
+ require_relative 'did_you_mean/spell_checkers/pattern_key_name_checker'
11
+ require_relative 'did_you_mean/formatter'
11
12
  require_relative 'did_you_mean/tree_spell_checker'
12
13
 
13
14
  # The +DidYouMean+ gem adds functionality to suggest possible method/class
@@ -85,28 +86,70 @@ require_relative 'did_you_mean/tree_spell_checker'
85
86
  #
86
87
  module DidYouMean
87
88
  # Map of error types and spell checker objects.
88
- SPELL_CHECKERS = Hash.new(NullChecker)
89
+ @spell_checkers = Hash.new(NullChecker)
90
+
91
+ # Returns a sharable hash map of error types and spell checker objects.
92
+ def self.spell_checkers
93
+ @spell_checkers
94
+ end
89
95
 
90
96
  # Adds +DidYouMean+ functionality to an error using a given spell checker
91
97
  def self.correct_error(error_class, spell_checker)
92
- SPELL_CHECKERS[error_class.name] = spell_checker
93
- error_class.prepend(Correctable) unless error_class < Correctable
98
+ if defined?(Ractor)
99
+ new_mapping = { **@spell_checkers, error_class.to_s => spell_checker }
100
+ new_mapping.default = NullChecker
101
+
102
+ @spell_checkers = Ractor.make_shareable(new_mapping)
103
+ else
104
+ spell_checkers[error_class.to_s] = spell_checker
105
+ end
106
+
107
+ error_class.prepend(Correctable) if error_class.is_a?(Class) && !(error_class < Correctable)
94
108
  end
95
109
 
96
110
  correct_error NameError, NameErrorCheckers
97
111
  correct_error KeyError, KeyErrorChecker
98
112
  correct_error NoMethodError, MethodNameChecker
99
113
  correct_error LoadError, RequirePathChecker if RUBY_VERSION >= '2.8.0'
114
+ correct_error NoMatchingPatternKeyError, PatternKeyNameChecker if defined?(::NoMatchingPatternKeyError)
115
+
116
+ # TODO: Remove on 3.3:
117
+ class DeprecatedMapping # :nodoc:
118
+ def []=(key, value)
119
+ warn "Calling `DidYouMean::SPELL_CHECKERS[#{key.to_s}] = #{value.to_s}' has been deprecated. " \
120
+ "Please call `DidYouMean.correct_error(#{key.to_s}, #{value.to_s})' instead."
121
+
122
+ DidYouMean.correct_error(key, value)
123
+ end
124
+
125
+ def merge!(hash)
126
+ warn "Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. " \
127
+ "Please call `DidYouMean.correct_error(error_name, spell_checker)' instead."
128
+
129
+ hash.each do |error_class, spell_checker|
130
+ DidYouMean.correct_error(error_class, spell_checker)
131
+ end
132
+ end
133
+ end
134
+
135
+ # TODO: Remove on 3.3:
136
+ SPELL_CHECKERS = DeprecatedMapping.new
137
+ deprecate_constant :SPELL_CHECKERS
138
+ private_constant :DeprecatedMapping
100
139
 
101
140
  # Returns the currently set formatter. By default, it is set to +DidYouMean::Formatter+.
102
141
  def self.formatter
103
- @@formatter
142
+ if defined?(Ractor)
143
+ Ractor.current[:__did_you_mean_formatter__] || Formatter
144
+ else
145
+ Formatter
146
+ end
104
147
  end
105
148
 
106
149
  # Updates the primary formatter used to format the suggestions.
107
150
  def self.formatter=(formatter)
108
- @@formatter = formatter
151
+ if defined?(Ractor)
152
+ Ractor.current[:__did_you_mean_formatter__] = formatter
153
+ end
109
154
  end
110
-
111
- self.formatter = PlainFormatter.new
112
155
  end
@@ -1,7 +1,9 @@
1
1
  require_relative '../helper'
2
2
 
3
3
  class NameErrorExtensionTest < Test::Unit::TestCase
4
- SPELL_CHECKERS = DidYouMean::SPELL_CHECKERS
4
+ include DidYouMean::TestHelper
5
+
6
+ SPELL_CHECKERS = DidYouMean.spell_checkers
5
7
 
6
8
  class TestSpellChecker
7
9
  def initialize(*); end
@@ -9,18 +11,23 @@ class NameErrorExtensionTest < Test::Unit::TestCase
9
11
  end
10
12
 
11
13
  def setup
12
- @org, SPELL_CHECKERS['NameError'] = SPELL_CHECKERS['NameError'], TestSpellChecker
14
+ @original_spell_checker = DidYouMean.spell_checkers['NameError']
15
+ DidYouMean.correct_error(NameError, TestSpellChecker)
13
16
 
14
17
  @error = assert_raise(NameError){ doesnt_exist }
15
18
  end
16
19
 
17
20
  def teardown
18
- SPELL_CHECKERS['NameError'] = @org
21
+ DidYouMean.correct_error(NameError, @original_spell_checker)
19
22
  end
20
23
 
21
24
  def test_message
22
- assert_match(/Did you mean\? does_exist/, @error.to_s)
23
- assert_match(/Did you mean\? does_exist/, @error.message)
25
+ if Exception.method_defined?(:detailed_message)
26
+ assert_match(/Did you mean\? does_exist/, @error.detailed_message)
27
+ else
28
+ assert_match(/Did you mean\? does_exist/, @error.to_s)
29
+ assert_match(/Did you mean\? does_exist/, @error.message)
30
+ end
24
31
  end
25
32
 
26
33
  def test_to_s_does_not_make_disruptive_changes_to_error_message
@@ -28,8 +35,8 @@ class NameErrorExtensionTest < Test::Unit::TestCase
28
35
  raise NameError, "uninitialized constant Object"
29
36
  end
30
37
 
31
- error.to_s
32
- assert_equal 1, error.to_s.scan("Did you mean?").count
38
+ get_message(error)
39
+ assert_equal 1, get_message(error).scan("Did you mean?").count
33
40
  end
34
41
 
35
42
  def test_correctable_error_objects_are_dumpable
@@ -40,7 +47,7 @@ class NameErrorExtensionTest < Test::Unit::TestCase
40
47
  e
41
48
  end
42
49
 
43
- error.to_s
50
+ get_message(error)
44
51
 
45
52
  assert_equal "undefined method `sizee' for #<File:test_name_error_extension.rb (closed)>",
46
53
  Marshal.load(Marshal.dump(error)).original_message