1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

Promote did_you_mean to default gem

At the moment, there are some problems with regard to bundler + did_you_mean because of did_you_mean being a bundled gem. Since the vendored version of thor inside bundler and ruby itself explicitly requires did_you_mean, it can become difficult to load it when using Bundler.setup. See this issue: https://github.com/yuki24/did_you_mean/issues/117#issuecomment-482733159 for more details.
This commit is contained in:
Kevin Deisz 2019-10-29 10:08:37 -04:00 committed by Yuki Nishijima
parent a2fc6a51dd
commit 171803d5d3
Notes: git 2019-12-01 11:08:56 +09:00
42 changed files with 2074 additions and 4 deletions

View file

@ -162,6 +162,9 @@ Zachary Scott (zzak)
_unmaintained_
https://github.com/ruby/delegate
https://rubygems.org/gems/delegate
[lib/did_you_mean.rb]
Yuki Nishijima (yuki24)
https://github.com/ruby/did_you_mean
[lib/fileutils.rb]
_unmaintained_
https://github.com/ruby/fileutils
@ -342,8 +345,6 @@ Zachary Scott (zzak)
== Bundled gems upstream repositories
[did_you_mean]
https://github.com/yuki24/did_you_mean
[minitest]
https://github.com/seattlerb/minitest
[net-telnet]

View file

@ -62,6 +62,7 @@ Bundler:: Manage your Ruby application's gem dependencies
CGI:: Support for the Common Gateway Interface protocol
CSV:: Provides an interface to read and write CSV files and data
Delegator:: Provides three abilities to delegate method calls to an object
DidYouMean:: "Did you mean?" experience in Ruby
FileUtils:: Several file utility methods for copying, moving, removing, etc
Forwardable:: Provides delegation of specified methods to a designated object
GetoptLong:: Parse command line options similar to the GNU C getopt_long()
@ -112,7 +113,6 @@ Zlib:: Ruby interface for the zlib compression/decompression library
== Libraries
DidYouMean:: "Did you mean?" experience in Ruby
MiniTest:: A test suite with TDD, BDD, mocking and benchmarking
Net::Telnet:: Telnet client library for Ruby
PowerAssert:: Power Assert for Ruby.

View file

@ -1,4 +1,3 @@
did_you_mean 1.3.1 https://github.com/yuki24/did_you_mean
minitest 5.13.0 https://github.com/seattlerb/minitest
net-telnet 0.2.0 https://github.com/ruby/net-telnet
power_assert 1.1.5 https://github.com/k-tsj/power_assert

110
lib/did_you_mean.rb Normal file
View file

