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:
parent
a2fc6a51dd
commit
171803d5d3
Notes:
git
2019-12-01 11:08:56 +09:00
42 changed files with 2074 additions and 4 deletions
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
110
lib/did_you_mean.rb
Normal 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
|
25
lib/did_you_mean/core_ext/name_error.rb
Normal file
25
lib/did_you_mean/core_ext/name_error.rb
Normal 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
|
23
lib/did_you_mean/did_you_mean.gemspec
Normal file
23
lib/did_you_mean/did_you_mean.gemspec
Normal 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
|
2
lib/did_you_mean/experimental.rb
Normal file
2
lib/did_you_mean/experimental.rb
Normal 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."
|
20
lib/did_you_mean/experimental/initializer_name_correction.rb
Normal file
20
lib/did_you_mean/experimental/initializer_name_correction.rb
Normal 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
|
76
lib/did_you_mean/experimental/ivar_name_correction.rb
Normal file
76
lib/did_you_mean/experimental/ivar_name_correction.rb
Normal 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
|
33
lib/did_you_mean/formatters/plain_formatter.rb
Normal file
33
lib/did_you_mean/formatters/plain_formatter.rb
Normal 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
|
49
lib/did_you_mean/formatters/verbose_formatter.rb
Normal file
49
lib/did_you_mean/formatters/verbose_formatter.rb
Normal 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
|
87
lib/did_you_mean/jaro_winkler.rb
Normal file
87
lib/did_you_mean/jaro_winkler.rb
Normal 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
|
57
lib/did_you_mean/levenshtein.rb
Normal file
57
lib/did_you_mean/levenshtein.rb
Normal 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
|
46
lib/did_you_mean/spell_checker.rb
Normal file
46
lib/did_you_mean/spell_checker.rb
Normal 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
|
20
lib/did_you_mean/spell_checkers/key_error_checker.rb
Normal file
20
lib/did_you_mean/spell_checkers/key_error_checker.rb
Normal 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
|
56
lib/did_you_mean/spell_checkers/method_name_checker.rb
Normal file
56
lib/did_you_mean/spell_checkers/method_name_checker.rb
Normal 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
|
20
lib/did_you_mean/spell_checkers/name_error_checkers.rb
Normal file
20
lib/did_you_mean/spell_checkers/name_error_checkers.rb
Normal 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
|
|
@ -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
|
|
@ -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
|
6
lib/did_you_mean/spell_checkers/null_checker.rb
Normal file
6
lib/did_you_mean/spell_checkers/null_checker.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
module DidYouMean
|
||||
class NullChecker
|
||||
def initialize(*); end
|
||||
def corrections; [] end
|
||||
end
|
||||
end
|
137
lib/did_you_mean/tree_spell_checker.rb
Normal file
137
lib/did_you_mean/tree_spell_checker.rb
Normal 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
|
4
lib/did_you_mean/verbose.rb
Normal file
4
lib/did_you_mean/verbose.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
require_relative '../did_you_mean'
|
||||
require_relative 'formatters/verbose_formatter'
|
||||
|
||||
DidYouMean.formatter = DidYouMean::VerboseFormatter.new
|
3
lib/did_you_mean/version.rb
Normal file
3
lib/did_you_mean/version.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
module DidYouMean
|
||||
VERSION = "1.3.1"
|
||||
end
|
48
test/did_you_mean/core_ext/test_name_error_extension.rb
Normal file
48
test/did_you_mean/core_ext/test_name_error_extension.rb
Normal 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
|
36
test/did_you_mean/edit_distance/test_jaro_winkler.rb
Normal file
36
test/did_you_mean/edit_distance/test_jaro_winkler.rb
Normal 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
|
4
test/did_you_mean/fixtures/book.rb
Normal file
4
test/did_you_mean/fixtures/book.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class Book
|
||||
class Cover
|
||||
end
|
||||
end
|
15
test/did_you_mean/fixtures/mini_dir.yml
Normal file
15
test/did_you_mean/fixtures/mini_dir.yml
Normal 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
|
112
test/did_you_mean/fixtures/rspec_dir.yml
Normal file
112
test/did_you_mean/fixtures/rspec_dir.yml
Normal 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
|
29
test/did_you_mean/helper.rb
Normal file
29
test/did_you_mean/helper.rb
Normal 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
|
79
test/did_you_mean/spell_checking/test_class_name_check.rb
Normal file
79
test/did_you_mean/spell_checking/test_class_name_check.rb
Normal 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
|
54
test/did_you_mean/spell_checking/test_key_name_check.rb
Normal file
54
test/did_you_mean/spell_checking/test_key_name_check.rb
Normal 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
|
140
test/did_you_mean/spell_checking/test_method_name_check.rb
Normal file
140
test/did_you_mean/spell_checking/test_method_name_check.rb
Normal 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
|
|
@ -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
|
140
test/did_you_mean/spell_checking/test_variable_name_check.rb
Normal file
140
test/did_you_mean/spell_checking/test_variable_name_check.rb
Normal 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
|
77
test/did_you_mean/test_spell_checker.rb
Normal file
77
test/did_you_mean/test_spell_checker.rb
Normal 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
|
22
test/did_you_mean/test_verbose_formatter.rb
Normal file
22
test/did_you_mean/test_verbose_formatter.rb
Normal 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
|
61
test/did_you_mean/tree_spell/change_word.rb
Normal file
61
test/did_you_mean/tree_spell/change_word.rb
Normal 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
|
89
test/did_you_mean/tree_spell/human_typo.rb
Normal file
89
test/did_you_mean/tree_spell/human_typo.rb
Normal 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
|
38
test/did_you_mean/tree_spell/test_change_word.rb
Normal file
38
test/did_you_mean/tree_spell/test_change_word.rb
Normal 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
|
24
test/did_you_mean/tree_spell/test_human_typo.rb
Normal file
24
test/did_you_mean/tree_spell/test_human_typo.rb
Normal 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
|
173
test/did_you_mean/tree_spell_checker_test.rb
Normal file
173
test/did_you_mean/tree_spell_checker_test.rb
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue