did_you_mean-2.0.0/0000755000004100000410000000000014735503325014204 5ustar www-datawww-datadid_you_mean-2.0.0/did_you_mean.gemspec0000644000004100000410000000165714735503325020216 0ustar www-datawww-data# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) begin require_relative "lib/did_you_mean/version" rescue LoadError # Fallback to load version file in ruby core repository require_relative "version" end Gem::Specification.new do |spec| spec.name = "did_you_mean" spec.version = DidYouMean::VERSION spec.authors = ["Yuki Nishijima"] spec.email = ["mail@yukinishijima.net"] spec.summary = '"Did you mean?" experience in Ruby' spec.description = 'The gem that has been saving people from typos since 2014.' spec.homepage = "https://github.com/ruby/did_you_mean" spec.license = "MIT" spec.files = `git ls-files`.split($/).reject{|path| path.start_with?('evaluation/') } spec.test_files = spec.files.grep(%r{^(test)/}) spec.require_paths = ["lib"] spec.required_ruby_version = '>= 2.5.0' end did_you_mean-2.0.0/.gitignore0000644000004100000410000000027514735503325016200 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc test/tmp test/version_tmp tmp log evaluation/dictionary.yml benchmark/results did_you_mean-2.0.0/.github/0000755000004100000410000000000014735503325015544 5ustar www-datawww-datadid_you_mean-2.0.0/.github/dependabot.yml0000644000004100000410000000016614735503325020377 0ustar www-datawww-dataversion: 2 updates: - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'weekly' did_you_mean-2.0.0/.github/workflows/0000755000004100000410000000000014735503325017601 5ustar www-datawww-datadid_you_mean-2.0.0/.github/workflows/ruby.yml0000644000004100000410000000211714735503325021306 0ustar www-datawww-dataname: Ruby on: pull_request: branches: - 'master' push: branches: - 'master' jobs: ruby-versions: uses: ruby/actions/.github/workflows/ruby_versions.yml@master with: engine: cruby-jruby min_version: 2.5 build: needs: ruby-versions runs-on: ubuntu-latest strategy: matrix: ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Build and test with Rake run: | gem i test-unit test-unit-ruby-core RUBYOPT='--disable-did_you_mean' rake benchmark: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: 3.3.6 - name: Test performance and accuracy run: | gem install bundler bundle install --jobs 4 --retry 2 RUBYOPT='--disable-did_you_mean' bundle exec rake test:accuracy RUBYOPT='--disable-did_you_mean' bundle exec rake benchmark:memory did_you_mean-2.0.0/lib/0000755000004100000410000000000014735503325014752 5ustar www-datawww-datadid_you_mean-2.0.0/lib/did_you_mean/0000755000004100000410000000000014735503325017406 5ustar www-datawww-datadid_you_mean-2.0.0/lib/did_you_mean/jaro_winkler.rb0000644000004100000410000000341014735503325022417 0ustar www-datawww-datamodule DidYouMean module Jaro module_function def distance(str1, str2) str1, str2 = str2, str1 if str1.length > str2.length length1, length2 = str1.length, str2.length m = 0.0 t = 0.0 range = length2 > 3 ? length2 / 2 - 1 : 0 flags1 = 0 flags2 = 0 # Avoid duplicating enumerable objects str1_codepoints = str1.codepoints str2_codepoints = str2.codepoints i = 0 while i < length1 last = i + range j = (i >= range) ? i - range : 0 while j <= last if flags2[j] == 0 && str1_codepoints[i] == str2_codepoints[j] flags2 |= (1 << j) flags1 |= (1 << i) m += 1 break end j += 1 end i += 1 end k = i = 0 while i < length1 if flags1[i] != 0 j = index = k k = while j < length2 index = j break(j + 1) if flags2[j] != 0 j += 1 end t += 1 if str1_codepoints[i] != str2_codepoints[index] end i += 1 end t = (t / 2).floor m == 0 ? 0 : (m / length1 + m / length2 + (m - t) / m) / 3 end end module JaroWinkler WEIGHT = 0.1 THRESHOLD = 0.7 module_function def distance(str1, str2) jaro_distance = Jaro.distance(str1, str2) if jaro_distance > THRESHOLD codepoints2 = str2.codepoints prefix_bonus = 0 str1.each_codepoint do |char1| char1 == codepoints2[prefix_bonus] && prefix_bonus < 4 ? prefix_bonus += 1 : break end jaro_distance + (prefix_bonus * WEIGHT * (1 - jaro_distance)) else jaro_distance end end end end did_you_mean-2.0.0/lib/did_you_mean/tree_spell_checker.rb0000644000004100000410000000547114735503325023564 0ustar www-datawww-data# frozen_string_literal: true module DidYouMean # spell checker for a dictionary that has a tree # structure, see doc/tree_spell_checker_api.md class TreeSpellChecker attr_reader :dictionary, :separator, :augment def initialize(dictionary:, separator: '/', augment: nil) @dictionary = dictionary @separator = separator @augment = augment end def correct(input) plausibles = plausible_dimensions(input) return fall_back_to_normal_spell_check(input) if plausibles.empty? suggestions = find_suggestions(input, plausibles) return fall_back_to_normal_spell_check(input) if suggestions.empty? suggestions end def dictionary_without_leaves @dictionary_without_leaves ||= dictionary.map { |word| word.split(separator)[0..-2] }.uniq end def tree_depth @tree_depth ||= dictionary_without_leaves.max { |a, b| a.size <=> b.size }.size end def dimensions @dimensions ||= tree_depth.times.map do |index| dictionary_without_leaves.map { |element| element[index] }.compact.uniq end end def find_leaves(path) path_with_separator = "#{path}#{separator}" dictionary .select {|str| str.include?(path_with_separator) } .map {|str| str.gsub(path_with_separator, '') } end def plausible_dimensions(input) input.split(separator)[0..-2] .map .with_index { |element, index| correct_element(dimensions[index], element) if dimensions[index] } .compact end def possible_paths(states) states.map { |state| state.join(separator) } end private def find_suggestions(input, plausibles) states = plausibles[0].product(*plausibles[1..-1]) paths = possible_paths(states) leaf = input.split(separator).last find_ideas(paths, leaf) end def fall_back_to_normal_spell_check(input) return [] unless augment ::DidYouMean::SpellChecker.new(dictionary: dictionary).correct(input) end def find_ideas(paths, leaf) paths.flat_map do |path| names = find_leaves(path) ideas = correct_element(names, leaf) ideas_to_paths(ideas, leaf, names, path) end.compact end def ideas_to_paths(ideas, leaf, names, path) if ideas.empty? nil elsif names.include?(leaf) ["#{path}#{separator}#{leaf}"] else ideas.map {|str| "#{path}#{separator}#{str}" } end end def correct_element(names, element) return names if names.size == 1 str = normalize(element) return [str] if names.include?(str) ::DidYouMean::SpellChecker.new(dictionary: names).correct(str) end def normalize(str) str.downcase! str.tr!('@', ' ') if str.include?('@') str end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/0000755000004100000410000000000014735503325022374 5ustar www-datawww-datadid_you_mean-2.0.0/lib/did_you_mean/spell_checkers/null_checker.rb0000644000004100000410000000015014735503325025353 0ustar www-datawww-datamodule DidYouMean class NullChecker def initialize(*); end def corrections; [] end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/name_error_checkers.rb0000644000004100000410000000106714735503325026725 0ustar www-datawww-datarequire_relative 'name_error_checkers/class_name_checker' require_relative 'name_error_checkers/variable_name_checker' module DidYouMean class << (NameErrorCheckers = Object.new) def new(exception) case exception.original_message when /uninitialized constant/ ClassNameChecker when /undefined local variable or method/, /undefined method/, /uninitialized class variable/, /no member '.*' in struct/ VariableNameChecker else NullChecker end.new(exception) end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/key_error_checker.rb0000644000004100000410000000122714735503325026410 0ustar www-datawww-datarequire_relative "../spell_checker" module DidYouMean class KeyErrorChecker def initialize(key_error) @key = key_error.key @keys = key_error.receiver.keys end def corrections @corrections ||= exact_matches.empty? ? SpellChecker.new(dictionary: @keys).correct(@key).map(&:inspect) : exact_matches end private def exact_matches @exact_matches ||= @keys.select { |word| @key == word.to_s }.map { |obj| format_object(obj) } end def format_object(symbol_or_object) if symbol_or_object.is_a?(Symbol) ":#{symbol_or_object}" else symbol_or_object.to_s end end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/method_name_checker.rb0000644000004100000410000000434514735503325026673 0ustar www-datawww-datarequire_relative "../spell_checker" module DidYouMean class MethodNameChecker attr_reader :method_name, :receiver NAMES_TO_EXCLUDE = { NilClass => nil.methods } NAMES_TO_EXCLUDE.default = [] Ractor.make_shareable(NAMES_TO_EXCLUDE) if defined?(Ractor) # +MethodNameChecker::RB_RESERVED_WORDS+ is the list of reserved words in # Ruby that take an argument. Unlike # +VariableNameChecker::RB_RESERVED_WORDS+, these reserved words require # an argument, and a +NoMethodError+ is raised due to the presence of the # argument. # # The +MethodNameChecker+ will use this list to suggest a reversed word if # a +NoMethodError+ is raised and found closest matches. # # Also see +VariableNameChecker::RB_RESERVED_WORDS+. RB_RESERVED_WORDS = %i( alias case def defined? elsif end ensure for rescue super undef unless until when while yield ) Ractor.make_shareable(RB_RESERVED_WORDS) if defined?(Ractor) def initialize(exception) @method_name = exception.name @receiver = exception.receiver @private_call = exception.respond_to?(:private_call?) ? exception.private_call? : false end def corrections @corrections ||= begin dictionary = method_names dictionary = RB_RESERVED_WORDS + dictionary if @private_call SpellChecker.new(dictionary: dictionary).correct(method_name) - names_to_exclude end end def method_names if Object === receiver method_names = receiver.methods + receiver.singleton_methods method_names += receiver.private_methods if @private_call method_names.uniq! # Assume that people trying to use a writer are not interested in a reader # and vice versa if method_name.match?(/=\Z/) method_names.select! { |name| name.match?(/=\Z/) } else method_names.reject! { |name| name.match?(/=\Z/) } end method_names else [] end end def names_to_exclude Object === receiver ? NAMES_TO_EXCLUDE[receiver.class] : [] end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb0000644000004100000410000000133014735503325027727 0ustar www-datawww-datarequire_relative "../spell_checker" module DidYouMean class PatternKeyNameChecker def initialize(no_matching_pattern_key_error) @key = no_matching_pattern_key_error.key @keys = no_matching_pattern_key_error.matchee.keys end def corrections @corrections ||= exact_matches.empty? ? SpellChecker.new(dictionary: @keys).correct(@key).map(&:inspect) : exact_matches end private def exact_matches @exact_matches ||= @keys.select { |word| @key == word.to_s }.map { |obj| format_object(obj) } end def format_object(symbol_or_object) if symbol_or_object.is_a?(Symbol) ":#{symbol_or_object}" else symbol_or_object.to_s end end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/require_path_checker.rb0000644000004100000410000000235614735503325027103 0ustar www-datawww-data# frozen-string-literal: true require_relative "../spell_checker" require_relative "../tree_spell_checker" require "rbconfig" module DidYouMean class RequirePathChecker attr_reader :path INITIAL_LOAD_PATH = $LOAD_PATH.dup.freeze Ractor.make_shareable(INITIAL_LOAD_PATH) if defined?(Ractor) ENV_SPECIFIC_EXT = ".#{RbConfig::CONFIG["DLEXT"]}" Ractor.make_shareable(ENV_SPECIFIC_EXT) if defined?(Ractor) private_constant :INITIAL_LOAD_PATH, :ENV_SPECIFIC_EXT def self.requireables @requireables ||= INITIAL_LOAD_PATH .flat_map {|path| Dir.glob("**/???*{.rb,#{ENV_SPECIFIC_EXT}}", base: path) } .map {|path| path.chomp!(".rb") || path.chomp!(ENV_SPECIFIC_EXT) } end def initialize(exception) @path = exception.path end def corrections @corrections ||= begin threshold = path.size * 2 dictionary = self.class.requireables.reject {|str| str.size >= threshold } spell_checker = path.include?("/") ? TreeSpellChecker : SpellChecker spell_checker.new(dictionary: dictionary).correct(path).uniq end end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/name_error_checkers/0000755000004100000410000000000014735503325026374 5ustar www-datawww-datadid_you_mean-2.0.0/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb0000644000004100000410000000230014735503325032505 0ustar www-datawww-data# frozen-string-literal: true require_relative "../../spell_checker" module DidYouMean class ClassNameChecker attr_reader :class_name def initialize(exception) @class_name, @receiver, @original_message = exception.name, exception.receiver, exception.original_message end def corrections @corrections ||= SpellChecker.new(dictionary: class_names) .correct(class_name) .map(&:full_name) .reject {|qualified_name| @original_message.include?(qualified_name) } end def class_names scopes.flat_map do |scope| scope.constants.map do |c| ClassName.new(c, scope == Object ? "" : "#{scope}::") end end end def scopes @scopes ||= @receiver.to_s.split("::").inject([Object]) do |_scopes, scope| _scopes << _scopes.last.const_get(scope) end.uniq end class ClassName < String attr :namespace def initialize(name, namespace = '') super(name.to_s) @namespace = namespace end def full_name self.class.new("#{namespace}#{self}") end end private_constant :ClassName end end did_you_mean-2.0.0/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb0000644000004100000410000000411514735503325033173 0ustar www-datawww-data# frozen-string-literal: true require_relative "../../spell_checker" module DidYouMean class VariableNameChecker attr_reader :name, :method_names, :lvar_names, :ivar_names, :cvar_names NAMES_TO_EXCLUDE = { 'foo' => [:fork, :for] } NAMES_TO_EXCLUDE.default = [] Ractor.make_shareable(NAMES_TO_EXCLUDE) if defined?(Ractor) # +VariableNameChecker::RB_RESERVED_WORDS+ is the list of all reserved # words in Ruby. They could be declared like methods are, and a typo would # cause Ruby to raise a +NameError+ because of the way they are declared. # # The +:VariableNameChecker+ will use this list to suggest a reversed word # if a +NameError+ is raised and found closest matches, excluding: # # * +do+ # * +if+ # * +in+ # * +or+ # # Also see +MethodNameChecker::RB_RESERVED_WORDS+. RB_RESERVED_WORDS = %i( BEGIN END alias and begin break case class def defined? else elsif end ensure false for module next nil not redo rescue retry return self super then true undef unless until when while yield __LINE__ __FILE__ __ENCODING__ ) Ractor.make_shareable(RB_RESERVED_WORDS) if defined?(Ractor) def initialize(exception) @name = exception.name.to_s.tr("@", "") @lvar_names = exception.respond_to?(:local_variables) ? exception.local_variables : [] receiver = exception.receiver @method_names = receiver.methods + receiver.private_methods @ivar_names = receiver.instance_variables @cvar_names = receiver.class.class_variables @cvar_names += receiver.class_variables if receiver.kind_of?(Module) end def corrections @corrections ||= SpellChecker .new(dictionary: (RB_RESERVED_WORDS + lvar_names + method_names + ivar_names + cvar_names)) .correct(name).uniq - NAMES_TO_EXCLUDE[@name] end end end did_you_mean-2.0.0/lib/did_you_mean/verbose.rb0000644000004100000410000000021114735503325021372 0ustar www-datawww-datawarn "The verbose formatter has been removed and now `require 'did_you_mean/verbose'` has no effect. Please " \ "remove this call." did_you_mean-2.0.0/lib/did_you_mean/levenshtein.rb0000644000004100000410000000253714735503325022266 0ustar www-datawww-datamodule DidYouMean module Levenshtein # :nodoc: # This code is based directly on the Text gem implementation # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. # # Returns a value representing the "cost" of transforming str1 into str2 def distance(str1, str2) n = str1.length m = str2.length return m if n.zero? return n if m.zero? d = (0..m).to_a x = nil # to avoid duplicating an enumerable object, create it outside of the loop str2_codepoints = str2.codepoints str1.each_codepoint.with_index(1) do |char1, i| j = 0 while j < m cost = (char1 == str2_codepoints[j]) ? 0 : 1 x = min3( d[j+1] + 1, # insertion i + 1, # deletion d[j] + cost # substitution ) d[j] = i i = x j += 1 end d[m] = x end x end module_function :distance private # detects the minimum value out of three arguments. This method is # faster than `[a, b, c].min` and puts less GC pressure. # See https://github.com/ruby/did_you_mean/pull/1 for a performance # benchmark. def min3(a, b, c) if a < b && a < c a elsif b < c b else c end end module_function :min3 end end did_you_mean-2.0.0/lib/did_you_mean/experimental.rb0000644000004100000410000000021314735503325022424 0ustar www-datawww-datawarn "Experimental features in the did_you_mean gem has been removed " \ "and `require \"did_you_mean/experimental\"' has no effect." did_you_mean-2.0.0/lib/did_you_mean/core_ext/0000755000004100000410000000000014735503325021216 5ustar www-datawww-datadid_you_mean-2.0.0/lib/did_you_mean/core_ext/name_error.rb0000644000004100000410000000242014735503325023672 0ustar www-datawww-datamodule DidYouMean module Correctable if Exception.method_defined?(:detailed_message) # just for compatibility def original_message # we cannot use alias here because to_s end def detailed_message(highlight: true, did_you_mean: true, **) msg = super.dup return msg unless did_you_mean suggestion = DidYouMean.formatter.message_for(corrections) if highlight suggestion = suggestion.gsub(/.+/) { "\e[1m" + $& + "\e[m" } end msg << suggestion msg rescue super end else SKIP_TO_S_FOR_SUPER_LOOKUP = true private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP def original_message meth = method(:to_s) while meth.owner.const_defined?(:SKIP_TO_S_FOR_SUPER_LOOKUP) meth = meth.super_method end meth.call end def to_s msg = super.dup suggestion = DidYouMean.formatter.message_for(corrections) msg << suggestion if !msg.include?(suggestion) msg rescue super end end def corrections @corrections ||= spell_checker.corrections end def spell_checker DidYouMean.spell_checkers[self.class.to_s].new(self) end end end did_you_mean-2.0.0/lib/did_you_mean/spell_checker.rb0000644000004100000410000000242114735503325022535 0ustar www-datawww-data# frozen-string-literal: true require_relative "levenshtein" require_relative "jaro_winkler" module DidYouMean class SpellChecker def initialize(dictionary:) @dictionary = dictionary end def correct(input) normalized_input = normalize(input) threshold = normalized_input.length > 3 ? 0.834 : 0.77 words = @dictionary.select { |word| JaroWinkler.distance(normalize(word), normalized_input) >= threshold } words.reject! { |word| input.to_s == word.to_s } words.sort_by! { |word| JaroWinkler.distance(word.to_s, normalized_input) } words.reverse! # Correct mistypes threshold = (normalized_input.length * 0.25).ceil corrections = words.select { |c| Levenshtein.distance(normalize(c), normalized_input) <= threshold } # Correct misspells if corrections.empty? corrections = words.select do |word| word = normalize(word) length = normalized_input.length < word.length ? normalized_input.length : word.length Levenshtein.distance(word, normalized_input) < length end.first(1) end corrections end private def normalize(str_or_symbol) #:nodoc: str = str_or_symbol.to_s.downcase str.tr!("@", "") str end end end did_you_mean-2.0.0/lib/did_you_mean/version.rb0000644000004100000410000000006114735503325021415 0ustar www-datawww-datamodule DidYouMean VERSION = "2.0.0".freeze end did_you_mean-2.0.0/lib/did_you_mean/formatters/0000755000004100000410000000000014735503325021574 5ustar www-datawww-datadid_you_mean-2.0.0/lib/did_you_mean/formatters/verbose_formatter.rb0000644000004100000410000000040214735503325025645 0ustar www-datawww-data# frozen-string-literal: true warn "`require 'did_you_mean/formatters/verbose_formatter'` is deprecated and falls back to the default formatter. " require_relative '../formatter' module DidYouMean # For compatibility: VerboseFormatter = Formatter end did_you_mean-2.0.0/lib/did_you_mean/formatters/plain_formatter.rb0000644000004100000410000000025014735503325025304 0ustar www-datawww-datarequire_relative '../formatter' warn "`require 'did_you_mean/formatters/plain_formatter'` is deprecated. Please `require 'did_you_mean/formatter'` " \ "instead." did_you_mean-2.0.0/lib/did_you_mean/formatter.rb0000644000004100000410000000243014735503325021735 0ustar www-datawww-data# frozen-string-literal: true module DidYouMean # The +DidYouMean::Formatter+ is the basic, default formatter for the # gem. The formatter responds to the +message_for+ method and it returns a # human readable string. class Formatter # Returns a human readable string that contains +corrections+. This # formatter is designed to be less verbose to not take too much screen # space while being helpful enough to the user. # # @example # # formatter = DidYouMean::Formatter.new # # # displays suggestions in two lines with the leading empty line # puts formatter.message_for(["methods", "method"]) # # Did you mean? methods # method # # => nil # # # displays an empty line # puts formatter.message_for([]) # # # => nil # def self.message_for(corrections) corrections.empty? ? "" : "\nDid you mean? #{corrections.join("\n ")}" end def message_for(corrections) warn "The instance method #message_for has been deprecated. Please use the class method " \ "DidYouMean::Formatter.message_for(...) instead." self.class.message_for(corrections) end end PlainFormatter = Formatter deprecate_constant :PlainFormatter end did_you_mean-2.0.0/lib/did_you_mean.rb0000644000004100000410000001100614735503325017731 0ustar www-datawww-datarequire_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 `
': 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) # 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 did_you_mean-2.0.0/LICENSE.txt0000644000004100000410000000206414735503325016031 0ustar www-datawww-dataCopyright (c) 2014-2016 Yuki Nishijima MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. did_you_mean-2.0.0/test/0000755000004100000410000000000014735503325015163 5ustar www-datawww-datadid_you_mean-2.0.0/test/test_ractor_compatibility.rb0000644000004100000410000000720314735503325022774 0ustar www-datawww-datarequire_relative './helper' return if not DidYouMean::TestHelper.ractor_compatible? class RactorCompatibilityTest < Test::Unit::TestCase def test_class_name_suggestion_works_in_ractor assert_ractor(<<~CODE, require_relative: "helper") class ::Book; end include DidYouMean::TestHelper error = Ractor.new { begin Boook rescue NameError => e e.corrections # It is important to call the #corrections method within Ractor. e end }.take assert_correction "Book", error.corrections CODE end def test_key_name_suggestion_works_in_ractor assert_ractor(<<~CODE, require_relative: "helper") include DidYouMean::TestHelper error = Ractor.new { begin hash = { "foo" => 1, bar: 2 } hash.fetch(:bax) rescue KeyError => e e.corrections # It is important to call the #corrections method within Ractor. e end }.take assert_correction ":bar", error.corrections assert_match "Did you mean? :bar", get_message(error) CODE end def test_method_name_suggestion_works_in_ractor assert_ractor(<<~CODE, require_relative: "helper") include DidYouMean::TestHelper error = Ractor.new { begin self.to__s rescue NoMethodError => e e.corrections # It is important to call the #corrections method within Ractor. e end }.take assert_correction :to_s, error.corrections assert_match "Did you mean? to_s", get_message(error) CODE end if defined?(::NoMatchingPatternKeyError) def test_pattern_key_name_suggestion_works_in_ractor assert_ractor(<<~CODE, require_relative: "helper") include DidYouMean::TestHelper error = Ractor.new { begin eval(<<~RUBY, binding, __FILE__, __LINE__) hash = {foo: 1, bar: 2, baz: 3} hash => {fooo:} fooo = 1 # suppress "unused variable: fooo" warning RUBY rescue NoMatchingPatternKeyError => e e.corrections # It is important to call the #corrections method within Ractor. e end }.take assert_correction ":foo", error.corrections assert_match "Did you mean? :foo", get_message(error) CODE end end def test_can_raise_other_name_error_in_ractor assert_ractor(<<~CODE, require_relative: "helper") class FirstNameError < NameError; end include DidYouMean::TestHelper error = Ractor.new { begin raise FirstNameError, "Other name error" rescue FirstNameError => e e.corrections # It is important to call the #corrections method within Ractor. e end }.take assert_not_match(/Did you mean\?/, error.message) CODE end def test_variable_name_suggestion_works_in_ractor assert_ractor(<<~CODE, require_relative: "helper") include DidYouMean::TestHelper error = Ractor.new { in_ractor = in_ractor = 1 begin in_reactor rescue NameError => e e.corrections # It is important to call the #corrections method within Ractor. e end }.take assert_correction :in_ractor, error.corrections assert_match "Did you mean? in_ractor", get_message(error) CODE end end did_you_mean-2.0.0/test/helper.rb0000644000004100000410000000227314735503325016773 0ustar www-datawww-datarequire 'test/unit' module DidYouMean module TestHelper class << self attr_reader :root def ractor_compatible? defined?(Ractor) && RUBY_VERSION >= "3.1.0" end end if File.file?(File.expand_path('../lib/did_you_mean.rb', __dir__)) # In this case we're being run from inside the gem, so we just want to # require the root of the library @root = File.expand_path('../lib/did_you_mean', __dir__) require_relative @root else # In this case we're being run from inside ruby core, and we want to # include the experimental features in the test suite @root = File.expand_path('../../lib/did_you_mean', __dir__) require_relative @root # We are excluding experimental features for now. # require_relative File.join(@root, 'experimental') end def assert_correction(expected, array) assert_equal Array(expected), array, "Expected #{array.inspect} to only include #{expected.inspect}" end def get_message(err) if err.respond_to?(:detailed_message) err.detailed_message(highlight: false) else err.to_s end end module_function :get_message end end did_you_mean-2.0.0/test/lib/0000755000004100000410000000000014735503325015731 5ustar www-datawww-datadid_you_mean-2.0.0/test/lib/helper.rb0000644000004100000410000000014614735503325017536 0ustar www-datawww-datarequire "test/unit" require "core_assertions" Test::Unit::TestCase.include Test::Unit::CoreAssertions did_you_mean-2.0.0/test/test_spell_checker.rb0000644000004100000410000000603714735503325021360 0ustar www-datawww-datarequire_relative './helper' class SpellCheckerTest < Test::Unit::TestCase def test_spell_checker_corrects_mistypes assert_spell 'foo', input: 'doo', dictionary: ['foo', 'fork'] assert_spell 'email', input: 'meail', dictionary: ['email', 'fail', 'eval'] assert_spell 'fail', input: 'fial', dictionary: ['email', 'fail', 'eval'] assert_spell 'fail', input: 'afil', dictionary: ['email', 'fail', 'eval'] assert_spell 'eval', input: 'eavl', dictionary: ['email', 'fail', 'eval'] assert_spell 'eval', input: 'veal', dictionary: ['email', 'fail', 'eval'] assert_spell 'sub!', input: 'suv!', dictionary: ['sub', 'gsub', 'sub!'] assert_spell 'sub', input: 'suv', dictionary: ['sub', 'gsub', 'sub!'] assert_spell 'Foo', input: 'FOo', dictionary: ['Foo', 'FOo'] assert_spell %w(gsub! gsub), input: 'gsuv!', dictionary: %w(sub gsub gsub!) assert_spell %w(sub! sub gsub!), input: 'ssub!', dictionary: %w(sub sub! gsub gsub!) group_methods = %w(groups group_url groups_url group_path) assert_spell 'groups', input: 'group', dictionary: group_methods group_classes = %w( GroupMembership GroupMembershipPolicy GroupMembershipDecorator GroupMembershipSerializer GroupHelper Group GroupMailer NullGroupMembership ) assert_spell 'GroupMembership', dictionary: group_classes, input: 'GroupMemberhip' assert_spell 'GroupMembershipDecorator', dictionary: group_classes, input: 'GroupMemberhipDecorator' names = %w(first_name_change first_name_changed? first_name_will_change!) assert_spell names, input: 'first_name_change!', dictionary: names assert_empty DidYouMean::SpellChecker.new(dictionary: ['proc']).correct('product_path') assert_empty DidYouMean::SpellChecker.new(dictionary: ['fork']).correct('fooo') end def test_spell_checker_corrects_misspells assert_spell 'descendants', input: 'dependents', dictionary: ['descendants'] assert_spell 'drag_to', input: 'drag', dictionary: ['drag_to'] assert_spell 'set_result_count', input: 'set_result', dictionary: ['set_result_count'] end def test_spell_checker_sorts_results_by_simiarity expected = %w( name12345 name1234 name123 ) actual = DidYouMean::SpellChecker.new(dictionary: %w( name12 name123 name1234 name12345 name123456 )).correct('name123456') assert_equal expected, actual end def test_spell_checker_excludes_input_from_dictionary assert_empty DidYouMean::SpellChecker.new(dictionary: ['input']).correct('input') assert_empty DidYouMean::SpellChecker.new(dictionary: [:input]).correct('input') assert_empty DidYouMean::SpellChecker.new(dictionary: ['input']).correct(:input) end private def assert_spell(expected, input: , dictionary: ) corrections = DidYouMean::SpellChecker.new(dictionary: dictionary).correct(input) assert_equal Array(expected), corrections, "Expected to suggest #{expected}, but got #{corrections.inspect}" end end did_you_mean-2.0.0/test/fixtures/0000755000004100000410000000000014735503325017034 5ustar www-datawww-datadid_you_mean-2.0.0/test/fixtures/mini_dir.yml0000644000004100000410000000104514735503325021351 0ustar www-datawww-data--- - test/core_ext/name_error_extension_test.rb - test/edit_distance/jaro_winkler_test.rb - test/fixtures/book.rb - test/spell_checker_test.rb - test/spell_checking/class_name_check_test.rb - test/spell_checking/key_name_check_test.rb - test/spell_checking/method_name_check_test.rb - test/spell_checking/uncorrectable_name_check_test.rb - test/spell_checking/variable_name_check_test.rb - test/test_helper.rb - test/tree_spell_checker_test.rb - test/tree_spell_explore_test.rb - test/tree_spell_human_typo_test.rb - test/verbose_formatter_test.rb did_you_mean-2.0.0/test/fixtures/book.rb0000644000004100000410000000004314735503325020310 0ustar www-datawww-dataclass Book class Spine end end did_you_mean-2.0.0/test/fixtures/rspec_dir.yml0000644000004100000410000001112014735503325021524 0ustar www-datawww-data--- - spec/spec_helper.rb - spec/integration/suite_hooks_errors_spec.rb - spec/integration/filtering_spec.rb - spec/integration/spec_file_load_errors_spec.rb - spec/integration/failed_line_detection_spec.rb - spec/integration/persistence_failures_spec.rb - spec/integration/bisect_runners_spec.rb - spec/integration/order_spec.rb - spec/integration/fail_if_no_examples_spec.rb - spec/integration/bisect_spec.rb - spec/integration/output_stream_spec.rb - spec/support/sandboxing.rb - spec/support/spec_files.rb - spec/support/fake_libs/json.rb - spec/support/fake_libs/open3.rb - spec/support/fake_libs/drb/acl.rb - spec/support/fake_libs/drb/drb.rb - spec/support/fake_libs/mocha/api.rb - spec/support/fake_libs/test/unit/assertions.rb - spec/support/fake_libs/flexmock/rspec.rb - spec/support/fake_libs/rake/tasklib.rb - spec/support/fake_libs/coderay.rb - spec/support/fake_libs/rr.rb - spec/support/fake_libs/rake.rb - spec/support/fake_libs/erb.rb - spec/support/fake_libs/rspec/mocks.rb - spec/support/fake_libs/rspec/expectations.rb - spec/support/fake_libs/minitest/assertions.rb - spec/support/fake_libs/minitest.rb - spec/support/matchers.rb - spec/support/runner_support.rb - spec/support/isolated_home_directory.rb - spec/support/config_options_helper.rb - spec/support/mathn_integration_support.rb - spec/support/helper_methods.rb - spec/support/formatter_support.rb - spec/support/fake_bisect_runner.rb - spec/support/shared_example_groups.rb - spec/support/aruba_support.rb - spec/rspec/core/runner_spec.rb - spec/rspec/core/did_you_mean_spec.rb - spec/rspec/core/drb_spec.rb - spec/rspec/core/metadata_spec.rb - spec/rspec/core/example_group_spec.rb - spec/rspec/core/configuration/only_failures_support_spec.rb - spec/rspec/core/rake_task_spec.rb - spec/rspec/core/memoized_helpers_spec.rb - spec/rspec/core/ordering_spec.rb - spec/rspec/core/option_parser_spec.rb - spec/rspec/core/example_execution_result_spec.rb - spec/rspec/core/suite_hooks_spec.rb - spec/rspec/core/set_spec.rb - spec/rspec/core/configuration_spec.rb - spec/rspec/core/rspec_matchers_spec.rb - spec/rspec/core/hooks_filtering_spec.rb - spec/rspec/core/bisect/shell_command_spec.rb - spec/rspec/core/bisect/server_spec.rb - spec/rspec/core/bisect/example_minimizer_spec.rb - spec/rspec/core/bisect/shell_runner_spec.rb - spec/rspec/core/bisect/utilities_spec.rb - spec/rspec/core/bisect/coordinator_spec.rb - spec/rspec/core/resources/a_foo.rb - spec/rspec/core/resources/formatter_specs.rb - spec/rspec/core/resources/inconsistently_ordered_specs.rb - spec/rspec/core/resources/a_bar.rb - spec/rspec/core/resources/utf8_encoded.rb - spec/rspec/core/resources/a_spec.rb - spec/rspec/core/resources/acceptance/bar.rb - spec/rspec/core/resources/acceptance/foo_spec.rb - spec/rspec/core/resources/custom_example_group_runner.rb - spec/rspec/core/failed_example_notification_spec.rb - spec/rspec/core/hooks_spec.rb - spec/rspec/core/formatters/profile_formatter_spec.rb - spec/rspec/core/formatters/deprecation_formatter_spec.rb - spec/rspec/core/formatters/syntax_highlighter_spec.rb - spec/rspec/core/formatters/base_text_formatter_spec.rb - spec/rspec/core/formatters/snippet_extractor_spec.rb - spec/rspec/core/formatters/progress_formatter_spec.rb - spec/rspec/core/formatters/html_snippet_extractor_spec.rb - spec/rspec/core/formatters/helpers_spec.rb - spec/rspec/core/formatters/html_formatter_spec.rb - spec/rspec/core/formatters/json_formatter_spec.rb - spec/rspec/core/formatters/documentation_formatter_spec.rb - spec/rspec/core/formatters/exception_presenter_spec.rb - spec/rspec/core/formatters/console_codes_spec.rb - spec/rspec/core/formatters/fallback_message_formatter_spec.rb - spec/rspec/core/invocations_spec.rb - spec/rspec/core/configuration_options_spec.rb - spec/rspec/core/pending_spec.rb - spec/rspec/core/profiler_spec.rb - spec/rspec/core/project_initializer_spec.rb - spec/rspec/core/aggregate_failures_spec.rb - spec/rspec/core/dsl_spec.rb - spec/rspec/core/ruby_project_spec.rb - spec/rspec/core/formatters_spec.rb - spec/rspec/core/metadata_filter_spec.rb - spec/rspec/core/example_group_constants_spec.rb - spec/rspec/core/world_spec.rb - spec/rspec/core/shared_context_spec.rb - spec/rspec/core/pending_example_spec.rb - spec/rspec/core/filter_manager_spec.rb - spec/rspec/core/shared_example_group_spec.rb - spec/rspec/core/example_status_persister_spec.rb - spec/rspec/core/backtrace_formatter_spec.rb - spec/rspec/core/output_wrapper_spec.rb - spec/rspec/core/example_spec.rb - spec/rspec/core/reporter_spec.rb - spec/rspec/core/filterable_item_repository_spec.rb - spec/rspec/core/notifications_spec.rb - spec/rspec/core/warnings_spec.rb - spec/rspec/core_spec.rb did_you_mean-2.0.0/test/spell_checking/0000755000004100000410000000000014735503325020135 5ustar www-datawww-datadid_you_mean-2.0.0/test/spell_checking/test_variable_name_check.rb0000644000004100000410000000761614735503325025455 0ustar www-datawww-datarequire_relative '../helper' class VariableNameCheckTest < Test::Unit::TestCase include DidYouMean::TestHelper class User def initialize @email_address = 'email_address@address.net' @first_name = nil @last_name = nil end def first_name; end def to_s "#{@first_name} #{@last_name} <#{email_address}>" end private def cia_codename; "Alexa" end end module UserModule def from_module; end end def setup @user = User.new.extend(UserModule) end def test_corrections_include_instance_method error = assert_raise(NameError) do @user.instance_eval { flrst_name } end @user.instance_eval do remove_instance_variable :@first_name remove_instance_variable :@last_name end assert_correction :first_name, error.corrections assert_match "Did you mean? first_name", get_message(error) end def test_corrections_include_method_from_module error = assert_raise(NameError) do @user.instance_eval { fr0m_module } end assert_correction :from_module, error.corrections assert_match "Did you mean? from_module", get_message(error) end def test_corrections_include_local_variable_name if RUBY_ENGINE != "jruby" person = person = nil error = (eprson rescue $!) # Do not use @assert_raise here as it changes a scope. assert_correction :person, error.corrections assert_match "Did you mean? person", get_message(error) end end def test_corrections_include_ruby_predefined_objects some_var = some_var = nil false_error = assert_raise(NameError) do some_var = fals end true_error = assert_raise(NameError) do some_var = treu end nil_error = assert_raise(NameError) do some_var = nul end file_error = assert_raise(NameError) do __FIEL__ end assert_correction :false, false_error.corrections assert_match "Did you mean? false", get_message(false_error) assert_correction :true, true_error.corrections assert_match "Did you mean? true", get_message(true_error) assert_correction :nil, nil_error.corrections assert_match "Did you mean? nil", get_message(nil_error) assert_correction :__FILE__, file_error.corrections assert_match "Did you mean? __FILE__", get_message(file_error) end def test_suggests_yield error = assert_raise(NameError) { yeild } assert_correction :yield, error.corrections assert_match "Did you mean? yield", get_message(error) end def test_corrections_include_instance_variable_name error = assert_raise(NameError){ @user.to_s } assert_correction :@email_address, error.corrections assert_match "Did you mean? @email_address", get_message(error) end def test_corrections_include_private_method error = assert_raise(NameError) do @user.instance_eval { cia_code_name } end assert_correction :cia_codename, error.corrections assert_match "Did you mean? cia_codename", get_message(error) end @@does_exist = true def test_corrections_include_class_variable_name error = assert_raise(NameError){ @@doesnt_exist } assert_correction :@@does_exist, error.corrections assert_match "Did you mean? @@does_exist", get_message(error) end def test_struct_name_error value = Struct.new(:does_exist).new error = assert_raise(NameError){ value[:doesnt_exist] } assert_correction [:does_exist, :does_exist=], error.corrections assert_match "Did you mean? does_exist", get_message(error) end def test_exclude_typical_incorrect_suggestions error = assert_raise(NameError){ foo } assert_empty error.corrections end def test_exclude_duplicates_with_same_name error = assert_raise(NameError) do eval(<<~RUBY, binding, __FILE__, __LINE__) bar = 1 def bar;end zar RUBY end assert_correction [:bar], error.corrections end end did_you_mean-2.0.0/test/spell_checking/test_uncorrectable_name_check.rb0000644000004100000410000000051214735503325026504 0ustar www-datawww-datarequire_relative '../helper' class UncorrectableNameCheckTest < Test::Unit::TestCase class FirstNameError < NameError; end def setup @error = assert_raise(FirstNameError) do raise FirstNameError, "Other name error" end end def test_message assert_not_match(/Did you mean\?/, @error.message) end end did_you_mean-2.0.0/test/spell_checking/test_key_name_check.rb0000644000004100000410000000340414735503325024447 0ustar www-datawww-datarequire_relative '../helper' class KeyNameCheckTest < Test::Unit::TestCase include DidYouMean::TestHelper def test_corrects_hash_key_name_with_fetch hash = { "foo" => 1, bar: 2 } error = assert_raise(KeyError) { hash.fetch(:bax) } assert_correction ":bar", error.corrections assert_match "Did you mean? :bar", get_message(error) error = assert_raise(KeyError) { hash.fetch("fooo") } assert_correction %("foo"), error.corrections assert_match %(Did you mean? "foo"), get_message(error) end def test_corrects_hash_key_name_with_fetch_values hash = { "foo" => 1, bar: 2 } error = assert_raise(KeyError) { hash.fetch_values("foo", :bar, :bax) } assert_correction ":bar", error.corrections assert_match "Did you mean? :bar", get_message(error) error = assert_raise(KeyError) { hash.fetch_values("foo", :bar, "fooo") } assert_correction %("foo"), error.corrections assert_match %(Did you mean? "foo"), get_message(error) end def test_correct_symbolized_hash_keys_with_string_value hash = { foo_1: 1, bar_2: 2 } error = assert_raise(KeyError) { hash.fetch('foo_1') } assert_correction %(:foo_1), error.corrections assert_match %(Did you mean? :foo_1), get_message(error) end def test_corrects_sprintf_key_name error = assert_raise(KeyError) { sprintf("%d", {fooo: 1}) } assert_correction ":fooo", error.corrections assert_match "Did you mean? :fooo", get_message(error) end def test_corrects_env_key_name ENV["FOO"] = "1" ENV["BAR"] = "2" error = assert_raise(KeyError) { ENV.fetch("BAX") } assert_correction %("BAR"), error.corrections assert_match %(Did you mean? "BAR"), get_message(error) ensure ENV.delete("FOO") ENV.delete("BAR") end end did_you_mean-2.0.0/test/spell_checking/test_require_path_check.rb0000644000004100000410000000162114735503325025346 0ustar www-datawww-datarequire_relative '../helper' return if !(RUBY_VERSION >= '2.8.0') class RequirePathCheckTest < Test::Unit::TestCase include DidYouMean::TestHelper def test_load_error_from_require_has_suggestions error = assert_raise LoadError do require 'open_struct' end assert_correction 'ostruct', error.corrections assert_match "Did you mean? ostruct", get_message(error) end def test_load_error_from_require_for_nested_files_has_suggestions error = assert_raise LoadError do require 'net/htt' end assert_correction 'net/http', error.corrections assert_match "Did you mean? net/http", get_message(error) error = assert_raise LoadError do require 'net-http' end assert_correction ['net/http', 'net/https'], error.corrections assert_match "Did you mean? net/http", get_message(error) end end did_you_mean-2.0.0/test/spell_checking/test_method_name_check.rb0000644000004100000410000001041614735503325025140 0ustar www-datawww-datarequire_relative '../helper' class MethodNameCheckTest < Test::Unit::TestCase include DidYouMean::TestHelper class User attr_writer :writer attr_reader :reader def friends; end def first_name; end def descendants; end def call_incorrect_private_method raiae NoMethodError end def raise_no_method_error self.firstname rescue NoMethodError => e raise e, e.message, e.backtrace end protected def the_protected_method; end private def friend; end def the_private_method; end class << self def load; end end end module UserModule def from_module; end end def setup @user = User.new.extend(UserModule) end def test_corrections_include_instance_method error = assert_raise(NoMethodError){ @user.flrst_name } assert_correction :first_name, error.corrections assert_match "Did you mean? first_name", get_message(error) end def test_corrections_include_private_method error = assert_raise(NoMethodError){ @user.friend } assert_correction :friends, error.corrections assert_match "Did you mean? friends", get_message(error) end def test_corrections_include_method_from_module error = assert_raise(NoMethodError){ @user.fr0m_module } assert_correction :from_module, error.corrections assert_match "Did you mean? from_module", get_message(error) end def test_corrections_include_class_method error = assert_raise(NoMethodError){ User.l0ad } assert_correction :load, error.corrections assert_match "Did you mean? load", get_message(error) end def test_private_methods_should_not_be_suggested error = assert_raise(NoMethodError){ User.new.the_protected_method } refute_includes error.corrections, :the_protected_method error = assert_raise(NoMethodError){ User.new.the_private_method } refute_includes error.corrections, :the_private_method end def test_corrections_when_private_method_is_called_with_args error = assert_raise(NoMethodError){ @user.call_incorrect_private_method } assert_correction :raise, error.corrections assert_match "Did you mean? raise", get_message(error) end def test_exclude_methods_on_nil error = assert_raise(NoMethodError){ nil.map } assert_empty error.corrections end def test_does_not_exclude_custom_methods_on_nil def nil.empty? end error = assert_raise(NoMethodError){ nil.empty } assert_correction :empty?, error.corrections ensure NilClass.class_eval { undef empty? } end def test_does_not_append_suggestions_twice error = assert_raise NoMethodError do begin @user.firstname rescue NoMethodError => e raise e, e.message, e.backtrace end end assert_equal 1, get_message(error).scan(/Did you mean/).count end def test_does_not_append_suggestions_three_times error = assert_raise NoMethodError do begin @user.raise_no_method_error rescue NoMethodError => e raise e, e.message, e.backtrace end end assert_equal 1, get_message(error).scan(/Did you mean/).count end def test_suggests_corrections_on_nested_error error = assert_raise NoMethodError do begin @user.firstname rescue NoMethodError @user.firstname end end assert_equal 1, get_message(error).scan(/Did you mean/).count end def test_suggests_yield error = assert_raise(NoMethodError) { yeild(1) } assert_correction :yield, error.corrections assert_match "Did you mean? yield", get_message(error) end def test_does_not_suggest_yield error = assert_raise(NoMethodError) { 1.yeild } assert_correction [], error.corrections assert_not_match(/Did you mean\? +yield/, get_message(error)) end if RUBY_ENGINE != "jruby" # Do not suggest `name=` for `name` def test_does_not_suggest_writer error = assert_raise(NoMethodError) { @user.writer } assert_correction [], error.corrections assert_not_match(/Did you mean\? writer=/, get_message(error)) end # Do not suggest `name` for `name=` def test_does_not_suggest_reader error = assert_raise(NoMethodError) { @user.reader = 1 } assert_correction [], error.corrections assert_not_match(/Did you mean\? reader/, get_message(error)) end end did_you_mean-2.0.0/test/spell_checking/test_class_name_check.rb0000644000004100000410000000424114735503325024764 0ustar www-datawww-datarequire_relative '../helper' module ACRONYM end class Project def self.bo0k Bo0k end end class Book class TableOfContents; end def tableof_contents TableofContents end class Page def tableof_contents TableofContents end def self.tableof_contents TableofContents end end end class ClassNameCheckTest < Test::Unit::TestCase include DidYouMean::TestHelper def test_corrections error = assert_raise(NameError) { ::Bo0k } assert_correction "Book", error.corrections end def test_corrections_include_case_specific_class_name error = assert_raise(NameError) { ::Acronym } assert_correction "ACRONYM", error.corrections end def test_corrections_include_top_level_class_name error = assert_raise(NameError) { Project.bo0k } assert_correction "Book", error.corrections end def test_names_in_corrections_have_namespaces error = assert_raise(NameError) { ::Book::TableofContents } assert_correction "Book::TableOfContents", error.corrections end def test_corrections_candidates_for_names_in_upper_level_scopes error = assert_raise(NameError) { Book::Page.tableof_contents } assert_correction "Book::TableOfContents", error.corrections end def test_corrections_should_work_from_within_instance_method error = assert_raise(NameError) { ::Book.new.tableof_contents } assert_correction "Book::TableOfContents", error.corrections end def test_corrections_should_work_from_within_instance_method_on_nested_class error = assert_raise(NameError) { ::Book::Page.new.tableof_contents } assert_correction "Book::TableOfContents", error.corrections end def test_does_not_suggest_user_input Book.send(:remove_const, :Spine) if Book.constants.include?(:Spine) error = assert_raise(NameError) { ::Book::Spine } # This is a weird require, but in a multi-threaded condition, a constant may # be loaded between when a NameError occurred and when the spell checker # attempts to find a possible suggestion. The manual require here simulates # a race condition a single test. require_relative '../fixtures/book' assert_empty error.corrections end end did_you_mean-2.0.0/test/spell_checking/test_pattern_key_name_check.rb0000644000004100000410000000112014735503325026175 0ustar www-datawww-datarequire_relative '../helper' return if !defined?(::NoMatchingPatternKeyError) class PatternKeyNameCheckTest < Test::Unit::TestCase include DidYouMean::TestHelper def test_corrects_hash_key_name_with_single_pattern_match error = assert_raise(NoMatchingPatternKeyError) do eval(<<~RUBY, binding, __FILE__, __LINE__) hash = {foo: 1, bar: 2, baz: 3} hash => {fooo:} fooo = 1 # suppress "unused variable: fooo" warning RUBY end assert_correction ":foo", error.corrections assert_match "Did you mean? :foo", get_message(error) end end did_you_mean-2.0.0/test/core_ext/0000755000004100000410000000000014735503325016773 5ustar www-datawww-datadid_you_mean-2.0.0/test/core_ext/test_name_error_extension.rb0000644000004100000410000000271414735503325024610 0ustar www-datawww-datarequire_relative '../helper' class NameErrorExtensionTest < Test::Unit::TestCase include DidYouMean::TestHelper SPELL_CHECKERS = DidYouMean.spell_checkers class TestSpellChecker def initialize(*); end def corrections; ["does_exist"]; end end def setup @original_spell_checker = DidYouMean.spell_checkers['NameError'] DidYouMean.correct_error(NameError, TestSpellChecker) @error = assert_raise(NameError){ doesnt_exist } end def teardown DidYouMean.correct_error(NameError, @original_spell_checker) end def test_message if Exception.method_defined?(:detailed_message) assert_match(/Did you mean\? does_exist/, @error.detailed_message) else assert_match(/Did you mean\? does_exist/, @error.to_s) assert_match(/Did you mean\? does_exist/, @error.message) end end def test_to_s_does_not_make_disruptive_changes_to_error_message error = assert_raise(NameError) do raise NameError, "uninitialized constant Object" end get_message(error) assert_equal 1, get_message(error).scan("Did you mean?").count end def test_correctable_error_objects_are_dumpable error = begin Dir.chdir(__dir__) { File.open('test_name_error_extension.rb') { |f| f.sizee } } rescue NoMethodError => e e end get_message(error) assert_match(/^undefined method [`']sizee' for /, Marshal.load(Marshal.dump(error)).original_message) end end did_you_mean-2.0.0/test/edit_distance/0000755000004100000410000000000014735503325017762 5ustar www-datawww-datadid_you_mean-2.0.0/test/edit_distance/test_jaro_winkler.rb0000644000004100000410000000261114735503325024034 0ustar www-datawww-datarequire_relative '../helper' # These tests were originally written by Jian Weihang (簡煒航) as part of his work # on the jaro_winkler gem. The original code could be found here: # https://github.com/tonytonyjan/jaro_winkler/blob/9bd12421/spec/jaro_winkler_spec.rb # # Copyright (c) 2014 Jian Weihang class JaroWinklerTest < Test::Unit::TestCase def test_jaro_winkler_distance assert_distance 0.9667, 'henka', 'henkan' assert_distance 1.0, 'al', 'al' assert_distance 0.9611, 'martha', 'marhta' assert_distance 0.8324, 'jones', 'johnson' assert_distance 0.9167, 'abcvwxyz', 'zabcvwxy' assert_distance 0.9583, 'abcvwxyz', 'cabvwxyz' assert_distance 0.84, 'dwayne', 'duane' assert_distance 0.8133, 'dixon', 'dicksonx' assert_distance 0.0, 'fvie', 'ten' assert_distance 0.9067, 'does_exist', 'doesnt_exist' assert_distance 1.0, 'x', 'x' end def test_jarowinkler_distance_with_utf8_strings assert_distance 0.9818, '變形金剛4:絕跡重生', '變形金剛4: 絕跡重生' assert_distance 0.8222, '連勝文', '連勝丼' assert_distance 0.8222, '馬英九', '馬英丸' assert_distance 0.6667, '良い', 'いい' end private def assert_distance(score, str1, str2) assert_equal score, DidYouMean::JaroWinkler.distance(str1, str2).round(4) end end did_you_mean-2.0.0/test/test_tree_spell_checker.rb0000644000004100000410000001537114735503325022400 0ustar www-datawww-data# frozen_string_literal: true require "yaml" require_relative "./helper" class TreeSpellCheckerTest < Test::Unit::TestCase MINI_DIRECTORIES = YAML.load_file(File.expand_path("fixtures/mini_dir.yml", __dir__)) RSPEC_DIRECTORIES = YAML.load_file(File.expand_path("fixtures/rspec_dir.yml", __dir__)) def setup @dictionary = %w( spec/models/concerns/vixen_spec.rb spec/models/concerns/abcd_spec.rb spec/models/concerns/vixenus_spec.rb spec/models/concerns/efgh_spec.rb spec/modals/confirms/abcd_spec.rb spec/modals/confirms/efgh_spec.rb spec/models/gafafa_spec.rb spec/models/gfsga_spec.rb spec/controllers/vixen_controller_spec.rb ) @test_str = "spek/modeks/confirns/viken_spec.rb" @tree_spell_checker = DidYouMean::TreeSpellChecker.new(dictionary: @dictionary) end def test_corrupt_root assert_tree_spell "test/verbose_formatter_test.rb", input: "btets/cverbose_formatter_etst.rb suggestions", dictionary: MINI_DIRECTORIES end def test_leafless_state assert_tree_spell "spec/modals/confirms/efgh_spec.rb", input: "spec/modals/confirXX/efgh_spec.rb", dictionary: [*@dictionary, "spec/features"] assert_tree_spell "spec/features", input: "spec/featuresXX", dictionary: [*@dictionary, "spec/features"] end def test_rake_dictionary assert_tree_spell "parallel:prepare", input: "parallel:preprare", dictionary: %w[parallel:prepare parallel:create parallel:rake parallel:migrate], separator: ":" end def test_special_words_mini [ %w(test/fixtures/book.rb test/fixture/book.rb), %w(test/edit_distance/jaro_winkler_test.rb test/edit_distace/jaro_winkler_test.rb), %w(test/edit_distance/jaro_winkler_test.rb teste/dit_distane/jaro_winkler_test.rb), %w(test/fixtures/book.rb test/fixturWes/book.rb), %w(test/test_helper.rb tes!t/test_helper.rb), %w(test/fixtures/book.rb test/hfixtures/book.rb), %w(test/edit_distance/jaro_winkler_test.rb test/eidt_distance/jaro_winkler_test.@rb), %w(test/spell_checker_test.rb test/spell_checke@r_test.rb), %w(test/tree_spell_human_typo_test.rb testt/ree_spell_human_typo_test.rb), %w(test/edit_distance/jaro_winkler_test.rb test/edit_distance/jaro_winkler_tuest.rb), ].each do |expected, user_input| assert_tree_spell expected, input: user_input, dictionary: MINI_DIRECTORIES end [ %w(test/spell_checking/variable_name_check_test.rb test/spell_checking/vriabl_ename_check_test.rb), %w(test/spell_checking/key_name_check_test.rb tesit/spell_checking/key_name_choeck_test.rb), ].each do |expected, user_input| assert_equal expected, DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES).correct(user_input)[0] end end def test_special_words_rspec [ %w(spec/rspec/core/formatters/exception_presenter_spec.rb spec/rspec/core/formatters/eception_presenter_spec.rb), %w(spec/rspec/core/metadata_spec.rb spec/rspec/core/metadata_spe.crb), %w(spec/rspec/core/ordering_spec.rb spec/spec/core/odrering_spec.rb), %w(spec/support/mathn_integration_support.rb spec/support/mathn_itegrtion_support.rb), ].each do |expected, user_input| assert_tree_spell expected, input: user_input, dictionary: RSPEC_DIRECTORIES end end def test_file_in_root assert_tree_spell "test/spell_checker_test.rb", input: "test/spell_checker_test.r", dictionary: MINI_DIRECTORIES end def test_no_plausible_states assert_tree_spell [], input: "testspell_checker_test.rb", dictionary: MINI_DIRECTORIES end def test_no_plausible_states_with_augmentation assert_tree_spell [], input: "testspell_checker_test.rb", dictionary: MINI_DIRECTORIES suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES, augment: true).correct("testspell_checker_test.rb") assert_equal suggestions.first, "test/spell_checker_test.rb" end def test_no_idea_with_augmentation assert_tree_spell [], input: "test/spell_checking/key_name.rb", dictionary: MINI_DIRECTORIES suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES, augment: true).correct("test/spell_checking/key_name.rb") assert_equal suggestions.first, "test/spell_checking/key_name_check_test.rb" end def test_works_out_suggestions assert_tree_spell %w(spec/models/concerns/vixen_spec.rb spec/models/concerns/vixenus_spec.rb), input: "spek/modeks/confirns/viken_spec.rb", dictionary: %w(spec/models/concerns/vixen_spec.rb spec/models/concerns/vixenus_spec.rb) end def test_works_when_input_is_correct assert_tree_spell "spec/models/concerns/vixenus_spec.rb", input: "spec/models/concerns/vixenus_spec.rb", dictionary: @dictionary end def test_find_out_leaves_in_a_path names = @tree_spell_checker.find_leaves("spec/modals/confirms") assert_equal %w[abcd_spec.rb efgh_spec.rb], names end def test_works_out_nodes exp_paths = ["spec/models/concerns", "spec/models/confirms", "spec/modals/concerns", "spec/modals/confirms", "spec/controllers/concerns", "spec/controllers/confirms"] states = @tree_spell_checker.dimensions nodes = states[0].product(*states[1..-1]) paths = @tree_spell_checker.possible_paths(nodes) assert_equal paths, exp_paths end def test_works_out_state_space suggestions = @tree_spell_checker.plausible_dimensions(@test_str) assert_equal [["spec"], %w[models modals], %w[confirms concerns]], suggestions end def test_parses_dictionary states = @tree_spell_checker.dimensions assert_equal [["spec"], %w[models modals controllers], %w[concerns confirms]], states end def test_parses_elementary_dictionary dimensions = DidYouMean::TreeSpellChecker .new(dictionary: %w(spec/models/user_spec.rb spec/services/account_spec.rb)) .dimensions assert_equal [["spec"], %w[models services]], dimensions end private def assert_tree_spell(expected, input:, dictionary:, separator: "/") suggestions = DidYouMean::TreeSpellChecker.new(dictionary: dictionary, separator: separator).correct(input) assert_equal Array(expected), suggestions, "Expected to suggest #{expected}, but got #{suggestions.inspect}" end end did_you_mean-2.0.0/test/tree_spell/0000755000004100000410000000000014735503325017321 5ustar www-datawww-datadid_you_mean-2.0.0/test/tree_spell/change_word.rb0000644000004100000410000000277414735503325022140 0ustar www-datawww-datamodule TreeSpell # Changes a word with one of four actions: # insertion, substitution, deletion and transposition. class ChangeWord # initialize with input string def initialize(input) @input = input @len = input.length end # insert char after index of i_place def insertion(i_place, char) @word = input.dup return char + word if i_place == 0 return word + char if i_place == len - 1 word.insert(i_place + 1, char) end # substitute char at index of i_place def substitution(i_place, char) @word = input.dup word[i_place] = char word end # delete character at index of i_place def deletion(i_place) @word = input.dup word.slice!(i_place) word end # transpose char at i_place with char at i_place + direction # if i_place + direction is out of bounds just swap in other direction def transposition(i_place, direction) @word = input.dup w = word.dup return swap_first_two(w) if i_place + direction < 0 return swap_last_two(w) if i_place + direction >= len swap_two(w, i_place, direction) w end private attr_accessor :word, :input, :len def swap_first_two(w) w[1] + w[0] + word[2..-1] end def swap_last_two(w) w[0...(len - 2)] + word[len - 1] + word[len - 2] end def swap_two(w, i_place, direction) w[i_place] = word[i_place + direction] w[i_place + direction] = word[i_place] end end end did_you_mean-2.0.0/test/tree_spell/human_typo.rb0000644000004100000410000000322714735503325022035 0ustar www-datawww-data# module for classes needed to test TreeSpellChecker module TreeSpell require_relative 'change_word' # Simulate an error prone human typist # see doc/human_typo_api.md for the api description class HumanTypo POPULAR_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?<>,.!`+=-_":;@#$%^&*()'.split("").freeze ACTION_TYPES = %i(insert transpose delete substitute).freeze def initialize(input, lambda: 0.05) @input = input check_input @len = input.length @lambda = lambda end def call @word = input.dup i_place = initialize_i_place loop do action = ACTION_TYPES.sample @word = make_change action, i_place @len = word.length i_place += exponential break if i_place >= len end word end private attr_accessor :input, :word, :len, :lambda def initialize_i_place i_place = nil loop do i_place = exponential break if i_place < len end i_place end def exponential (rand / (lambda / 2)).to_i end def make_change(action, i_place) cw = ChangeWord.new(word) case action when :delete cw.deletion(i_place) when :insert cw.insertion(i_place, POPULAR_CHARS.sample) when :substitute cw.substitution(i_place, POPULAR_CHARS.sample) when :transpose cw.transposition(i_place, rand >= 0.5 ? +1 : -1) end end def check_input fail check_input_message if input.nil? || input.length < 5 end def check_input_message "input length must be greater than 5 characters: #{input}" end end end did_you_mean-2.0.0/test/tree_spell/test_change_word.rb0000644000004100000410000000267114735503325023173 0ustar www-datawww-datarequire_relative '../helper' require_relative 'change_word' class ChangeWordTest < Test::Unit::TestCase def setup @input = 'spec/services/anything_spec' @cw = TreeSpell::ChangeWord.new(@input) @len = @input.length end def test_deletion assert_match @cw.deletion(5), 'spec/ervices/anything_spec' assert_match @cw.deletion(@len - 1), 'spec/services/anything_spe' assert_match @cw.deletion(0), 'pec/services/anything_spec' end def test_substitution assert_match @cw.substitution(5, '$'), 'spec/$ervices/anything_spec' assert_match @cw.substitution(@len - 1, '$'), 'spec/services/anything_spe$' assert_match @cw.substitution(0, '$'), '$pec/services/anything_spec' end def test_insertion assert_match @cw.insertion(7, 'X'), 'spec/serXvices/anything_spec' assert_match @cw.insertion(0, 'X'), 'Xspec/services/anything_spec' assert_match @cw.insertion(@len - 1, 'X'), 'spec/services/anything_specX' end def test_transposition n = @input.length assert_match @cw.transposition(0, -1), 'psec/services/anything_spec' assert_match @cw.transposition(n - 1, +1), 'spec/services/anything_spce' assert_match @cw.transposition(4, +1), 'specs/ervices/anything_spec' assert_match @cw.transposition(4, -1), 'spe/cservices/anything_spec' assert_match @cw.transposition(21, -1), 'spec/services/anythign_spec' assert_match @cw.transposition(21, +1), 'spec/services/anythin_gspec' end end did_you_mean-2.0.0/test/tree_spell/test_human_typo.rb0000644000004100000410000000122214735503325023065 0ustar www-datawww-datarequire_relative '../helper' require_relative 'human_typo' class HumanTypoTest < Test::Unit::TestCase def setup @input = 'spec/services/anything_spec' @sh = TreeSpell::HumanTypo.new(@input, lambda: 0.05) @len = @input.length end def test_changes # srand seed ensures all four actions are called srand 247_696_449 sh = TreeSpell::HumanTypo.new(@input, lambda: 0.20) word_error = sh.call assert_equal word_error, 'spec/suervcieq/anythin_gpec' end def test_check_input assert_raise(RuntimeError, "input length must be greater than 5 characters: tiny") do TreeSpell::HumanTypo.new('tiny') end end end did_you_mean-2.0.0/test/tree_spell/test_explore.rb0000644000004100000410000001027114735503325022364 0ustar www-datawww-datarequire 'set' require 'yaml' require_relative '../helper' require_relative 'human_typo' # statistical tests on tree_spell algorithms class ExploreTest < Test::Unit::TestCase MINI_DIRECTORIES = YAML.load_file(File.expand_path('../fixtures/mini_dir.yml', __dir__)) RSPEC_DIRECTORIES = YAML.load_file(File.expand_path('../fixtures/rspec_dir.yml', __dir__)) def test_checkers_with_many_typos_on_mini n_repeat = 10_000 many_typos n_repeat, MINI_DIRECTORIES, 'Minitest' end def test_checkers_with_many_typos_on_rspec n_repeat = 10_000 many_typos n_repeat, RSPEC_DIRECTORIES, 'Rspec' end def test_human_typo n_repeat = 10_000 total_changes = 0 word = 'any_string_that_is_40_characters_long_sp' n_repeat.times do word_error = TreeSpell::HumanTypo.new(word).call total_changes += DidYouMean::Levenshtein.distance(word, word_error) end mean_changes = (total_changes.to_f / n_repeat).round(2) puts '' puts "HumanTypo mean_changes: #{mean_changes} with n_repeat: #{n_repeat}" puts 'Expected mean_changes: 2.1 with n_repeat: 10000, plus/minus 0.03' puts '' end def test_execution_speed n_repeat = 1_000 puts '' puts 'Testing execution time of Standard' measure_execution_speed(n_repeat) do |files, error| DidYouMean::SpellChecker.new(dictionary: files).correct error end puts '' puts 'Testing execution time of Tree' measure_execution_speed(n_repeat) do |files, error| DidYouMean::TreeSpellChecker.new(dictionary: files).correct error end puts '' puts 'Testing execution time of Augmented Tree' measure_execution_speed(n_repeat) do |files, error| DidYouMean::TreeSpellChecker.new(dictionary: files, augment: true).correct error end end private def measure_execution_speed(n_repeat, &block) len = RSPEC_DIRECTORIES.length start_time = Time.now n_repeat.times do word = RSPEC_DIRECTORIES[rand len] word_error = TreeSpell::HumanTypo.new(word).call block.call(RSPEC_DIRECTORIES, word_error) end time_ms = (Time.now - start_time).to_f * 1000 / n_repeat puts "Average time (ms): #{time_ms.round(1)}" end def many_typos(n_repeat, files, title) first_times = [0, 0, 0] total_suggestions = [0, 0, 0] total_failures = [0, 0, 0] len = files.length n_repeat.times do word = files[rand len] word_error = TreeSpell::HumanTypo.new(word).call suggestions_a = group_suggestions word_error, files check_first_is_right word, suggestions_a, first_times check_no_suggestions suggestions_a, total_suggestions check_for_failure word, suggestions_a, total_failures end print_results first_times, total_suggestions, total_failures, n_repeat, title end def group_suggestions(word_error, files) a0 = DidYouMean::TreeSpellChecker.new(dictionary: files).correct word_error a1 = ::DidYouMean::SpellChecker.new(dictionary: files).correct word_error a2 = a0.empty? ? a1 : a0 [a0, a1, a2] end def check_for_failure(word, suggestions_a, total_failures) suggestions_a.each_with_index.map do |a, i| total_failures[i] += 1 unless a.include? word end end def check_first_is_right(word, suggestions_a, first_times) suggestions_a.each_with_index.map do |a, i| first_times[i] += 1 if word == a.first end end def check_no_suggestions(suggestions_a, total_suggestions) suggestions_a.each_with_index.map do |a, i| total_suggestions[i] += a.length end end def print_results(first_times, total_suggestions, total_failures, n_repeat, title) algorithms = ['Tree ', 'Standard ', 'Augmented'] print_header title (0..2).each do |i| ft = (first_times[i].to_f / n_repeat * 100).round(1) mns = (total_suggestions[i].to_f / (n_repeat - total_failures[i])).round(1) f = (total_failures[i].to_f / n_repeat * 100).round(1) puts " #{algorithms[i]} #{' ' * 7} #{ft} #{' ' * 14} #{mns} #{' ' * 15} #{f} #{' ' * 16}" end end def print_header(title) puts "#{' ' * 30} #{title} Summary #{' ' * 31}" puts '-' * 80 puts " Method | First Time (\%) Mean Suggestions Failures (\%) #{' ' * 13}" puts '-' * 80 end end did_you_mean-2.0.0/Rakefile0000644000004100000410000000416314735503325015655 0ustar www-datawww-datarequire 'bundler/gem_tasks' require 'rake/testtask' Rake::TestTask.new do |task| task.libs << "test/lib" << "test" task.test_files = Dir['test/**/test_*.rb'].reject {|path| path.end_with?("test_explore.rb") } task.verbose = true task.warning = true task.ruby_opts = %w[ --disable-did_you_mean -rhelper ] end Rake::TestTask.new("test:explore") do |task| task.libs << "test" task.pattern = 'test/tree_spell/test_explore.rb' task.verbose = true task.warning = true task.ruby_opts = %w[ --disable-did_you_mean ] end task default: %i(test) namespace :test do namespace :accuracy do desc "Download Wiktionary's Simple English data and save it as a dictionary" task :prepare do sh "RUBYOPT='--disable-did_you_mean' ruby evaluation/dictionary_generator.rb" end end desc "Calculate accuracy of the gem's spell checker" task :accuracy do if !File.exist?("evaluation/dictionary.yml") puts 'Generating dictionary for evaluation:' Rake::Task["test:accuracy:prepare"].execute puts "\n" end sh "RUBYOPT='--disable-did_you_mean' ruby evaluation/calculator.rb" end end namespace :benchmark do namespace :ips do desc "Measure performance of the gem's Jaro distance implementation" task :jaro do sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/jaro_winkler/speed.rb" end desc "Benchmark performance of the gem's Levenshtein distance implementation" task :levenshtein do sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/levenshtein/speed.rb" end end desc "Benchmark memory usage in the gem's spell checker" task :memory do sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/memory_usage.rb" end namespace :memory do desc "Benchmark memory usage in the gem's Jaro distance implementation" task :jaro do sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/jaro_winkler/memory_usage.rb" end desc "Benchmark memory usage in the gem's Levenshtein distance implementation" task :levenshtein do sh "RUBYOPT='--disable-did_you_mean' ruby benchmark/levenshtein/memory_usage.rb" end end end did_you_mean-2.0.0/Gemfile0000644000004100000410000000042414735503325015477 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in did_you_mean.gemspec gemspec gem 'benchmark-ips' gem 'benchmark_driver' gem 'memory_profiler' gem 'jaro_winkler', '>= 1.4.0' gem 'test-unit' gem "test-unit-ruby-core" group :development do gem "rake" end did_you_mean-2.0.0/benchmark/0000755000004100000410000000000014735503325016136 5ustar www-datawww-datadid_you_mean-2.0.0/benchmark/levenshtein/0000755000004100000410000000000014735503325020462 5ustar www-datawww-datadid_you_mean-2.0.0/benchmark/levenshtein/speed.rb0000644000004100000410000000113214735503325022104 0ustar www-datawww-data# frozen-string-literal: true require 'benchmark/ips' require 'did_you_mean/levenshtein' Benchmark.ips do |x| x.report "original" do DidYouMean::Levenshtein.distance "user_signed_in?", "user_logged_in?" end # This #proposed method is not defined. Write your own method using this # name so we can reliably run the benchmark and measure the difference. # # Alternatively, you could directly update the #distance method and remove # this completely. # # x.report "proposed" do # DidYouMean::Levenshtein.proposed "user_signed_in?", "user_logged_in?" # end x.compare! end did_you_mean-2.0.0/benchmark/levenshtein/memory_usage.rb0000644000004100000410000000037014735503325023503 0ustar www-datawww-data# frozen-string-literal: true require 'memory_profiler' require 'did_you_mean/levenshtein' report = MemoryProfiler.report do 1000.times do DidYouMean::Levenshtein.distance "user_signed_in?", "user_logged_in?" end end report.pretty_print did_you_mean-2.0.0/benchmark/memory_usage.rb0000644000004100000410000000063214735503325021160 0ustar www-datawww-data# frozen-string-literal: true require 'memory_profiler' require 'did_you_mean' # public def foo; end # error = (self.fooo rescue $!) # executable = -> { error.to_s } METHODS = ''.methods INPUT = 'start_with?' collection = DidYouMean::SpellChecker.new(dictionary: METHODS) executable = proc { collection.correct(INPUT) } GC.disable MemoryProfiler.report { 100.times(&executable) }.pretty_print did_you_mean-2.0.0/benchmark/speed.yml0000644000004100000410000000071214735503325017761 0ustar www-datawww-dataprelude: | require 'did_you_mean/spell_checker' str1, str2 = "user_signed_in?", "user_logged_in?" METHODS = ''.methods INPUT = 'starts_with?' collection = DidYouMean::SpellChecker.new(dictionary: METHODS) benchmark: Jaro: DidYouMean::Jaro.distance(str1, str2) Jaro Winkler: DidYouMean::JaroWinkler.distance(str1, str2) Levenshtein: DidYouMean::Levenshtein.distance(str1, str2) Spell checker: collection.correct(INPUT) did_you_mean-2.0.0/benchmark/jaro_winkler/0000755000004100000410000000000014735503325020624 5ustar www-datawww-datadid_you_mean-2.0.0/benchmark/jaro_winkler/speed.rb0000644000004100000410000000111514735503325022247 0ustar www-datawww-data# frozen-string-literal: true require 'benchmark/ips' require 'did_you_mean/jaro_winkler' Benchmark.ips do |x| x.report "original" do DidYouMean::Jaro.distance "user_signed_in?", "user_logged_in?" end # This #proposed method is not defined. Write your own method using this # name so we can reliably run the benchmark and measure the difference. # # Alternatively, you could directly update the #distance method and remove # this completely. # # x.report "proposed" do # DidYouMean::Jaro.proposed "user_signed_in?", "user_logged_in?" # end x.compare! end did_you_mean-2.0.0/benchmark/jaro_winkler/memory_usage.rb0000644000004100000410000000036214735503325023646 0ustar www-datawww-data# frozen-string-literal: true require 'memory_profiler' require 'did_you_mean/jaro_winkler' report = MemoryProfiler.report do 1000.times do DidYouMean::Jaro.distance "user_signed_in?", "user_logged_in?" end end report.pretty_print did_you_mean-2.0.0/benchmark/require_path_checker.rb0000644000004100000410000000225414735503325022642 0ustar www-datawww-data# frozen-string-literal: true # # Run the following command to run this script: # # $ ruby --disable-did_you_mean benchmark/require_path_checker.rb # require_relative '../lib/did_you_mean/spell_checkers/require_path_checker' require 'benchmark/ips' Benchmark.ips do |x| x.config(time: 10, warmup: 10) exception_with_slash = begin require 'net/htto' rescue LoadError => error error end exception_without_slash = begin require 'net-http' rescue LoadError => error error end checker_for_path = DidYouMean::RequirePathChecker.new(exception_with_slash) checker_for_file = DidYouMean::RequirePathChecker.new(exception_without_slash) x.report "original (with a /)" do checker_for_path.corrections end x.report "original (without /)" do checker_for_file.corrections end #x.report "proposed (with a /)" do # checker_for_path.experiment #end # #x.report "proposed (without /)" do # checker_for_file.experiment #end x.compare! end did_you_mean-2.0.0/README.md0000644000004100000410000001132414735503325015464 0ustar www-datawww-data# 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) ## Installation Ruby 2.3 and later ships with this gem and it will automatically be `require`d when a Ruby process starts up. No special setup is required. ## Examples ### NameError #### Correcting a Misspelled Method Name ```ruby methosd # => NameError: undefined local variable or method `methosd' for main:Object # Did you mean? methods # method ``` #### Correcting a Misspelled Class Name ```ruby OBject # => NameError: uninitialized constant OBject # Did you mean? Object ``` #### Suggesting an Instance Variable Name ```ruby @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 ``` #### Correcting a Class Variable Name ```ruby @@full_name = "Yuki Nishijima" @@full_anme # => NameError: uninitialized class variable @@full_anme in Object # Did you mean? @@full_name ``` ### NoMethodError ```ruby full_name = "Yuki Nishijima" full_name.starts_with?("Y") # => NoMethodError: undefined method `starts_with?' for "Yuki Nishijima":String # Did you mean? start_with? ``` ### KeyError ```ruby hash = {foo: 1, bar: 2, baz: 3} hash.fetch(:fooo) # => KeyError: key not found: :fooo # Did you mean? :foo ``` ### LoadError ```ruby require 'net-http' # => LoadError (cannot load such file -- net-http) # Did you mean? net/http ``` ### NoMatchingPatternKeyError ```ruby hash = {foo: 1, bar: 2, baz: 3} hash => {fooo:} # => NoMatchingPatternKeyError: key not found: :fooo # Did you mean? :foo ``` ## Using the `DidYouMean::SpellChecker` If you need to programmatically find the closest matches to the user input, you could do so by re-using the `DidYouMean::SpellChecker` object. ```ruby spell_checker = DidYouMean::SpellChecker.new(dictionary: ['email', 'fail', 'eval']) spell_checker.correct('meail') # => ['email'] spell_checker.correct('afil') # => ['fail'] ``` ## 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: ```bash $ ruby --disable-did_you_mean -e "1.zeor?" -e:1:in `
': 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 apply options using the `RUBYOPT` environment variable: ```bash $ 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: ```ruby 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) ``` ## Benchmarking Performance is very important as the `did_you_mean` gem attempts to find the closest matches on the fly right after an exception is thrown. You could use the following rake tasks to get insights into how the gem performs: ```bash bundle exec rake benchmark:ips:jaro bundle exec rake benchmark:ips:levenshtein bundle exec rake benchmark:memory bundle exec rake benchmark:memory:jaro bundle exec rake benchmark:memory:levenshtein ``` **Be sure to always use `bundle exec` otherwise it will activate the pre-installed version of the `did_you_mean` gem rather than using what's in the `lib/`.** You could also use the [`benchmark-driver`](https://github.com/benchmark-driver/benchmark-driver) gem to know how each Ruby performs differently. ```bash 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 ``` ## Contributing 1. Fork it (https://github.com/ruby/did_you_mean/fork) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Make sure all tests pass (`bundle exec rake`) 5. Push to the branch (`git push origin my-new-feature`) 6. Create new Pull Request ## License Copyright (c) 2014-16 Yuki Nishijima. See MIT-LICENSE for further details. did_you_mean-2.0.0/appveyor.yml0000644000004100000410000000071714735503325016601 0ustar www-datawww-datainstall: - set PATH=C:\Ruby26-x64\bin;%PATH% build: off branches: only: - master environment: ruby_version: "24-%Platform%" zlib_version: "1.2.11" matrix: - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 before_test: - ruby -v - gem -v - bundle -v - gem uni did_you_mean - gem i test-unit-ruby-core test_script: - rake did_you_mean-2.0.0/CHANGELOG.md0000644000004100000410000005620414735503325016024 0ustar www-datawww-data## v2.0.0 _unreleased_ #### 🚨 Breaking Changes - Removed deprecated constants for Ruby 3.4 (#194) #### ⭐️ New Features - Do not include a backtick in error messages and backtraces (#192, https://bugs.ruby-lang.org/issues/16495) #### 🐞 Bug Fixes - Do not use #inspect to avoid unexpected performance degradation (bd11eefd6cc724919dd645965856966744a554c6) ## v1.6.3 _released at 2022-12-19 5:57:00 UTC_ - Do not suggest #name= for #name. ## v1.6.2 _released at 2022-12-05 10:29:20 UTC_ - Define Exception#detailed_message instead of clobbering #message - Fixed correction duplicates in VaribaleNameChecker ## v1.6.1 _released at 2020-12-22 13:22:35 UTC_ #### Deprecations - Deprecate custom formatters to reduce complexity for Ractor support. - Deprecate access to the `DidYouMean::SPELL_CHECKERS` constant for Ractor support. #### Features - The `did_you_mean` gem is now Ractor-compatible (`8faba54b`) - Suggest keys on NoMatchingPatternKeyError (#159, @k-tsj) - Make the same name check case-sensitive (#164, @pocke) Before: ```ruby DidYouMean::SpellChecker.new(dictionary: ['Method', 'MEthod']).correct("MEthod") # => ['Method', 'method'] ``` After: ```ruby DidYouMean::SpellChecker.new(dictionary: ['Method', 'MEthod']).correct("MEthod") # => ['Method'] ``` ## v1.6.0 (yanked) _This version has been yanked due to significant and unexpected breaking changes._ ## [v1.5.0](https://github.com/ruby/did_you_mean/tree/v1.5.0) _released at 2020-12-22 05:47:21 UTC_ #### Features - Suggest require paths on LoadError ([#143](https://github.com/ruby/did_you_mean/pull/143)) ## [v1.4.0](https://github.com/ruby/did_you_mean/tree/v1.4.0) _released at 2020-05-09 02:56:43 UTC_ As of Ruby 2.7, the `did_you_mean` gem has been promoted up to a default gem. I would like to thank [@kddeisz](https://github.com/kddeisz) for his hard work on making the entire gem easily portable the main ruby/ruby repo ([#132](https://github.com/ruby/did_you_mean/pull/132), [#131](https://github.com/ruby/did_you_mean/pull/131), and [ruby/ruby#2631](https://github.com/ruby/ruby/pull/2631)). #### Features - Add a new tree spell checker ([#119](https://github.com/ruby/did_you_mean/pull/119), [@obromios](https://github.com/obromios)) - Add a public API for registering an error ([#123](https://github.com/ruby/did_you_mean/pull/123), [@kddeisz](https://github.com/kddeisz)) #### Bug fixes - Fixes a bug where wrong suggestion could be made when string requested on hash and keys are symbols ([@localhostdotdev](https://github.com/localhostdotdev), [#134](https://github.com/ruby/did_you_mean/pull/134)) #### Breaking changes - Experimental features have been removed ([#135](https://github.com/ruby/did_you_mean/pull/135)) #### Internal changes - Replace Travis CI with GitHub Actions ([#124](https://github.com/ruby/did_you_mean/pull/124)) - Drop mintiest dependency ([#129](https://github.com/ruby/did_you_mean/pull/129)) - Drop delegate dependency ([#138](https://github.com/ruby/did_you_mean/pull/138)) ## [v1.3.1](https://github.com/ruby/did_you_mean/tree/v1.3.1) _released at 2019-09-29 03:58:46 UTC_ #### Bug fixes - Fixes a test failure in Ruby core where DYM attempts to mutate immutable strings from `Symbol#to_s` ([#125](https://github.com/ruby/did_you_mean/pull/125), [@nobu](https://github.com/nobu), [@eregon](https://github.com/eregon), [@MSP-Greg](https://github.com/MSP-Greg)) - Removes the empty `tmp/` directory to comply with `rpmlint` ([#122](https://github.com/ruby/did_you_mean/issues/122), [@pvalena](https://github.com/pvalena)) - Fixes a bug where suggestions are not shown on subsequent errors ([#120](https://github.com/ruby/did_you_mean/issues/120), [@localhostdotdev](https://github.com/localhostdotdev)) ## [v1.3.0](https://github.com/ruby/did_you_mean/tree/v1.3.0) _released at 2018-12-18 15:37:10 UTC_ **Starting version 1.3, the `did_you_mean` gem will be compatible with 2.6 and 2.5, and we will try to keep all subsequent versions compatible with Ruby 2.5 on an best-effort basis.** - Version 1.2.0 only has support for Ruby 2.5.0 and later as it uses new features that are only available in 2.5. - Versions earlier than 1.1.\* will still be maintained until Ruby 2.4 is deprecated. - Versions earlier than 1.0.\* is still maintained, but are likely to be deprecated as Ruby 2.3 will (probably) be deprecated in 2019. - Support for versions below 1.0 has already ended. #### New features - Suggest reserved words if there are close matches ([2a082a7](https://github.com/ruby/did_you_mean/commit/2a082a71991f5afe2e27ce9538103eac4c428025)) ```ruby results = yiedl # NameError => undefined local variable or method `yiedl' for ... # Did you mean? yield ``` #### Bug fixes - Fixes a bug where name errors can not be dumped ([#108](https://github.com/ruby/did_you_mean/issues/108), [@jessebs](https://github.com/jessebs)) ## [v1.2.2](https://github.com/ruby/did_you_mean/tree/v1.2.2) _released at 2019-03-09 23:17:22 UTC_ #### Bug fixes - Fixes a bug where name errors can not be dumped ([#108](https://github.com/ruby/did_you_mean/issues/108), [@jessebs](https://github.com/jessebs)) ## [v1.2.1](https://github.com/ruby/did_you_mean/tree/v1.2.1) _released at 2018-04-03 04:44:47 UTC_ #### Bug Fixes - Fixed a bug where DYM suggests the same class name in the error message ([#102](https://github.com/ruby/did_you_mean/issues/102), [@schneems](https://github.com/schneems)) - Fixed a bug where the deprecated `DidYouMean::Formatter` has been removed unexpectedly ([#103](https://github.com/ruby/did_you_mean/issues/103), [4b5ba32](https://github.com/ruby/did_you_mean/commit/4b5ba3215975df1dd8e9c7eacffcf02abfffa92e)) #### Other Changes - Non-production code that has a non-commercial license has been removed from gem releases ([#105](https://github.com/ruby/did_you_mean/issues/105), [@jbotelho2-bb](https://github.com/jbotelho2-bb)) ## [v1.2.0](https://github.com/ruby/did_you_mean/tree/v1.2.0) _released at 2018-01-02 20:49:35 UTC_ **This version 1.2.0 only has support for Ruby 2.5.0 and later as it uses new features that are only available in 2.5. All future patch releases under 1.2.\* will only be compatible with Ruby 2.5.0 and later as well. Versions earlier than 1.1.\* will still be maintained until Ruby 2.4 is deprecated. Any other versions below 1.0 will no longer be maintained.** #### New features - The `KeyError` name suggestion feature has been promoted to a stable feature and is available by default ([acf5945](https://github.com/ruby/did_you_mean/commit/acf59450dfb67eefae9b465ccc8029af46ae7dd0), [https://bugs.ruby-lang.org/issues/12063](https://bugs.ruby-lang.org/issues/12063)) - Now suggests `true`, `false` or `nil` if a NameError occurs because of a typo in these names ([#94](https://github.com/ruby/did_you_mean/pull/94), [@styd](https://github.com/styd)) - New Formatter API: This provides a clean way to customize DidYouMean’s formatter without overriding the default formatter. Please refer to [the built-in verbose formatter](https://github.com/ruby/did_you_mean/blob/671cdff/lib/did_you_mean/verbose.rb) for how to use it ## [v1.1.3](https://github.com/ruby/did_you_mean/tree/v1.1.3) _released at 2019-03-09 23:16:54 UTC_ #### Bug fixes - Fixes a bug where name errors can not be dumped ([#108](https://github.com/ruby/did_you_mean/issues/108), [@jessebs](https://github.com/jessebs)) - Fixed a bug where DYM suggests the same class name in the error message ([#102](https://github.com/ruby/did_you_mean/issues/102), [@schneems](https://github.com/schneems)) ## [v1.1.2](https://github.com/ruby/did_you_mean/tree/v1.1.2) _released at 2017-09-24 07:28:48 UTC_ **This version is only compatible with Ruby 2.4 and later.** #### Bug Fixes - Fixed a bug where `did_you_mean` shows duplicate suggestions when the exception is raised multiple times ([#84](https://github.com/ruby/did_you_mean/pull/84), [c2e4008](https://github.com/ruby/did_you_mean/commit/c2e40083cef604c00ccd10efc6d7a5036ad9eb5b)) ## [v1.1.1](https://github.com/ruby/did_you_mean/tree/v1.1.1) _released at 2017-09-24 07:24:02 UTC_ ### This version has been yanked from Rubygems.org and is not available. ## [v1.1.0](https://github.com/ruby/did_you_mean/tree/v1.1.0) _released at 2016-12-19 23:19:06 UTC_ The version `1.1.0` only has support for Ruby 2.4.0 and later. Also, all patch releases under `1.1.*` will only be compatible with Ruby 2.4.0 and later as well. Versions under `1.0.*` will still be maintained until Ruby 2.3 is deprecated. Any other versions below `1.0` will no longer be maintained. #### New Features - Suggest a method name on a NameError from the `Struct#[]` or `Struct#[]=` method ([#73](https://github.com/ruby/did_you_mean/pull/73)): ```ruby Struct.new(:foo).new[:fooo] # => NameError: no member 'fooo' in struct # Did you mean? foo # foo= ``` - Added a public interface for the gem's spell checker: ```ruby DidYouMean::SpellChecker.new(dictionary: ['email', 'fail', 'eval']).correct('meail') # => ['email'] ``` - Methods defined on `nil` by default are no longer suggested. Note that methods, defined after the gem is loaded, will still be suggested (e.g. ActiveSupport). #### Bug Fixes - Fixed a bug where private method names were added to the dictionary when an argument was passed in to a public method. Use the `NoMethodError#private_call?` method instead ([0a1b761](https://github.com/ruby/did_you_mean/commit/0a1b7612252055e583a373b473932f789381ca0f)) ## [v1.0.4](https://github.com/ruby/did_you_mean/tree/v1.0.4) _released at 2019-03-09 23:16:38 UTC_ #### Bug fixes - Fixes a bug where name errors can not be dumped ([#108](https://github.com/ruby/did_you_mean/issues/108), [@jessebs](https://github.com/jessebs)) - Fixed a bug where DYM suggests the same class name in the error message ([#102](https://github.com/ruby/did_you_mean/issues/102), [@schneems](https://github.com/schneems)) ## [v1.0.3](https://github.com/ruby/did_you_mean/tree/v1.0.3) _released at 2017-09-24 07:22:07 UTC_ **This version is compatible with Ruby 2.3 and older** #### Bug Fixes - Fixed a bug where `did_you_mean` shows duplicate suggestions when the exception is raised multiple times ([#84](https://github.com/ruby/did_you_mean/pull/84), [c2e4008](https://github.com/ruby/did_you_mean/commit/c2e40083cef604c00ccd10efc6d7a5036ad9eb5b)) ## [v1.0.2](https://github.com/ruby/did_you_mean/tree/v1.0.2) _released at 2016-11-20 18:03:07 UTC_ **This version is compatible with Ruby 2.3 and older** #### Features - Experimental features are officially available through `require 'did_you_mean/experimental'` #### Deprecations - `require 'did_you_mean/extra_features'` is now deprecated in favor of `require 'did_you_mean/experimental'` #### Internal Changes - Replaced the `DidYouMean::SpellCheckable` module with the `DidYouMean::SpellChecker` class. This is a slower implementation but close to the model explained in [this talk](https://speakerdeck.com/yuki24/saving-people-from-typos), more reusable and possibly makes it easier to expose the class as a public interface. ## [v1.0.1](https://github.com/ruby/did_you_mean/tree/v1.0.1) _released at 2016-05-15 05:17:22 UTC_ #### Bug Fixes - Fixed a bug where the gem suggests what is actually typed by the user: [1c52c88](https://github.com/ruby/did_you_mean/commit/1c52c887c62b0921e799f94bcc4a846dc7cbc057) - Fixed features that didn't work on JRuby 9.1.0.0: [dc48dde](https://github.com/ruby/did_you_mean/commit/dc48dde1b2a8f05aab1fcf897e1cb3075a206f53), [4de23f8](https://github.com/ruby/did_you_mean/commit/4de23f880502c80c5f321371d39c08bb0fa34040), [00e3059](https://github.com/ruby/did_you_mean/commit/00e305971060d150fae4817b5e895d6478b37579). The local variable name correction is still disabled. Also see: [jruby/jruby#3480](https://github.com/jruby/jruby/issues/3480) ## [v1.0.0](https://github.com/ruby/did_you_mean/tree/v1.0.0) _released at 2015-12-25 05:13:04 UTC_ #### Features - Introduced a [verbose formatter](https://github.com/ruby/did_you_mean#verbose-formatter) - Introduced an easy way to enabling [experimental features](https://github.com/ruby/did_you_mean#experimental-features) #### Bug Fixes - Fixed a bug where the Jaro-Winkler implementation returns the wrong distance when 2 identical strings are given. fixes [#58](https://github.com/ruby/did_you_mean/pull/58) #### Internal Changes - Slightly changed the spell checking algorithm. Take a look at [e2f5b24](https://github.com/ruby/did_you_mean/commit/e2f5b2437f967565e4830eab6077f73ae166e0a7) for more details. fixes [#60](https://github.com/ruby/did_you_mean/issues/60) ## [v1.0.0.rc1](https://github.com/ruby/did_you_mean/tree/v1.0.0.rc1) _released at 2015-12-25 05:02:25 UTC_ #### Internal Changes - No longer uses `TracePoint` API by default. fixes [#55](https://github.com/ruby/did_you_mean/issues/55) and [#56](https://github.com/ruby/did_you_mean/issues/56) ## [v1.0.0.beta3](https://github.com/ruby/did_you_mean/tree/v1.0.0.beta3) _released at 2015-12-25 04:56:13 UTC_ #### Internal Changes - Use the `frozen-string-literal` pragma rather than calling `.freeze` everywhere - Use the `NameError#receiver` method in `DidYouMean:: ClassNameChecker` to know the namespace where the constant call is made - Refactored the `SpellCheckerTest` ## [v1.0.0.beta2](https://github.com/ruby/did_you_mean/tree/v1.0.0.beta2) _released at 2015-12-25 04:50:36 UTC_ #### Bug Fixes - Fixed a bug where the gem doesn't install properly on Ruby 2.3.0dev ## [v1.0.0.beta1](https://github.com/ruby/did_you_mean/tree/v1.0.0.beta1) _released at 2015-12-25 05:27:53 UTC_ #### Breaking Changes - Dropped support for MRIs older than 2.3, JRuby and Rubinus #### Internal Changes - The C extension has been removed since the `NameError#receiver` method has become part of the MRI 2.3 - The interception gem has been removed from the dependencies - Removed code that was needed to support multiple Ruby implementations ## [v0.10.0](https://github.com/ruby/did_you_mean/tree/v0.10.0) _released at 2015-08-21 06:44:11 UTC_ #### Features - Now it corrects an instance variable name if the ivar name is mistyped and `NoMethodError` is raised: ```ruby @number = 1 @nubmer.zero? # => NoMethodError: undefined method `zero?' for nil:NilClass # # Did you mean? @number # ``` - Support for JRuby 9.0.0.0 - Prefix-based correction ( [@tjohn](https://github.com/tjohn), [#50](https://github.com/ruby/did_you_mean/issues/50 "Match start of method name"), [#49](https://github.com/ruby/did_you_mean/issues/49 "Use Jaro distance instead of Jaro-Winkler distance")) - Correction search is about 75% faster than 0.9.10 #### Breaking Changes - The ActiveRecord integration has been removed ## [v0.9.10](https://github.com/ruby/did_you_mean/tree/v0.9.10) _released at 2015-05-14 03:04:47 UTC_ #### Bug Fixes - Fixed a bug where a duplicate "did you mean?" message was appended each time `#to_s` is called ( [@danfinnie](https://github.com/danfinnie), [#51](https://github.com/ruby/did_you_mean/issues/51 "Duplicate output for constants in separate gem")) ## [v0.9.9](https://github.com/ruby/did_you_mean/tree/v0.9.9) _released at 2015-05-13 03:48:19 UTC_ #### Features - Order word suggestions based on Levenshtein distance ( [@tleish](https://github.com/tleish), [#31](https://github.com/ruby/did_you_mean/pull/31)) #### Internal Changes - Reduce memory allocation by about 40% - Speed up Levenshtein distance calculation by about 40% - The Java extension has been replaced with a pure JRuby implementation ## [v0.9.8](https://github.com/ruby/did_you_mean/tree/v0.9.8) _released at 2015-04-12 01:55:27 UTC_ #### Internal Changes - Speed up Levenshtein by 50% and reduce 97% of memory usage ## [v0.9.7](https://github.com/ruby/did_you_mean/tree/v0.9.7) _released at 2015-04-02 04:20:26 UTC_ #### Bug Fixes - Fixed an issue where _did\_you\_mean_ doesn't install on JRuby properly. ## [v0.9.6](https://github.com/ruby/did_you_mean/tree/v0.9.6) _released at 2015-01-24 23:19:27 UTC_ #### Bug Fixes - Fixed a bug where did\_you\_mean incorrectly suggests protected methods when it just isn't callable ( [@glittershark](https://github.com/glittershark), [#34](https://github.com/ruby/did_you_mean/issues/34 "Did\_you\_mean incorrectly called when attempting to call protected/private method")) ## [v0.9.5](https://github.com/ruby/did_you_mean/tree/v0.9.5) _released at 2015-01-07 12:41:23 UTC_ #### Bug Fixes - Whitelist `#safe_constantize` method from `ActiveSupport::Inflector` to avoid significant performance slowdown ( [@tleish](https://github.com/tleish), [#19](https://github.com/ruby/did_you_mean/issues/19 "Significant Slowdown when Using Debugger"), [#20](https://github.com/ruby/did_you_mean/pull/20 "Whitelisting safe\_constantize (ActiveSupport::Inflector) method")) ## [v0.9.4](https://github.com/ruby/did_you_mean/tree/v0.9.4) _released at 2014-11-19 20:00:00 UTC_ #### Bug Fixes - Fixed a bug where no suggestions will be made on JRuby ## [v0.9.3](https://github.com/ruby/did_you_mean/tree/v0.9.3) _released at 2014-11-18 03:50:11 UTC_ **This version has been yanked from rubygems.org as it doesn't work with jRuby at all. Please upgrade to 0.9.4 or higher as soon as possible.** #### Internal Changes - Replaced the crazy C extension with a so much better one (thanks to [@nobu](https://github.com/nobu)!) ## [v0.9.2](https://github.com/ruby/did_you_mean/tree/v0.9.2) _released at 2014-11-17 15:32:33 UTC_ #### Bug Fixes - Fixed a bug where did\_you\_mean doesn't compile on Ruby 2.1.2/2.1.5 ( [#16](https://github.com/ruby/did_you_mean/issues/16 "Gem building failed on Debian 6.0.10 x86\_64")) ## [v0.9.1](https://github.com/ruby/did_you_mean/tree/v0.9.1) _released at 2014-11-16 18:54:24 UTC_ **This version has been yanked from rubygems.org as it doesn't compile on Ruby 2.1.2 and 2.1.5. Please upgrade to 0.9.4 or higher as soon as possible.** #### Internal Changes - Shrink the gem size by removing unneeded ruby header files. - Now it forces everyone to upgrade the gem when they upgrade Ruby to a new version. This avoids introducing a bug like [#14](https://github.com/ruby/did_you_mean/issues/14 "Compatibility with `letter\_opener` gem"). ## [v0.9.0](https://github.com/ruby/did_you_mean/tree/v0.9.0) _released at 2014-11-09 01:26:31 UTC_ #### Features - did\_you\_mean now suggests instance variable names if `@` is missing ( [#12](https://github.com/ruby/did_you_mean/issues/12 "Suggest instance- and class-vars"), [39d1e2b](https://github.com/ruby/did_you_mean/commit/39d1e2bd66d6ff8acbc4dd5da922fc7e5fcefb20)) ```ruby @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 # ``` #### Bug Fixes - Fixed a bug where did\_you\_mean changes some behaviours of Ruby 2.1.3/2.1.4 installed on Max OS X ( [#14](https://github.com/ruby/did_you_mean/issues/14 "Compatibility with `letter\_opener` gem"), [44c451f](https://github.com/ruby/did_you_mean/commit/44c451f8c38b11763ba28ddf1ceb9696707ccea0), [9ebde21](https://github.com/ruby/did_you_mean/commit/9ebde211e92eac8494e704f627c62fea7fdbee16)) - Fixed a bug where sometimes `NoMethodError` suggests duplicate method names ( [9865cc5](https://github.com/ruby/did_you_mean/commit/9865cc5a9ce926dd9ad4c20d575b710e5f257a4b)) ## [v0.8.0](https://github.com/ruby/did_you_mean/tree/v0.8.0) _released at 2014-10-27 02:03:13 UTC_ **This version has been yanked from rubygems.org as it has a serious bug with Ruby 2.1.3 and 2.1.4 installed on Max OS X. Please upgrade to 0.9.4 or higher as soon as possible.** #### Features - JRuby support! #### Bug Fixes - Fixed a bug where did\_you\_mean unexpectedly disables [better\_errors](https://github.com/charliesome/better_errors)'s REPL - Replaced [binding\_of\_caller](https://github.com/banister/binding_of_caller) dependency with [interception](https://github.com/ConradIrwin/interception) - Fixed the wrong implementation of Levenshtein algorithm ( [#2](https://github.com/ruby/did_you_mean/pull/2 "Fix bug of DidYouMean::Levenshtein#min3."), [@fortissimo1997](https://github.com/fortissimo1997)) ## [v0.7.0](https://github.com/ruby/did_you_mean/tree/v0.7.0) _released at 2014-09-26 03:37:18 UTC_ **This version has been yanked from rubygems.org as it has a serious bug with Ruby 2.1.3 and 2.1.4 installed on Max OS X. Please upgrade to 0.9.4 or higher as soon as possible.** #### Features - Added support for Ruby 2.1.3, 2.2.0-preview1 and ruby-head - Added support for ActiveRecord 4.2.0.beta1 - Word searching is now about 40% faster than v0.6.0 - Removed `text` gem dependency - Better output on pry and Rspec #### Internal Changes - A lot of internal refactoring ## [v0.6.0](https://github.com/ruby/did_you_mean/tree/v0.6.0) _released at 2014-05-18 00:23:24 UTC_ **This version has been yanked from rubygems.org as it has a serious bug with Ruby 2.1.3 and 2.1.4 installed on Max OS X. Please upgrade to 0.9.4 or higher as soon as possible.** #### Features - Added basic support for constants. Now you'll see class name suggestions when you misspelled a class names/module names: ```ruby > Ocject # => NameError: uninitialized constant Ocject # # Did you mean? Object # ``` #### Bug Fixes - Fixed a bug where did\_you\_mean segfaults on Ruby head(2.2.0dev) ## [v0.5.0](https://github.com/ruby/did_you_mean/tree/v0.5.0) _released at 2014-05-10 17:59:54 UTC_ #### Features - Added support for Ruby 2.1.2 ## [v0.4.0](https://github.com/ruby/did_you_mean/tree/v0.4.0) _released at 2014-04-20 02:10:31 UTC_ #### Features - did\_you\_mean now suggests a similar attribute name when you misspelled it. ```ruby User.new(flrst_name: "wrong flrst name") # => ActiveRecord::UnknownAttributeError: unknown attribute: flrst_name # # Did you mean? first_name: string # ``` #### Bug Fixes - Fixed a bug where did\_you\_mean doesn't work with `ActiveRecord::UnknownAttributeError` ## [v0.3.1](https://github.com/ruby/did_you_mean/tree/v0.3.1) _released at 2014-03-20 23:16:20 UTC_ #### Features - Changed output for readability. - Made the spell checking algorithm slight better to find the correct method. ## [v0.3.0](https://github.com/ruby/did_you_mean/tree/v0.3.0) _released at 2014-03-20 23:13:13 UTC_ #### Features - Added support for Ruby 2.1.1 and 2.2.0(head). ## [v0.2.0](https://github.com/ruby/did_you_mean/tree/v0.2.0) _released on 2014-03-20 23:12:13 UTC_ #### Features - did\_you\_mean no longer makes Ruby slow. #### Breaking Changes - dropped support for JRuby and Rubbinious. ## [v0.1.0: First Release](https://github.com/ruby/did_you_mean/tree/v0.1.0) _released on 2014-03-20 23:11:14 UTC_ - Now you will have "did you mean?" experience in Ruby! - but still very experimental since this gem makes Ruby a lot slower. did_you_mean-2.0.0/documentation/0000755000004100000410000000000014735503325017055 5ustar www-datawww-datadid_you_mean-2.0.0/documentation/CHANGELOG.md.erb0000644000004100000410000000034614735503325021440 0ustar www-datawww-data<% releases.each do |release| %> ## [<%= release.name %>](https://github.com/<%= repository %>/tree/<%= release.tag_name %>) _released at <%= release.published_at %>_ <%= release.body.gsub(/\r\n/, "\n") %> <% end %> did_you_mean-2.0.0/documentation/tree_spell_algorithm.md0000644000004100000410000001566314735503325023616 0ustar www-datawww-data# TreeSpellChecker Algorithm ## Overview The algorithm is designed to work on a dictionary that has a rooted tree structure. The algorithm treats the problem as a hidden state system, which tries to identify the true state of the input. Due to typographical errors, the state of the input is a hidden version of the true system state. Each word in the dictionary is mapped to a multi-dimensional state, with the first dimension being being the root, the second dimension being the next branch, and so on. Each dimension is discrete with a finite number of elements. The first dimension corresponds to the root, so only has one element. The algorithm assumes the input state has the correct structure, and so generates the state of the input. It starts with the root of the input word and maps it to the root element. It then looks at the value of second dimension of the input word and chooses closest elements of the possible second dimension elements. It then continues to the third and higher dimensions. It terminates when it has worked out possible elements corresponding to the highest dimension of the input word. At this point it has the possible elements at for each dimension of the input word. It then generates all possible legitimate states from these elements. Finally it then compares the possible leaves at the end of these legitimate states with the leaf of the input word. From this process it produces suggested states. ## Accuracy The accuracy of the algorithm was tested using the HumanTypo class. It simulates a human typist by assuming that errors are Poisson distributed at a rate of one typo per 20 characters. Typos can be either a deletion, an insertion, substitution or a transposition. I ran 10,000 repititions on both the `test` directory of the ```did_you_mean``` gem and on the ```spec``` directory of the ```rspec-core``` gem. The results were as follows: ``` Minitest Summary -------------------------------------------------------------------------------- Method | First Time (%) Mean Suggestions Failures (%) -------------------------------------------------------------------------------- Tree 98.0 1.1 2.0 Standard 98.1 2.2 1.6 Augmented 100.0 1.1 0.0 ``` and ``` Rspec Summary -------------------------------------------------------------------------------- Method | First Time (%) Mean Suggestions Failures (%) -------------------------------------------------------------------------------- Tree 94.7 1.0 5.3 Standard 98.2 4.2 1.1 Augmented 99.7 1.2 0.2 ``` As well, I checked the results on the ```test``` directory with ```HumanTypo``` generating errors at three times the rate: ``` Minitest Summary (lambda = 0.15) -------------------------------------------------------------------------------- Method | First Time (%) Mean Suggestions Failures (%) -------------------------------------------------------------------------------- Tree 88.9 1.0 11.0 Standard 95.0 1.4 4.3 Combined 99.0 1.0 0.8 ``` In all cases, the tree speller, when augmented by the standard spell checker performed with higher accuracy, and giving far fewer suggestions. ## Execution Speed I tested the execution time on the ```test ``` directory: ``` Testing execution time of Standard Average time (ms): 5.2 Testing execution time of Tree Average time (ms): 1.1 Testing execution time of Augmented Tree Average time (ms): 1.2 ``` and on the ```spec``` directory ``` Testing execution time of Standard Average time (ms): 40.6 Testing execution time of Tree Average time (ms): 2.7 Testing execution time of Augmented Tree Average time (ms): 4.5 ``` I was surprised by how much faster the tree checker was compared to the standard checker. I think the reason is that the predominant computational load will scale with O(log n) where n is the total number of words in the dictionary. My reasoning is that the algorithm very quickly prunes out states as it moves through the dimensions. ## Augmentation option Given the major difference in speed between the standard and tree checker, and the likelihood that the disparity will grow rapidly with the size of the dictionary, then I suspect for some applications, it will not be not practicable to augment the tree checker by using the standard checker when the tree checker fails to find a suggestion. Accordingly, I have added an option, ```:augment?```. The default is nil, but if true, then the standard checker is used if there are no suggestions. ## Generation of Performance data This is done using ```test/tree_spell/explore_test.rb```. This is not a proper test file in that there are no assertions in it. As well, it takes over ten minutes to run, accordingly, I have disabled it by setting the constant TREE_SPELL_EXPLORE to false at the top of the file. To run the file, set TREE_SPELL_EXPLORE to true. It is also possible to run quick assessments by using a smaller value of n_repeat in the various tests. ## Future Work I have identified two categories of remaining errors. The first class is when one of the elements is corrupted to being very small. Then the standard checker does not suggest the correct element, e.g. if an element is ```core``` and it is reduced to ```co```, the standard speller will not make a suggestion. The second class of error is when the structure of the word is broken because one of the separators has been removed. I think it might be possible to remove the first type of error and dramatically reduce the second type of error in a future version. This would be done as follows: - At each level, choose the dictionary element with the smallest distance. - This will work well unless the structure is broken, in which case it could return a wildly wrong suggestion. - To guard against this, the suggestion could be checked against the input word using the standard checker. If the standard checker rejects the suggestion, then it is assumed the structure is broken. - A large proportion of the time, a broken structure will be just due to one separator missing, and the order of the elements will not be affected. Accordingly, the structure can be fixed by comparing the input elements with a concatenation of two levels of the dictionary elements. It would be possible to use the same idea to fix more than one separator missing, but this could quickly become computationally expensive. did_you_mean-2.0.0/documentation/human_typo_api.md0000644000004100000410000000107314735503325022414 0ustar www-datawww-data# HumanTypo API ## Description Simulate an error prone human typist. Assumes typographical errors are Poisson distributed and each error is either a deletion, insertion, substitution, or transposition ## Initialization ``` def initialize(input, lambda: 0.05) end ``` where ### input: A string with the word to be corrupted. ## lambda: Error rate of the poisson process The default of 0.05 corresponds to one error every 20 characters, and is thought to approximate the average, competent typist ## Methods ``` def call end ``` Returns a word with typographical errors. did_you_mean-2.0.0/documentation/tree_spell_checker_api.md0000644000004100000410000000121014735503325024044 0ustar www-datawww-data# TreeSpellChecker API ## Description ## Initialization ``` def initialize(dictionary:, separator: '/', augment: nil) end ``` where ####dictionary: The dictionary is a list of possible words * that are used to correct a misspelling * The dictionary must be tree structured with a single character separator * e.g 'spec/models/goals_spec_rb'. ####separator: A single character. Cannot be cannot be alphabetical, '@' or '.'. ####augment: When set to true, the checker will used the standard ```SpellChecker``` to find possible suggestions. ## Methods ``` def correct(input) end ``` where ####input: Is the input word to be corrected. did_you_mean-2.0.0/documentation/changelog_generator.rb0000644000004100000410000000156014735503325023401 0ustar www-datawww-datarequire 'octokit' require 'reverse_markdown' require 'erb' class ChangeLogGenerator attr :repository, :template_path, :changelog_path def initialize(repository, template_path: "CHANGELOG.md.erb", changelog_path: "CHANGELOG.md") @repository = repository @template_path = template_path @changelog_path = changelog_path end def generate_and_save! changelog_in_md = ERB.new(template).result(binding) changelog_in_html = Octokit.markdown(changelog_in_md, context: repository, mode: "gfm") File.open(changelog_path, 'w') do |file| file.write ReverseMarkdown.convert(changelog_in_html, github_flavored: true) end end private def template open("#{__dir__}/#{template_path}").read end def releases @releases ||= Octokit.releases(repository) end end ChangeLogGenerator.new("ruby/did_you_mean").generate_and_save!