@ -0,0 +1,110 @@
require_relative "did_you_mean/version"
require_relative "did_you_mean/core_ext/name_error"
require_relative "did_you_mean/spell_checker"
require_relative 'did_you_mean/spell_checkers/name_error_checkers'
require_relative 'did_you_mean/spell_checkers/method_name_checker'
require_relative 'did_you_mean/spell_checkers/key_error_checker'
require_relative 'did_you_mean/spell_checkers/null_checker'
require_relative 'did_you_mean/formatters/plain_formatter'
require_relative 'did_you_mean/tree_spell_checker'
# The +DidYouMean+ gem adds functionality to suggest possible method/class
# names upon errors such as +NameError+ and +NoMethodError+. In Ruby 2.3 or
# later, it is automatically activated during startup.
#
# @example
#
# methosd
# # => NameError: undefined local variable or method `methosd' for main:Object
# # Did you mean? methods
# # method
#
# OBject
# # => NameError: uninitialized constant OBject
# # Did you mean? Object
#
# @full_name = "Yuki Nishijima"
# first_name, last_name = full_name.split(" ")
# # => NameError: undefined local variable or method `full_name' for main:Object
# # Did you mean? @full_name
#
# @@full_name = "Yuki Nishijima"
# @@full_anme
# # => NameError: uninitialized class variable @@full_anme in Object
# # Did you mean? @@full_name
#
# full_name = "Yuki Nishijima"
# full_name.starts_with?("Y")
# # => NoMethodError: undefined method `starts_with?' for "Yuki Nishijima":String
# # Did you mean? start_with?
#
# hash = {foo: 1, bar: 2, baz: 3}
# hash.fetch(:fooo)
# # => KeyError: key not found: :fooo
# # Did you mean? :foo
#
#
# == Disabling +did_you_mean+
#
# Occasionally, you may want to disable the +did_you_mean+ gem for e.g.
# debugging issues in the error object itself. You can disable it entirely by
# specifying +--disable-did_you_mean+ option to the +ruby+ command:
#
# $ ruby --disable-did_you_mean -e "1.zeor?"
# -e:1:in `<main>': undefined method `zeor?' for 1:Integer (NameError)
#
# When you do not have direct access to the +ruby+ command (e.g.
# +rails console+, +irb+), you could applyoptions using the +RUBYOPT+
# environment variable:
#
# $ RUBYOPT='--disable-did_you_mean' irb
# irb:0> 1.zeor?
# # => NoMethodError (undefined method `zeor?' for 1:Integer)
#
#
# == Getting the original error message
#
# Sometimes, you do not want to disable the gem entirely, but need to get the
# original error message without suggestions (e.g. testing). In this case, you
# could use the +#original_message+ method on the error object:
#
# no_method_error = begin
# 1.zeor?
# rescue NoMethodError => error
# error
# end
#
# no_method_error.message
# # => NoMethodError (undefined method `zeor?' for 1:Integer)
# # Did you mean? zero?
#
# no_method_error.original_message
# # => NoMethodError (undefined method `zeor?' for 1:Integer)
#
module DidYouMean
# Map of error types and spell checker objects.
SPELL_CHECKERS = Hash.new(NullChecker)
# Adds +DidYouMean+ functionality to an error using a given spell checker
def self.correct_error(error_class, spell_checker)
SPELL_CHECKERS[error_class.name] = spell_checker
error_class.prepend(Correctable) unless error_class < Correctable
end
correct_error NameError, NameErrorCheckers
correct_error KeyError, KeyErrorChecker
correct_error NoMethodError, MethodNameChecker
# Returns the currenctly set formatter. By default, it is set to +DidYouMean::Formatter+.
def self.formatter
@@formatter
end
# Updates the primary formatter used to format the suggestions.
def self.formatter=(formatter)
@@formatter = formatter
end
self.formatter = PlainFormatter.new
end

View file

@ -0,0 +1,25 @@
module DidYouMean
module Correctable
def original_message
method(:to_s).super_method.call
end
def to_s
msg = super.dup
suggestion = DidYouMean.formatter.message_for(corrections)
msg << suggestion if !msg.end_with?(suggestion)
msg
rescue
super
end
def corrections
@corrections ||= spell_checker.corrections
end
def spell_checker
SPELL_CHECKERS[self.class.to_s].new(self)
end
end
end

View file

@ -0,0 +1,23 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'did_you_mean/version'
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

View file

@ -0,0 +1,2 @@
warn "Experimental features in the did_you_mean gem has been removed " \
"and `require \"did_you_mean/experimental\"' has no effect."

View file

@ -0,0 +1,20 @@
# frozen-string-literal: true
require_relative '../levenshtein'
module DidYouMean
module Experimental
module InitializerNameCorrection
def method_added(name)
super
distance = Levenshtein.distance(name.to_s, 'initialize')
if distance != 0 && distance <= 2
warn "warning: #{name} might be misspelled, perhaps you meant initialize?"
end
end
end
::Class.prepend(InitializerNameCorrection)
end
end

View file

@ -0,0 +1,76 @@
# frozen-string-literal: true
require_relative '../../did_you_mean'
module DidYouMean
module Experimental #:nodoc:
class IvarNameCheckerBuilder #:nodoc:
attr_reader :original_checker
def initialize(original_checker) #:nodoc:
@original_checker = original_checker
end
def new(no_method_error) #:nodoc:
IvarNameChecker.new(no_method_error, original_checker: @original_checker)
end
end
class IvarNameChecker #:nodoc:
REPLS = {
"(irb)" => -> { Readline::HISTORY.to_a.last }
}
TRACE = TracePoint.trace(:raise) do |tp|
e = tp.raised_exception
if SPELL_CHECKERS.include?(e.class.to_s) && !e.instance_variable_defined?(:@frame_binding)
e.instance_variable_set(:@frame_binding, tp.binding)
end
end
attr_reader :original_checker
def initialize(no_method_error, original_checker: )
@original_checker = original_checker.new(no_method_error)
@location = no_method_error.backtrace_locations.first
@ivar_names = no_method_error.frame_binding.receiver.instance_variables
no_method_error.remove_instance_variable(:@frame_binding)
end
def corrections
original_checker.corrections + ivar_name_corrections
end
def ivar_name_corrections
@ivar_name_corrections ||= SpellChecker.new(dictionary: @ivar_names).correct(receiver_name.to_s)
end
private
def receiver_name
return unless @original_checker.receiver.nil?
abs_path = @location.absolute_path
lineno = @location.lineno
/@(\w+)*\.#{@original_checker.method_name}/ =~ line(abs_path, lineno).to_s && $1
end
def line(abs_path, lineno)
if REPLS[abs_path]
REPLS[abs_path].call
elsif File.exist?(abs_path)
File.open(abs_path) do |file|
file.detect { file.lineno == lineno }
end
end
end
end
end
NameError.send(:attr, :frame_binding)
SPELL_CHECKERS['NoMethodError'] = Experimental::IvarNameCheckerBuilder.new(SPELL_CHECKERS['NoMethodError'])
end

View file

@ -0,0 +1,33 @@
# frozen-string-literal: true
module DidYouMean
# The +DidYouMean::PlainFormatter+ is the basic, default formatter for the
# gem. The formatter responds to the +message_for+ method and it returns a
# human readable string.
class PlainFormatter
# 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::PlainFormatter.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 message_for(corrections)
corrections.empty? ? "" : "\nDid you mean? #{corrections.join("\n ")}"
end
end
end

View file

@ -0,0 +1,49 @@
# frozen-string-literal: true
module DidYouMean
# The +DidYouMean::VerboseFormatter+ uses extra empty lines to make the
# suggestion stand out more in the error message.
#
# In order to activate the verbose formatter,
#
# @example
#
# OBject
# # => NameError: uninitialized constant OBject
# # Did you mean? Object
#
# require 'did_you_mean/verbose'
#
# OBject
# # => NameError: uninitialized constant OBject
# #
# # Did you mean? Object
# #
#
class VerboseFormatter
# 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::PlainFormatter.new
#
# puts formatter.message_for(["methods", "method"])
#
#
# Did you mean? methods
# method
#
# # => nil
#
def message_for(corrections)
return "" if corrections.empty?
output = "\n\n Did you mean? ".dup
output << corrections.join("\n ")
output << "\n "
end
end
end

View file

@ -0,0 +1,87 @@
module 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

View file

@ -0,0 +1,57 @@
module 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

View file

@ -0,0 +1,46 @@
# frozen-string-literal: true
require_relative "levenshtein"
require_relative "jaro_winkler"
module DidYouMean
class SpellChecker
def initialize(dictionary:)
@dictionary = dictionary
end
def correct(input)
input = normalize(input)
threshold = input.length > 3 ? 0.834 : 0.77
words = @dictionary.select { |word| JaroWinkler.distance(normalize(word), input) >= threshold }
words.reject! { |word| input == word.to_s }
words.sort_by! { |word| JaroWinkler.distance(word.to_s, input) }
words.reverse!
# Correct mistypes
threshold = (input.length * 0.25).ceil
corrections = words.select { |c| Levenshtein.distance(normalize(c), input) <= threshold }
# Correct misspells
if corrections.empty?
corrections = words.select do |word|
word = normalize(word)
length = input.length < word.length ? input.length : word.length
Levenshtein.distance(word, 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

View file

@ -0,0 +1,20 @@
require_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

View file

@ -0,0 +1,56 @@
require_relative "../spell_checker"
module DidYouMean
class MethodNameChecker
attr_reader :method_name, :receiver
NAMES_TO_EXCLUDE = { NilClass => nil.methods }
NAMES_TO_EXCLUDE.default = []
# +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
)
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 ||= SpellChecker.new(dictionary: RB_RESERVED_WORDS + method_names).correct(method_name) - NAMES_TO_EXCLUDE[@receiver.class]
end
def method_names
method_names = receiver.methods + receiver.singleton_methods
method_names += receiver.private_methods if @private_call
method_names.uniq!
method_names
end
end
end

