did_you_mean-1.6.3/0000755000004100000410000000000014353445451014215 5ustar www-datawww-datadid_you_mean-1.6.3/test/0000755000004100000410000000000014353445451015174 5ustar www-datawww-datadid_you_mean-1.6.3/test/core_ext/0000755000004100000410000000000014353445451017004 5ustar www-datawww-datadid_you_mean-1.6.3/test/core_ext/test_name_error_extension.rb0000644000004100000410000000276414353445451024626 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_equal "undefined method `sizee' for #", Marshal.load(Marshal.dump(error)).original_message end end did_you_mean-1.6.3/test/helper.rb0000644000004100000410000000227314353445451017004 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-1.6.3/test/fixtures/0000755000004100000410000000000014353445451017045 5ustar www-datawww-datadid_you_mean-1.6.3/test/fixtures/rspec_dir.yml0000644000004100000410000001112014353445451021535 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-1.6.3/test/fixtures/mini_dir.yml0000644000004100000410000000104514353445451021362 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-1.6.3/test/fixtures/book.rb0000644000004100000410000000004314353445451020321 0ustar www-datawww-dataclass Book class Spine end end did_you_mean-1.6.3/test/tree_spell/0000755000004100000410000000000014353445451017332 5ustar www-datawww-datadid_you_mean-1.6.3/test/tree_spell/change_word.rb0000644000004100000410000000277414353445451022151 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-1.6.3/test/tree_spell/test_change_word.rb0000644000004100000410000000267114353445451023204 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-1.6.3/test/tree_spell/human_typo.rb0000644000004100000410000000322714353445451022046 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-1.6.3/test/tree_spell/test_human_typo.rb0000644000004100000410000000122214353445451023076 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-1.6.3/test/tree_spell/test_explore.rb0000644000004100000410000001027114353445451022375 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-1.6.3/test/test_tree_spell_checker.rb0000644000004100000410000001537114353445451022411 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-1.6.3/test/test_ractor_compatibility.rb0000644000004100000410000000720314353445451023005 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-1.6.3/test/lib/0000755000004100000410000000000014353445451015742 5ustar www-datawww-datadid_you_mean-1.6.3/test/lib/helper.rb0000644000004100000410000000015714353445451017551 0ustar www-datawww-datarequire "test/unit" require_relative "core_assertions" Test::Unit::TestCase.include Test::Unit::CoreAssertions did_you_mean-1.6.3/test/lib/envutil.rb0000644000004100000410000002437714353445451017772 0ustar www-datawww-data# -*- coding: us-ascii -*- # frozen_string_literal: true require "open3" require "timeout" require_relative "find_executable" begin require 'rbconfig' rescue LoadError end begin require "rbconfig/sizeof" rescue LoadError end module EnvUtil def rubybin if ruby = ENV["RUBY"] return ruby end ruby = "ruby" exeext = RbConfig::CONFIG["EXEEXT"] rubyexe = (ruby + exeext if exeext and !exeext.empty?) 3.times do if File.exist? ruby and File.executable? ruby and !File.directory? ruby return File.expand_path(ruby) end if rubyexe and File.exist? rubyexe and File.executable? rubyexe return File.expand_path(rubyexe) end ruby = File.join("..", ruby) end if defined?(RbConfig.ruby) RbConfig.ruby else "ruby" end end module_function :rubybin LANG_ENVS = %w"LANG LC_ALL LC_CTYPE" DEFAULT_SIGNALS = Signal.list DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM RUBYLIB = ENV["RUBYLIB"] class << self attr_accessor :timeout_scale attr_reader :original_internal_encoding, :original_external_encoding, :original_verbose, :original_warning def capture_global_values @original_internal_encoding = Encoding.default_internal @original_external_encoding = Encoding.default_external @original_verbose = $VERBOSE @original_warning = defined?(Warning.[]) ? %i[deprecated experimental].to_h {|i| [i, Warning[i]]} : nil end end def apply_timeout_scale(t) if scale = EnvUtil.timeout_scale t * scale else t end end module_function :apply_timeout_scale def timeout(sec, klass = nil, message = nil, &blk) return yield(sec) if sec == nil or sec.zero? sec = apply_timeout_scale(sec) Timeout.timeout(sec, klass, message, &blk) end module_function :timeout def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) reprieve = apply_timeout_scale(reprieve) if reprieve signals = Array(signal).select do |sig| DEFAULT_SIGNALS[sig.to_s] or DEFAULT_SIGNALS[Signal.signame(sig)] rescue false end signals |= [:ABRT, :KILL] case pgroup when 0, true pgroup = -pid when nil, false pgroup = pid end lldb = true if /darwin/ =~ RUBY_PLATFORM while signal = signals.shift if lldb and [:ABRT, :KILL].include?(signal) lldb = false # sudo -n: --non-interactive # lldb -p: attach # -o: run command system(*%W[sudo -n lldb -p #{pid} --batch -o bt\ all -o call\ rb_vmdebug_stack_dump_all_threads() -o quit]) true end begin Process.kill signal, pgroup rescue Errno::EINVAL next rescue Errno::ESRCH break end if signals.empty? or !reprieve Process.wait(pid) else begin Timeout.timeout(reprieve) {Process.wait(pid)} rescue Timeout::Error else break end end end $? end module_function :terminate def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false, encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error, stdout_filter: nil, stderr_filter: nil, ios: nil, signal: :TERM, rubybin: EnvUtil.rubybin, precommand: nil, **opt) timeout = apply_timeout_scale(timeout) in_c, in_p = IO.pipe out_p, out_c = IO.pipe if capture_stdout err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout opt[:in] = in_c opt[:out] = out_c if capture_stdout opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr if encoding out_p.set_encoding(encoding) if out_p err_p.set_encoding(encoding) if err_p end ios.each {|i, o = i|opt[i] = o} if ios c = "C" child_env = {} LANG_ENVS.each {|lc| child_env[lc] = c} if Array === args and Hash === args.first child_env.update(args.shift) end if RUBYLIB and lib = child_env["RUBYLIB"] child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR) end # remain env %w(ASAN_OPTIONS RUBY_ON_BUG).each{|name| child_env[name] = ENV[name] if ENV[name] } args = [args] if args.kind_of?(String) pid = spawn(child_env, *precommand, rubybin, *args, opt) in_c.close out_c&.close out_c = nil err_c&.close err_c = nil if block_given? return yield in_p, out_p, err_p, pid else th_stdout = Thread.new { out_p.read } if capture_stdout th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout in_p.write stdin_data.to_str unless stdin_data.empty? in_p.close if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) timeout_error = nil else status = terminate(pid, signal, opt[:pgroup], reprieve) terminated = Time.now end stdout = th_stdout.value if capture_stdout stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout out_p.close if capture_stdout err_p.close if capture_stderr && capture_stderr != :merge_to_stdout status ||= Process.wait2(pid)[1] stdout = stdout_filter.call(stdout) if stdout_filter stderr = stderr_filter.call(stderr) if stderr_filter if timeout_error bt = caller_locations msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)" msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n")) raise timeout_error, msg, bt.map(&:to_s) end return stdout, stderr, status end ensure [th_stdout, th_stderr].each do |th| th.kill if th end [in_c, in_p, out_c, out_p, err_c, err_p].each do |io| io&.close end [th_stdout, th_stderr].each do |th| th.join if th end end module_function :invoke_ruby def verbose_warning class << (stderr = "".dup) alias write concat def flush; end end stderr, $stderr = $stderr, stderr $VERBOSE = true yield stderr return $stderr ensure stderr, $stderr = $stderr, stderr $VERBOSE = EnvUtil.original_verbose EnvUtil.original_warning&.each {|i, v| Warning[i] = v} end module_function :verbose_warning def default_warning $VERBOSE = false yield ensure $VERBOSE = EnvUtil.original_verbose end module_function :default_warning def suppress_warning $VERBOSE = nil yield ensure $VERBOSE = EnvUtil.original_verbose end module_function :suppress_warning def under_gc_stress(stress = true) stress, GC.stress = GC.stress, stress yield ensure GC.stress = stress end module_function :under_gc_stress def with_default_external(enc) suppress_warning { Encoding.default_external = enc } yield ensure suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding } end module_function :with_default_external def with_default_internal(enc) suppress_warning { Encoding.default_internal = enc } yield ensure suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding } end module_function :with_default_internal def labeled_module(name, &block) Module.new do singleton_class.class_eval { define_method(:to_s) {name} alias inspect to_s alias name to_s } class_eval(&block) if block end end module_function :labeled_module def labeled_class(name, superclass = Object, &block) Class.new(superclass) do singleton_class.class_eval { define_method(:to_s) {name} alias inspect to_s alias name to_s } class_eval(&block) if block end end module_function :labeled_class if /darwin/ =~ RUBY_PLATFORM DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports") DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S' @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME'] def self.diagnostic_reports(signame, pid, now) return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame) cmd = File.basename(rubybin) cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd path = DIAGNOSTIC_REPORTS_PATH timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash" first = true 30.times do first ? (first = false) : sleep(0.1) Dir.glob(pat) do |name| log = File.read(name) rescue next if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log File.unlink(name) File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil return log end end end nil end else def self.diagnostic_reports(signame, pid, now) end end def self.failure_description(status, now, message = "", out = "") pid = status.pid if signo = status.termsig signame = Signal.signame(signo) sigdesc = "signal #{signo}" end log = diagnostic_reports(signame, pid, now) if signame sigdesc = "SIG#{signame} (#{sigdesc})" end if status.coredump? sigdesc = "#{sigdesc} (core dumped)" end full_message = ''.dup message = message.call if Proc === message if message and !message.empty? full_message << message << "\n" end full_message << "pid #{pid}" full_message << " exit #{status.exitstatus}" if status.exited? full_message << " killed by #{sigdesc}" if sigdesc if out and !out.empty? full_message << "\n" << out.b.gsub(/^/, '| ') full_message.sub!(/(? 0 and b > 0 assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) end rescue LoadError pend end # :call-seq: # assert_nothing_raised( *args, &block ) # #If any exceptions are given as arguments, the assertion will #fail if one of those exceptions are raised. Otherwise, the test fails #if any exceptions are raised. # #The final argument may be a failure message. # # assert_nothing_raised RuntimeError do # raise Exception #Assertion passes, Exception is not a RuntimeError # end # # assert_nothing_raised do # raise Exception #Assertion fails # end def assert_nothing_raised(*args) self._assertions += 1 if Module === args.last msg = nil else msg = args.pop end begin yield rescue Test::Unit::PendedError, *(Test::Unit::AssertionFailedError if args.empty?) raise rescue *(args.empty? ? Exception : args) => e msg = message(msg) { "Exception raised:\n<#{mu_pp(e)}>\n""Backtrace:\n" << Test.filter_backtrace(e.backtrace).map{|frame| " #{frame}"}.join("\n") } raise Test::Unit::AssertionFailedError, msg.call, e.backtrace end end def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) fname ||= caller_locations(2, 1)[0] mesg ||= fname.to_s verbose, $VERBOSE = $VERBOSE, verbose case when Array === fname fname, line = *fname when defined?(fname.path) && defined?(fname.lineno) fname, line = fname.path, fname.lineno else line = 1 end yield(code, fname, line, message(mesg) { if code.end_with?("\n") "```\n#{code}```\n" else "```\n#{code}\n```\n""no-newline" end }) ensure $VERBOSE = verbose end def assert_valid_syntax(code, *args, **opt) prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| yield if defined?(yield) assert_nothing_raised(SyntaxError, mesg) do assert_equal(:ok, syntax_check(src, fname, line), mesg) end end end def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) if child_env child_env = [child_env] else child_env = [] end out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) assert !status.signaled?, FailDesc[status, message, out] end def assert_ruby_status(args, test_stdin="", message=nil, **opt) out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) desc = FailDesc[status, message, out] assert(!status.signaled?, desc) message ||= "ruby exit status is not success:" assert(status.success?, desc) end ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") def separated_runner(token, out = nil) include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) out = out ? IO.new(out, 'w') : STDOUT at_exit { out.puts "#{token}", [Marshal.dump($!)].pack('m'), "#{token}", "#{token}assertions=#{self._assertions}" } Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) end def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) unless file and line loc, = caller_locations(1,1) file ||= loc.path line ||= loc.lineno end capture_stdout = true unless /mswin|mingw/ =~ RbConfig::CONFIG['host_os'] capture_stdout = false opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner) res_p, res_c = IO.pipe opt[:ios] = [res_c] end token_dump, token_re = new_test_token src = <\n\K.*\n(?=#{token_re}<\/error>$)/m].unpack1("m")) rescue => marshal_error ignore_stderr = nil res = nil end if res and !(SystemExit === res) if bt = res.backtrace bt.each do |l| l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} end bt.concat(caller) else res.set_backtrace(caller) end raise res end # really is it succeed? unless ignore_stderr # the body of assert_separately must not output anything to detect error assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) end assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) raise marshal_error if marshal_error end # Run Ractor-related test without influencing the main test suite def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) return unless defined?(Ractor) require = "require #{require.inspect}" if require if require_relative dir = File.dirname(caller_locations[0,1][0].absolute_path) full_path = File.expand_path(require_relative, dir) require = "#{require}; require #{full_path.inspect}" end assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) #{require} previous_verbose = $VERBOSE $VERBOSE = nil Ractor.new {} # trigger initial warning $VERBOSE = previous_verbose #{src} RUBY end # :call-seq: # assert_throw( tag, failure_message = nil, &block ) # #Fails unless the given block throws +tag+, returns the caught #value otherwise. # #An optional failure message may be provided as the final argument. # # tag = Object.new # assert_throw(tag, "#{tag} was not thrown!") do # throw tag # end def assert_throw(tag, msg = nil) ret = catch(tag) do begin yield(tag) rescue UncaughtThrowError => e thrown = e.tag end msg = message(msg) { "Expected #{mu_pp(tag)} to have been thrown"\ "#{%Q[, not #{thrown}] if thrown}" } assert(false, msg) end assert(true) ret end # :call-seq: # assert_raise( *args, &block ) # #Tests if the given block raises an exception. Acceptable exception #types may be given as optional arguments. If the last argument is a #String, it will be used as the error message. # # assert_raise do #Fails, no Exceptions are raised # end # # assert_raise NameError do # puts x #Raises NameError, so assertion succeeds # end def assert_raise(*exp, &b) case exp.last when String, Proc msg = exp.pop end begin yield rescue Test::Unit::PendedError => e return e if exp.include? Test::Unit::PendedError raise e rescue Exception => e expected = exp.any? { |ex| if ex.instance_of? Module then e.kind_of? ex else e.instance_of? ex end } assert expected, proc { flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) } return e ensure unless e exp = exp.first if exp.size == 1 flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) end end end # :call-seq: # assert_raise_with_message(exception, expected, msg = nil, &block) # #Tests if the given block raises an exception with the expected #message. # # assert_raise_with_message(RuntimeError, "foo") do # nil #Fails, no Exceptions are raised # end # # assert_raise_with_message(RuntimeError, "foo") do # raise ArgumentError, "foo" #Fails, different Exception is raised # end # # assert_raise_with_message(RuntimeError, "foo") do # raise "bar" #Fails, RuntimeError is raised but the message differs # end # # assert_raise_with_message(RuntimeError, "foo") do # raise "foo" #Raises RuntimeError with the message, so assertion succeeds # end def assert_raise_with_message(exception, expected, msg = nil, &block) case expected when String assert = :assert_equal when Regexp assert = :assert_match else raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" end ex = m = nil EnvUtil.with_default_internal(expected.encoding) do ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do yield end m = ex.message end msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} if assert == :assert_equal assert_equal(expected, m, msg) else msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } assert expected =~ m, msg block.binding.eval("proc{|_|$~=_}").call($~) end ex end TEST_DIR = File.join(__dir__, "test/unit") #:nodoc: # :call-seq: # assert(test, [failure_message]) # #Tests if +test+ is true. # #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used #as the failure message. Otherwise, the result of calling +msg+ will be #used as the message if the assertion fails. # #If no +msg+ is given, a default message will be used. # # assert(false, "This was expected to be true") def assert(test, *msgs) case msg = msgs.first when String, Proc when nil msgs.shift else bt = caller.reject { |s| s.start_with?(TEST_DIR) } raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt end unless msgs.empty? super end # :call-seq: # assert_respond_to( object, method, failure_message = nil ) # #Tests if the given Object responds to +method+. # #An optional failure message may be provided as the final argument. # # assert_respond_to("hello", :reverse) #Succeeds # assert_respond_to("hello", :does_not_exist) #Fails def assert_respond_to(obj, (meth, *priv), msg = nil) unless priv.empty? msg = message(msg) { "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" } return assert obj.respond_to?(meth, *priv), msg end #get rid of overcounting if caller_locations(1, 1)[0].path.start_with?(TEST_DIR) return if obj.respond_to?(meth) end super(obj, meth, msg) end # :call-seq: # assert_not_respond_to( object, method, failure_message = nil ) # #Tests if the given Object does not respond to +method+. # #An optional failure message may be provided as the final argument. # # assert_not_respond_to("hello", :reverse) #Fails # assert_not_respond_to("hello", :does_not_exist) #Succeeds def assert_not_respond_to(obj, (meth, *priv), msg = nil) unless priv.empty? msg = message(msg) { "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" } return assert !obj.respond_to?(meth, *priv), msg end #get rid of overcounting if caller_locations(1, 1)[0].path.start_with?(TEST_DIR) return unless obj.respond_to?(meth) end refute_respond_to(obj, meth, msg) end # pattern_list is an array which contains regexp and :*. # :* means any sequence. # # pattern_list is anchored. # Use [:*, regexp, :*] for non-anchored match. def assert_pattern_list(pattern_list, actual, message=nil) rest = actual anchored = true pattern_list.each_with_index {|pattern, i| if pattern == :* anchored = false else if anchored match = rest.rindex(pattern, 0) else match = rest.index(pattern) end if match post_match = $~ ? $~.post_match : rest[match+pattern.size..-1] else msg = message(msg) { expect_msg = "Expected #{mu_pp pattern}\n" if /\n[^\n]/ =~ rest actual_mesg = +"to match\n" rest.scan(/.*\n+/) { actual_mesg << ' ' << $&.inspect << "+\n" } actual_mesg.sub!(/\+\n\z/, '') else actual_mesg = "to match " + mu_pp(rest) end actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" expect_msg + actual_mesg } assert false, msg end rest = post_match anchored = true end } if anchored assert_equal("", rest) end end def assert_warning(pat, msg = nil) result = nil stderr = EnvUtil.with_default_internal(pat.encoding) { EnvUtil.verbose_warning { result = yield } } msg = message(msg) {diff pat, stderr} assert(pat === stderr, msg) result end def assert_warn(*args) assert_warning(*args) {$VERBOSE = false; yield} end def assert_deprecated_warning(mesg = /deprecated/) assert_warning(mesg) do Warning[:deprecated] = true if Warning.respond_to?(:[]=) yield end end def assert_deprecated_warn(mesg = /deprecated/) assert_warn(mesg) do Warning[:deprecated] = true if Warning.respond_to?(:[]=) yield end end class << (AssertFile = Struct.new(:failure_message).new) include Assertions include CoreAssertions def assert_file_predicate(predicate, *args) if /\Anot_/ =~ predicate predicate = $' neg = " not" end result = File.__send__(predicate, *args) result = !result if neg mesg = "Expected file ".dup << args.shift.inspect mesg << "#{neg} to be #{predicate}" mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? mesg << " #{failure_message}" if failure_message assert(result, mesg) end alias method_missing assert_file_predicate def for(message) clone.tap {|a| a.failure_message = message} end end class AllFailures attr_reader :failures def initialize @count = 0 @failures = {} end def for(key) @count += 1 yield key rescue Exception => e @failures[key] = [@count, e] end def foreach(*keys) keys.each do |key| @count += 1 begin yield key rescue Exception => e @failures[key] = [@count, e] end end end def message i = 0 total = @count.to_s fmt = "%#{total.size}d" @failures.map {|k, (n, v)| v = v.message "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" }.join("\n") end def pass? @failures.empty? end end # threads should respond to shift method. # Array can be used. def assert_join_threads(threads, message = nil) errs = [] values = [] while th = threads.shift begin values << th.value rescue Exception errs << [th, $!] th = nil end end values ensure if th&.alive? th.raise(Timeout::Error.new) th.join rescue errs << [th, $!] end if !errs.empty? msg = "exceptions on #{errs.length} threads:\n" + errs.map {|t, err| "#{t.inspect}:\n" + err.full_message(highlight: false, order: :top) }.join("\n---\n") if message msg = "#{message}\n#{msg}" end raise Test::Unit::AssertionFailedError, msg end end def assert_all?(obj, m = nil, &blk) failed = [] obj.each do |*a, &b| unless blk.call(*a, &b) failed << (a.size > 1 ? a : a[0]) end end assert(failed.empty?, message(m) {failed.pretty_inspect}) end def assert_all_assertions(msg = nil) all = AllFailures.new yield all ensure assert(all.pass?, message(msg) {all.message.chomp(".")}) end alias all_assertions assert_all_assertions def assert_all_assertions_foreach(msg = nil, *keys, &block) all = AllFailures.new all.foreach(*keys, &block) ensure assert(all.pass?, message(msg) {all.message.chomp(".")}) end alias all_assertions_foreach assert_all_assertions_foreach def diff(exp, act) require 'pp' q = PP.new(+"") q.guard_inspect_key do q.group(2, "expected: ") do q.pp exp end q.text q.newline q.group(2, "actual: ") do q.pp act end q.flush end q.output end def new_test_token token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" return token.dump, Regexp.quote(token) end end end end did_you_mean-1.6.3/test/test_spell_checker.rb0000644000004100000410000000603714353445451021371 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-1.6.3/test/spell_checking/0000755000004100000410000000000014353445451020146 5ustar www-datawww-datadid_you_mean-1.6.3/test/spell_checking/test_class_name_check.rb0000644000004100000410000000424114353445451024775 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-1.6.3/test/spell_checking/test_uncorrectable_name_check.rb0000644000004100000410000000051214353445451026515 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-1.6.3/test/spell_checking/test_pattern_key_name_check.rb0000644000004100000410000000112014353445451026206 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-1.6.3/test/spell_checking/test_variable_name_check.rb0000644000004100000410000000761614353445451025466 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-1.6.3/test/spell_checking/test_key_name_check.rb0000644000004100000410000000340414353445451024460 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-1.6.3/test/spell_checking/test_method_name_check.rb0000644000004100000410000001041614353445451025151 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-1.6.3/test/spell_checking/test_require_path_check.rb0000644000004100000410000000162114353445451025357 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-1.6.3/test/edit_distance/0000755000004100000410000000000014353445451017773 5ustar www-datawww-datadid_you_mean-1.6.3/test/edit_distance/test_jaro_winkler.rb0000644000004100000410000000261114353445451024045 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-1.6.3/README.md0000644000004100000410000001132414353445451015475 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-1.6.3/CHANGELOG.md0000644000004100000410000005542014353445451016034 0ustar www-datawww-data## 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-1.6.3/benchmark/0000755000004100000410000000000014353445451016147 5ustar www-datawww-datadid_you_mean-1.6.3/benchmark/speed.yml0000644000004100000410000000071214353445451017772 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-1.6.3/benchmark/jaro_winkler/0000755000004100000410000000000014353445451020635 5ustar www-datawww-datadid_you_mean-1.6.3/benchmark/jaro_winkler/memory_usage.rb0000644000004100000410000000036214353445451023657 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-1.6.3/benchmark/jaro_winkler/speed.rb0000644000004100000410000000111514353445451022260 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-1.6.3/benchmark/levenshtein/0000755000004100000410000000000014353445451020473 5ustar www-datawww-datadid_you_mean-1.6.3/benchmark/levenshtein/memory_usage.rb0000644000004100000410000000037014353445451023514 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-1.6.3/benchmark/levenshtein/speed.rb0000644000004100000410000000113214353445451022115 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-1.6.3/benchmark/memory_usage.rb0000644000004100000410000000063214353445451021171 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-1.6.3/benchmark/require_path_checker.rb0000644000004100000410000000225414353445451022653 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-1.6.3/.gitignore0000644000004100000410000000027514353445451016211 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-1.6.3/did_you_mean.gemspec0000644000004100000410000000173114353445451020220 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' spec.add_development_dependency "rake" end did_you_mean-1.6.3/Rakefile0000644000004100000410000000454214353445451015667 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) task :sync_tool do require 'fileutils' FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib" FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" end 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-1.6.3/lib/0000755000004100000410000000000014353445451014763 5ustar www-datawww-datadid_you_mean-1.6.3/lib/did_you_mean.rb0000644000004100000410000001250014353445451017742 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) # TODO: Remove on 3.3: class DeprecatedMapping # :nodoc: def []=(key, value) warn "Calling `DidYouMean::SPELL_CHECKERS[#{key.to_s}] = #{value.to_s}' has been deprecated. " \ "Please call `DidYouMean.correct_error(#{key.to_s}, #{value.to_s})' instead." DidYouMean.correct_error(key, value) end def merge!(hash) warn "Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. " \ "Please call `DidYouMean.correct_error(error_name, spell_checker)' instead." hash.each do |error_class, spell_checker| DidYouMean.correct_error(error_class, spell_checker) end end end # TODO: Remove on 3.3: SPELL_CHECKERS = DeprecatedMapping.new deprecate_constant :SPELL_CHECKERS private_constant :DeprecatedMapping # Returns the currently set formatter. By default, it is set to +DidYouMean::Formatter+. def self.formatter if defined?(Ractor) Ractor.current[:__did_you_mean_formatter__] || Formatter else Formatter end end # Updates the primary formatter used to format the suggestions. def self.formatter=(formatter) if defined?(Ractor) Ractor.current[:__did_you_mean_formatter__] = formatter end end end did_you_mean-1.6.3/lib/did_you_mean/0000755000004100000410000000000014353445451017417 5ustar www-datawww-datadid_you_mean-1.6.3/lib/did_you_mean/core_ext/0000755000004100000410000000000014353445451021227 5ustar www-datawww-datadid_you_mean-1.6.3/lib/did_you_mean/core_ext/name_error.rb0000644000004100000410000000242014353445451023703 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-1.6.3/lib/did_you_mean/spell_checkers/0000755000004100000410000000000014353445451022405 5ustar www-datawww-datadid_you_mean-1.6.3/lib/did_you_mean/spell_checkers/name_error_checkers/0000755000004100000410000000000014353445451026405 5ustar www-datawww-datadid_you_mean-1.6.3/lib/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb0000644000004100000410000000411514353445451033204 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-1.6.3/lib/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb0000644000004100000410000000230014353445451032516 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-1.6.3/lib/did_you_mean/spell_checkers/name_error_checkers.rb0000644000004100000410000000106714353445451026736 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-1.6.3/lib/did_you_mean/spell_checkers/pattern_key_name_checker.rb0000644000004100000410000000103314353445451027740 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(&:inspect) end end end did_you_mean-1.6.3/lib/did_you_mean/spell_checkers/null_checker.rb0000644000004100000410000000015014353445451025364 0ustar www-datawww-datamodule DidYouMean class NullChecker def initialize(*); end def corrections; [] end end end did_you_mean-1.6.3/lib/did_you_mean/spell_checkers/key_error_checker.rb0000644000004100000410000000073214353445451026421 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(&:inspect) end end end did_you_mean-1.6.3/lib/did_you_mean/spell_checkers/method_name_checker.rb0000644000004100000410000000434514353445451026704 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-1.6.3/lib/did_you_mean/spell_checkers/require_path_checker.rb0000644000004100000410000000235614353445451027114 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-1.6.3/lib/did_you_mean/formatter.rb0000644000004100000410000000243014353445451021746 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-1.6.3/lib/did_you_mean/formatters/0000755000004100000410000000000014353445451021605 5ustar www-datawww-datadid_you_mean-1.6.3/lib/did_you_mean/formatters/plain_formatter.rb0000644000004100000410000000025014353445451025315 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-1.6.3/lib/did_you_mean/formatters/verbose_formatter.rb0000644000004100000410000000040214353445451025656 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-1.6.3/lib/did_you_mean/version.rb0000644000004100000410000000006114353445451021426 0ustar www-datawww-datamodule DidYouMean VERSION = "1.6.3".freeze end did_you_mean-1.6.3/lib/did_you_mean/verbose.rb0000644000004100000410000000021114353445451021403 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-1.6.3/lib/did_you_mean/tree_spell_checker.rb0000644000004100000410000000547114353445451023575 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-1.6.3/lib/did_you_mean/experimental.rb0000644000004100000410000000021314353445451022435 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-1.6.3/lib/did_you_mean/spell_checker.rb0000644000004100000410000000242114353445451022546 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-1.6.3/lib/did_you_mean/levenshtein.rb0000644000004100000410000000253714353445451022277 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-1.6.3/lib/did_you_mean/jaro_winkler.rb0000644000004100000410000000345114353445451022435 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 / 2).floor - 1 range = 0 if range < 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 i = 0 str1.each_codepoint do |char1| char1 == codepoints2[i] && i < 4 ? prefix_bonus += 1 : break i += 1 end jaro_distance + (prefix_bonus * WEIGHT * (1 - jaro_distance)) else jaro_distance end end end end did_you_mean-1.6.3/Gemfile0000644000004100000410000000032214353445451015505 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' did_you_mean-1.6.3/appveyor.yml0000644000004100000410000000066114353445451016610 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 test_script: - rake did_you_mean-1.6.3/.github/0000755000004100000410000000000014353445451015555 5ustar www-datawww-datadid_you_mean-1.6.3/.github/dependabot.yml0000644000004100000410000000016614353445451020410 0ustar www-datawww-dataversion: 2 updates: - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'weekly' did_you_mean-1.6.3/.github/workflows/0000755000004100000410000000000014353445451017612 5ustar www-datawww-datadid_you_mean-1.6.3/.github/workflows/ruby.yml0000644000004100000410000000167014353445451021322 0ustar www-datawww-dataname: Ruby on: pull_request: branches: - 'master' push: branches: - 'master' jobs: build: runs-on: ubuntu-latest strategy: matrix: ruby: [ '2.5', '2.6', '2.7', '3.0', '3.1', 'ruby-head', 'jruby-9.2', 'jruby-9.3', 'jruby-head' ] steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Build and test with Rake run: | gem i test-unit RUBYOPT='--disable-did_you_mean' rake benchmark: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 3.1.3 - 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-1.6.3/documentation/0000755000004100000410000000000014353445451017066 5ustar www-datawww-datadid_you_mean-1.6.3/documentation/human_typo_api.md0000644000004100000410000000107314353445451022425 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-1.6.3/documentation/changelog_generator.rb0000644000004100000410000000156014353445451023412 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! did_you_mean-1.6.3/documentation/tree_spell_algorithm.md0000644000004100000410000001566314353445451023627 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-1.6.3/documentation/tree_spell_checker_api.md0000644000004100000410000000121014353445451024055 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-1.6.3/documentation/CHANGELOG.md.erb0000644000004100000410000000034614353445451021451 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-1.6.3/LICENSE.txt0000644000004100000410000000206414353445451016042 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.