summaryrefslogtreecommitdiff
path: root/lib/did_you_mean.rb
blob: 2df238da06d66ce839bb9aee0597e5773c54ee77 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
require_relative "did_you_mean/version"
require_relative "did_you_mean/core_ext/name_error"

require_relative "did_you_mean/spell_checker"
require_relative 'did_you_mean/spell_checkers/name_error_checkers'
require_relative 'did_you_mean/spell_checkers/method_name_checker'
require_relative 'did_you_mean/spell_checkers/key_error_checker'
require_relative 'did_you_mean/spell_checkers/null_checker'
require_relative 'did_you_mean/spell_checkers/require_path_checker'
require_relative 'did_you_mean/spell_checkers/pattern_key_name_checker'
require_relative 'did_you_mean/formatter'
require_relative 'did_you_mean/tree_spell_checker'

# The +DidYouMean+ gem adds functionality to suggest possible method/class
# names upon errors such as +NameError+ and +NoMethodError+. In Ruby 2.3 or
# later, it is automatically activated during startup.
#
# @example
#
#   methosd
#   # => NameError: undefined local variable or method `methosd' for main:Object
#   #   Did you mean?  methods
#   #                  method
#
#   OBject
#   # => NameError: uninitialized constant OBject
#   #    Did you mean?  Object
#
#   @full_name = "Yuki Nishijima"
#   first_name, last_name = full_name.split(" ")
#   # => NameError: undefined local variable or method `full_name' for main:Object
#   #    Did you mean?  @full_name
#
#   @@full_name = "Yuki Nishijima"
#   @@full_anme
#   # => NameError: uninitialized class variable @@full_anme in Object
#   #    Did you mean?  @@full_name
#
#   full_name = "Yuki Nishijima"
#   full_name.starts_with?("Y")
#   # => NoMethodError: undefined method `starts_with?' for "Yuki Nishijima":String
#   #    Did you mean?  start_with?
#
#   hash = {foo: 1, bar: 2, baz: 3}
#   hash.fetch(:fooo)
#   # => KeyError: key not found: :fooo
#   #    Did you mean?  :foo
#
#
# == Disabling +did_you_mean+
#
# Occasionally, you may want to disable the +did_you_mean+ gem for e.g.
# debugging issues in the error object itself. You can disable it entirely by
# specifying +--disable-did_you_mean+ option to the +ruby+ command:
#
#   $ ruby --disable-did_you_mean -e "1.zeor?"
#   -e:1:in `<main>': undefined method `zeor?' for 1:Integer (NameError)
#
# When you do not have direct access to the +ruby+ command (e.g.
# +rails console+, +irb+), you could applyoptions using the +RUBYOPT+
# environment variable:
#
#   $ RUBYOPT='--disable-did_you_mean' irb
#   irb:0> 1.zeor?
#   # => NoMethodError (undefined method `zeor?' for 1:Integer)
#
#
# == Getting the original error message
#
# Sometimes, you do not want to disable the gem entirely, but need to get the
# original error message without suggestions (e.g. testing). In this case, you
# could use the +#original_message+ method on the error object:
#
#   no_method_error = begin
#                       1.zeor?
#                     rescue NoMethodError => error
#                       error
#                     end
#
#   no_method_error.message
#   # => NoMethodError (undefined method `zeor?' for 1:Integer)
#   #    Did you mean?  zero?
#
#   no_method_error.original_message
#   # => NoMethodError (undefined method `zeor?' for 1:Integer)
#
module DidYouMean
  # Map of error types and spell checker objects.
  @spell_checkers = Hash.new(NullChecker)

  # Returns a sharable hash map of error types and spell checker objects.
  def self.spell_checkers
    @spell_checkers
  end

  # Adds +DidYouMean+ functionality to an error using a given spell checker
  def self.correct_error(error_class, spell_checker)
    if defined?(Ractor)
      new_mapping = { **@spell_checkers, error_class.to_s => spell_checker }
      new_mapping.default = NullChecker

      @spell_checkers = Ractor.make_shareable(new_mapping)
    else
      spell_checkers[error_class.to_s] = spell_checker
    end

    error_class.prepend(Correctable) if error_class.is_a?(Class) && !(error_class < Correctable)
  end

  correct_error NameError, NameErrorCheckers
  correct_error KeyError, KeyErrorChecker
  correct_error NoMethodError, MethodNameChecker
  correct_error LoadError, RequirePathChecker if RUBY_VERSION >= '2.8.0'
  correct_error NoMatchingPatternKeyError, PatternKeyNameChecker if defined?(::NoMatchingPatternKeyError)

  # TODO: Remove on 3.3:
  class DeprecatedMapping # :nodoc:
    def []=(key, value)
      warn "Calling `DidYouMean::SPELL_CHECKERS[#{key.to_s}] = #{value.to_s}' has been deprecated. " \
           "Please call `DidYouMean.correct_error(#{key.to_s}, #{value.to_s})' instead."

      DidYouMean.correct_error(key, value)
    end

    def merge!(hash)
      warn "Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. " \
           "Please call `DidYouMean.correct_error(error_name, spell_checker)' instead."

      hash.each do |error_class, spell_checker|
        DidYouMean.correct_error(error_class, spell_checker)
      end
    end
  end

  # TODO: Remove on 3.3:
  SPELL_CHECKERS = DeprecatedMapping.new
  deprecate_constant :SPELL_CHECKERS
  private_constant :DeprecatedMapping

  # Returns the currently set formatter. By default, it is set to +DidYouMean::Formatter+.
  def self.formatter
    if defined?(Ractor)
      Ractor.current[:__did_you_mean_formatter__] || Formatter
    else
      Formatter
    end
  end

  # Updates the primary formatter used to format the suggestions.
  def self.formatter=(formatter)
    if defined?(Ractor)
      Ractor.current[:__did_you_mean_formatter__] = formatter
    end
  end
end