View file

@ -0,0 +1,20 @@
require_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

View file

@ -0,0 +1,50 @@
# frozen-string-literal: true
require 'delegate'
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 < SimpleDelegator
attr :namespace
def initialize(name, namespace = '')
super(name)
@namespace = namespace
end
def full_name
self.class.new("#{namespace}#{__getobj__}")
end
end
private_constant :ClassName
end
end

View file

@ -0,0 +1,82 @@
# 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 = []
# +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__
)
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) - NAMES_TO_EXCLUDE[@name]
end
end
end

View file

@ -0,0 +1,6 @@
module DidYouMean
class NullChecker
def initialize(*); end
def corrections; [] end
end
end

View file

@ -0,0 +1,137 @@
module DidYouMean
# spell checker for a dictionary that has a tree
# structure, see doc/tree_spell_checker_api.md
class TreeSpellChecker
attr_reader :dictionary, :dimensions, :separator, :augment
def initialize(dictionary:, separator: '/', augment: nil)
@dictionary = dictionary
@separator = separator
@augment = augment
@dimensions = parse_dimensions
end
def correct(input)
plausibles = plausible_dimensions input
return no_idea(input) if plausibles.empty?
suggestions = find_suggestions input, plausibles
return no_idea(input) if suggestions.empty?
suggestions
end
private
def parse_dimensions
ParseDimensions.new(dictionary, separator).call
end
def find_suggestions(input, plausibles)
states = plausibles[0].product(*plausibles[1..-1])
paths = possible_paths states
leaf = input.split(separator).last
ideas = find_ideas(paths, leaf)
ideas.compact.flatten
end
def no_idea(input)
return [] unless augment
::DidYouMean::SpellChecker.new(dictionary: dictionary).correct(input)
end
def find_ideas(paths, leaf)
paths.map do |path|
names = find_leaves(path)
ideas = CorrectElement.new.call names, leaf
ideas_to_paths ideas, leaf, names, path
end
end
def ideas_to_paths(ideas, leaf, names, path)
return nil if ideas.empty?
return [path + separator + leaf] if names.include? leaf
ideas.map { |str| path + separator + str }
end
def find_leaves(path)
dictionary.map do |str|
next unless str.include? "#{path}#{separator}"
str.gsub("#{path}#{separator}", '')
end.compact
end
def possible_paths(states)
states.map do |state|
state.join separator
end
end
def plausible_dimensions(input)
elements = input.split(separator)[0..-2]
elements.each_with_index.map do |element, i|
next if dimensions[i].nil?
CorrectElement.new.call dimensions[i], element
end.compact
end
end
# parses the elements in each dimension
class ParseDimensions
def initialize(dictionary, separator)
@dictionary = dictionary
@separator = separator
end
def call
leafless = remove_leaves
dimensions = find_elements leafless
dimensions.map do |elements|
elements.to_set.to_a
end
end
private
def remove_leaves
dictionary.map do |a|
elements = a.split(separator)
elements[0..-2]
end.to_set.to_a
end
def find_elements(leafless)
max_elements = leafless.map(&:size).max
dimensions = Array.new(max_elements) { [] }
(0...max_elements).each do |i|
leafless.each do |elements|
dimensions[i] << elements[i] unless elements[i].nil?
end
end
dimensions
end
attr_reader :dictionary, :separator
end
# identifies the elements close to element
class CorrectElement
def initialize
end
def call(names, element)
return names if names.size == 1
str = normalize element
return [str] if names.include? str
checker = ::DidYouMean::SpellChecker.new(dictionary: names)
checker.correct(str)
end
private
def normalize(leaf)
str = leaf.dup
str.downcase!
return str unless str.include? '@'
str.tr!('@', ' ')
end
end
end

View file

@ -0,0 +1,4 @@
require_relative '../did_you_mean'
require_relative 'formatters/verbose_formatter'
DidYouMean.formatter = DidYouMean::VerboseFormatter.new

View file

@ -0,0 +1,3 @@
module DidYouMean
VERSION = "1.3.1"
end

View file

