Skip to content

Commit

Permalink
Promote did_you_mean to default gem
Browse files Browse the repository at this point in the history
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: ruby/did_you_mean#117 (comment) for more details.
  • Loading branch information
kddnewton authored and yuki24 committed Nov 30, 2019
1 parent f8cc05d commit 3df0328
Show file tree
Hide file tree
Showing 42 changed files with 2,074 additions and 4 deletions.
5 changes: 3 additions & 2 deletions doc/maintainers.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion doc/standard_library.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion gems/bundled_gems
Original file line number Diff line number Diff line change
@@ -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
Expand Down
110 changes: 110 additions & 0 deletions lib/did_you_mean.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions lib/did_you_mean/core_ext/name_error.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 23 additions & 0 deletions lib/did_you_mean/did_you_mean.gemspec
Original file line number Diff line number Diff line change
@@ -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 = ["[email protected]"]
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 changes: 2 additions & 0 deletions lib/did_you_mean/experimental.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions lib/did_you_mean/experimental/initializer_name_correction.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 76 additions & 0 deletions lib/did_you_mean/experimental/ivar_name_correction.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions lib/did_you_mean/formatters/plain_formatter.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 49 additions & 0 deletions lib/did_you_mean/formatters/verbose_formatter.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 3df0328

Please sign in to comment.