@ -0,0 +1,48 @@
require_relative '../helper'
class NameErrorExtensionTest < Test::Unit::TestCase
SPELL_CHECKERS = DidYouMean::SPELL_CHECKERS
class TestSpellChecker
def initialize(*); end
def corrections; ["does_exist"]; end
end
def setup
@org, SPELL_CHECKERS['NameError'] = SPELL_CHECKERS['NameError'], TestSpellChecker
@error = assert_raise(NameError){ doesnt_exist }
end
def teardown
SPELL_CHECKERS['NameError'] = @org
end
def test_message
assert_match(/Did you mean\? does_exist/, @error.to_s)
assert_match(/Did you mean\? does_exist/, @error.message)
end
def test_to_s_does_not_make_disruptive_changes_to_error_message
error = assert_raise(NameError) do
raise NameError, "uninitialized constant Object"
end
error.to_s
assert_equal 1, error.to_s.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').sizee }
rescue NoMethodError => e
e
end
error.to_s
assert_equal "undefined method `sizee' for #<File:test_name_error_extension.rb>",
Marshal.load(Marshal.dump(error)).original_message
end
end

View file

@ -0,0 +1,36 @@
require_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

View file

@ -0,0 +1,4 @@
class Book
class Cover
end
end

View file

@ -0,0 +1,15 @@
---
- 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

View file

@ -0,0 +1,112 @@
---
- 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

View file

@ -0,0 +1,29 @@
require 'test/unit'
module DidYouMean
module TestHelper
class << self
attr_reader :root
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
end
end

View file

@ -0,0 +1,79 @@
require_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
error = assert_raise(NameError) { ::Book::Cover }
# 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
# attemps 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

View file

@ -0,0 +1,54 @@
require_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", error.to_s
error = assert_raise(KeyError) { hash.fetch("fooo") }
assert_correction %("foo"), error.corrections
assert_match %(Did you mean? "foo"), error.to_s
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", error.to_s
error = assert_raise(KeyError) { hash.fetch_values("foo", :bar, "fooo") }
assert_correction %("foo"), error.corrections
assert_match %(Did you mean? "foo"), error.to_s
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), error.to_s
end
def test_corrects_sprintf_key_name
error = assert_raise(KeyError) { sprintf("%<foo>d", {fooo: 1}) }
assert_correction ":fooo", error.corrections
assert_match "Did you mean? :fooo", error.to_s
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"), error.to_s
ensure
ENV.delete("FOO")
ENV.delete("BAR")
end
end

View file

@ -0,0 +1,140 @@
require_relative '../helper'
class MethodNameCheckTest < Test::Unit::TestCase
include DidYouMean::TestHelper
class User
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", error.to_s
end
def test_corrections_include_private_method
error = assert_raise(NoMethodError){ @user.friend }
assert_correction :friends, error.corrections
assert_match "Did you mean? friends", error.to_s
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", error.to_s
end
def test_corrections_include_class_method
error = assert_raise(NoMethodError){ User.l0ad }
assert_correction :load, error.corrections
assert_match "Did you mean? load", error.to_s
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", error.to_s
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, error.to_s.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, error.to_s.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, error.to_s.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", error.to_s
end
end

View file

@ -0,0 +1,15 @@
require_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_equal "Other name error", @error.message
end
end

View file

@ -0,0 +1,140 @@
require_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", error.to_s
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", error.to_s
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", error.to_s
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", false_error.to_s
assert_correction :true, true_error.corrections
assert_match "Did you mean? true", true_error.to_s
assert_correction :nil, nil_error.corrections
assert_match "Did you mean? nil", nil_error.to_s
assert_correction :__FILE__, file_error.corrections
assert_match "Did you mean? __FILE__", file_error.to_s
end
def test_suggests_yield
error = assert_raise(NameError) { yeild }
assert_correction :yield, error.corrections
assert_match "Did you mean? yield", error.to_s
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", error.to_s
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", error.to_s
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", error.to_s
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", error.to_s
end
def test_exclude_typical_incorrect_suggestions
error = assert_raise(NameError){ foo }
assert_empty error.corrections
end
end

View file

@ -0,0 +1,77 @@
require_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 %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

View file

@ -0,0 +1,22 @@
require_relative './helper'
class VerboseFormatterTest < Test::Unit::TestCase
def setup
require_relative File.join(DidYouMean::TestHelper.root, 'verbose')
end
def teardown
DidYouMean.formatter = DidYouMean::PlainFormatter.new
end
def test_message
@error = assert_raise(NoMethodError){ 1.zeor? }
assert_equal <<~MESSAGE.chomp, @error.message
undefined method `zeor?' for 1:Integer
Did you mean? zero?
MESSAGE
end
end

View file

@ -0,0 +1,61 @@
module 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

View file

@ -0,0 +1,89 @@
# 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
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_type
@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 rand_char
popular_chars = alphabetic_characters + special_characters
n = popular_chars.length
popular_chars[rand(n)]
end
def alphabetic_characters
('a'..'z').to_a.join + ('A'..'Z').to_a.join
end
def special_characters
'?<>,.!`+=-_":;@#$%^&*()'
end
def toss
return +1 if rand >= 0.5
-1
end
def action_type
[:insert, :transpose, :delete, :substitute][rand(4)]
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, rand_char)
when :substitute
cw.substitution(i_place, rand_char)
when :transpose
cw.transposition(i_place, toss)
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

View file

@ -0,0 +1,38 @@
require_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_deleletion
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

View file

@ -0,0 +1,24 @@
require_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

View file

@ -0,0 +1,173 @@
require 'set'
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'
@tsp = DidYouMean::TreeSpellChecker.new(dictionary: @dictionary)
end
def test_corrupt_root
word = 'test/verbose_formatter_test.rb'
word_error = 'btets/cverbose_formatter_etst.rb suggestions'
tsp = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES)
s = tsp.correct(word_error).first
assert_match s, word
end
def test_leafless_state
tsp = DidYouMean::TreeSpellChecker.new(dictionary: @dictionary.push('spec/features'))
word = 'spec/modals/confirms/efgh_spec.rb'
word_error = 'spec/modals/confirXX/efgh_spec.rb'
s = tsp.correct(word_error).first
assert_equal s, word
s = tsp.correct('spec/featuresXX')
assert_equal 'spec/features', s.first
end
def test_rake_dictionary
dict = %w(parallel:prepare parallel:create parallel:rake parallel:migrate)
word_error = 'parallel:preprare'
tsp = DidYouMean::TreeSpellChecker.new(dictionary: dict, separator: ':')
s = tsp.correct(word_error).first
assert_match s, 'parallel:prepare'
end
def test_special_words_mini
tsp = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES)
special_words_mini.each do |word, word_error|
s = tsp.correct(word_error).first
assert_match s, word
end
end
def test_special_words_rspec
tsp = DidYouMean::TreeSpellChecker.new(dictionary: RSPEC_DIRECTORIES)
special_words_rspec.each do |word, word_error|
s = tsp.correct(word_error)
assert_match s.first, word
end
end
def special_words_rspec
[
['spec/rspec/core/formatters/exception_presenter_spec.rb','spec/rspec/core/formatters/eception_presenter_spec.rb'],
['spec/rspec/core/ordering_spec.rb', 'spec/spec/core/odrering_spec.rb'],
['spec/rspec/core/metadata_spec.rb', 'spec/rspec/core/metadata_spe.crb'],
['spec/support/mathn_integration_support.rb', 'spec/support/mathn_itegrtion_support.rb']
]
end
def special_words_mini
[
['test/fixtures/book.rb', 'test/fixture/book.rb'],
['test/fixtures/book.rb', 'test/fixture/book.rb'],
['test/edit_distance/jaro_winkler_test.rb', 'test/edit_distace/jaro_winkler_test.rb'],
['test/edit_distance/jaro_winkler_test.rb', 'teste/dit_distane/jaro_winkler_test.rb'],
['test/fixtures/book.rb', 'test/fixturWes/book.rb'],
['test/test_helper.rb', 'tes!t/test_helper.rb'],
['test/fixtures/book.rb', 'test/hfixtures/book.rb'],
['test/edit_distance/jaro_winkler_test.rb', 'test/eidt_distance/jaro_winkler_test.@rb'],
['test/spell_checker_test.rb', 'test/spell_checke@r_test.rb'],
['test/tree_spell_human_typo_test.rb', 'testt/ree_spell_human_typo_test.rb'],
['test/spell_checking/variable_name_check_test.rb', 'test/spell_checking/vriabl_ename_check_test.rb'],
['test/spell_checking/key_name_check_test.rb', 'tesit/spell_checking/key_name_choeck_test.rb'],
['test/edit_distance/jaro_winkler_test.rb', 'test/edit_distance/jaro_winkler_tuest.rb']
]
end
def test_file_in_root
word = 'test/spell_checker_test.rb'
word_error = 'test/spell_checker_test.r'
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES).correct word_error
assert_equal word, suggestions.first
end
def test_no_plausible_states
word_error = 'testspell_checker_test.rb'
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES).correct word_error
assert_equal [], suggestions
end
def test_no_plausible_states_with_augmentation
word_error = 'testspell_checker_test.rb'
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES).correct word_error
assert_equal [], suggestions
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES, augment: true).correct word_error
assert_equal 'test/spell_checker_test.rb', suggestions.first
end
def test_no_idea_with_augmentation
word_error = 'test/spell_checking/key_name.rb'
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES).correct word_error
assert_equal [], suggestions
suggestions = DidYouMean::TreeSpellChecker.new(dictionary: MINI_DIRECTORIES, augment: true).correct word_error
assert_equal 'test/spell_checking/key_name_check_test.rb', suggestions.first
end
def test_works_out_suggestions
exp = ['spec/models/concerns/vixen_spec.rb',
'spec/models/concerns/vixenus_spec.rb']
suggestions = @tsp.correct(@test_str)
assert_equal suggestions.to_set, exp.to_set
end
def test_works_when_input_is_correct
correct_input = 'spec/models/concerns/vixenus_spec.rb'
suggestions = @tsp.correct correct_input
assert_equal suggestions.first, correct_input
end
def test_find_out_leaves_in_a_path
path = 'spec/modals/confirms'
names = @tsp.send(:find_leaves, path)
assert_equal names.to_set, %w(abcd_spec.rb efgh_spec.rb).to_set
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'].to_set
states = @tsp.send(:parse_dimensions)
nodes = states[0].product(*states[1..-1])
paths = @tsp.send(:possible_paths, nodes)
assert_equal paths.to_set, exp_paths.to_set
end
def test_works_out_state_space
suggestions = @tsp.send(:plausible_dimensions, @test_str)
assert_equal suggestions, [["spec"], ["models", "modals"], ["confirms", "concerns"]]
end
def test_parses_dictionary
states = @tsp.send(:parse_dimensions)
assert_equal states, [["spec"], ["models", "modals", "controllers"], ["concerns", "confirms"]]
end
def test_parses_elementary_dictionary
dictionary = ['spec/models/user_spec.rb', 'spec/services/account_spec.rb']
tsp = DidYouMean::TreeSpellChecker.new(dictionary: dictionary)
states = tsp.send(:parse_dimensions)
assert_equal states, [['spec'], ['models', 'services']]
end
end

View file

@ -48,6 +48,7 @@
# * https://github.com/ruby/yaml
# * https://github.com/ruby/uri
# * https://github.com/ruby/openssl
# * https://github.com/ruby/did_you_mean
#
require 'fileutils'
@ -102,6 +103,7 @@ $repositories = {
yaml: "ruby/yaml",
uri: "ruby/uri",
openssl: "ruby/openssl",
did_you_mean: "ruby/did_you_mean"
}
def sync_default_gems(gem)
@ -262,6 +264,12 @@ def sync_default_gems(gem)
when "readlineext"
sync_lib "readline-ext"
mv "lib/readline-ext.gemspec", "ext/readline"
when "did_you_mean"
rm_rf(%w[lib/did_you_mean* test/did_you_mean])
cp_r(Dir.glob("#{upstream}/lib/did_you_mean*"), "lib")
cp_r("#{upstream}/did_you_mean.gemspec", "lib/did_you_mean")
cp_r("#{upstream}/test", "test/did_you_mean")
rm_rf(%w[test/did_you_mean/tree_spell/test_explore.rb])
else
sync_lib gem
end