Skip to content

Commit

Permalink
Sync did_you_mean
Browse files Browse the repository at this point in the history
  • Loading branch information
yuki24 committed Jun 6, 2020
1 parent 0c00a41 commit e5f5446
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 246 deletions.
4 changes: 3 additions & 1 deletion lib/did_you_mean.rb
Expand Up @@ -6,6 +6,7 @@
require_relative 'did_you_mean/spell_checkers/method_name_checker'
require_relative 'did_you_mean/spell_checkers/key_error_checker'
require_relative 'did_you_mean/spell_checkers/null_checker'
require_relative 'did_you_mean/spell_checkers/require_path_checker'
require_relative 'did_you_mean/formatters/plain_formatter'
require_relative 'did_you_mean/tree_spell_checker'

Expand Down Expand Up @@ -95,8 +96,9 @@ def self.correct_error(error_class, spell_checker)
correct_error NameError, NameErrorCheckers
correct_error KeyError, KeyErrorChecker
correct_error NoMethodError, MethodNameChecker
correct_error LoadError, RequirePathChecker if RUBY_VERSION >= '2.8.0'

# Returns the currenctly set formatter. By default, it is set to +DidYouMean::Formatter+.
# Returns the currently set formatter. By default, it is set to +DidYouMean::Formatter+.
def self.formatter
@@formatter
end
Expand Down
39 changes: 14 additions & 25 deletions lib/did_you_mean/experimental/ivar_name_correction.rb
@@ -1,22 +1,11 @@
# frozen-string-literal: true

require_relative '../../did_you_mean'
require_relative '../../did_you_mean/spell_checker'
require_relative '../../did_you_mean/spell_checkers/method_name_checker'

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:
class IvarNameChecker < ::DidYouMean::MethodNameChecker #:nodoc:
REPLS = {
"(irb)" => -> { Readline::HISTORY.to_a.last }
}
Expand All @@ -29,10 +18,10 @@ class IvarNameChecker #:nodoc:
end
end

attr_reader :original_checker
attr_reader :location, :ivar_names

def initialize(no_method_error, original_checker: )
@original_checker = original_checker.new(no_method_error)
def initialize(no_method_error)
super(no_method_error)

@location = no_method_error.backtrace_locations.first
@ivar_names = no_method_error.frame_binding.receiver.instance_variables
Expand All @@ -41,22 +30,22 @@ def initialize(no_method_error, original_checker: )
end

def corrections
original_checker.corrections + ivar_name_corrections
super + ivar_name_corrections
end

def ivar_name_corrections
@ivar_name_corrections ||= SpellChecker.new(dictionary: @ivar_names).correct(receiver_name.to_s)
@ivar_name_corrections ||= SpellChecker.new(dictionary: ivar_names).correct(receiver_name.to_s)
end

private

def receiver_name
return unless @original_checker.receiver.nil?
return unless receiver.nil?

abs_path = @location.absolute_path
lineno = @location.lineno
abs_path = location.absolute_path
lineno = location.lineno

/@(\w+)*\.#{@original_checker.method_name}/ =~ line(abs_path, lineno).to_s && $1
/@(\w+)*\.#{method_name}/ =~ line(abs_path, lineno).to_s && $1
end

def line(abs_path, lineno)
Expand All @@ -71,6 +60,6 @@ def line(abs_path, lineno)
end
end

NameError.send(:attr, :frame_binding)
SPELL_CHECKERS['NoMethodError'] = Experimental::IvarNameCheckerBuilder.new(SPELL_CHECKERS['NoMethodError'])
NoMethodError.send(:attr, :frame_binding)
SPELL_CHECKERS['NoMethodError'] = Experimental::IvarNameChecker
end
7 changes: 6 additions & 1 deletion lib/did_you_mean/spell_checkers/method_name_checker.rb
Expand Up @@ -43,7 +43,12 @@ def initialize(exception)
end

def corrections
@corrections ||= SpellChecker.new(dictionary: RB_RESERVED_WORDS + method_names).correct(method_name) - names_to_exclude
@corrections ||= begin
dictionary = method_names
dictionary = RB_RESERVED_WORDS + dictionary if @private_call

SpellChecker.new(dictionary: dictionary).correct(method_name) - names_to_exclude
end
end

def method_names
Expand Down
35 changes: 35 additions & 0 deletions lib/did_you_mean/spell_checkers/require_path_checker.rb
@@ -0,0 +1,35 @@
# frozen-string-literal: true

require_relative "../spell_checker"
require_relative "../tree_spell_checker"

module DidYouMean
class RequirePathChecker
attr_reader :path

INITIAL_LOAD_PATH = $LOAD_PATH.dup.freeze
ENV_SPECIFIC_EXT = ".#{RbConfig::CONFIG["DLEXT"]}"

private_constant :INITIAL_LOAD_PATH, :ENV_SPECIFIC_EXT

def self.requireables
@requireables ||= INITIAL_LOAD_PATH
.flat_map {|path| Dir.glob("**/???*{.rb,#{ENV_SPECIFIC_EXT}}", base: path) }
.map {|path| path.chomp!(".rb") || path.chomp!(ENV_SPECIFIC_EXT) }
end

def initialize(exception)
@path = exception.path
end

def corrections
@corrections ||= begin
threshold = path.size * 2
dictionary = self.class.requireables.reject {|str| str.size >= threshold }
spell_checker = path.include?("/") ? TreeSpellChecker : SpellChecker

spell_checker.new(dictionary: dictionary).correct(path).uniq
end
end
end
end
150 changes: 61 additions & 89 deletions lib/did_you_mean/tree_spell_checker.rb
@@ -1,137 +1,109 @@
# frozen_string_literal: true

module DidYouMean
# spell checker for a dictionary that has a tree
# structure, see doc/tree_spell_checker_api.md
class TreeSpellChecker
attr_reader :dictionary, :dimensions, :separator, :augment
attr_reader :dictionary, :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
plausibles = plausible_dimensions(input)
return fall_back_to_normal_spell_check(input) if plausibles.empty?

private
suggestions = find_suggestions(input, plausibles)
return fall_back_to_normal_spell_check(input) if suggestions.empty?

def parse_dimensions
ParseDimensions.new(dictionary, separator).call
suggestions
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
def dictionary_without_leaves
@dictionary_without_leaves ||= dictionary.map { |word| word.split(separator)[0..-2] }.uniq
end

def no_idea(input)
return [] unless augment
::DidYouMean::SpellChecker.new(dictionary: dictionary).correct(input)
def tree_depth
@tree_depth ||= dictionary_without_leaves.max { |a, b| a.size <=> b.size }.size
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
def dimensions
@dimensions ||= tree_depth.times.map do |index|
dictionary_without_leaves.map { |element| element[index] }.compact.uniq
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 }
def find_leaves(path)
path_with_separator = "#{path}#{separator}"

dictionary
.select {|str| str.include?(path_with_separator) }
.map {|str| str.gsub(path_with_separator, '') }
end

def find_leaves(path)
dictionary.map do |str|
next unless str.include? "#{path}#{separator}"
str.gsub("#{path}#{separator}", '')
end.compact
def plausible_dimensions(input)
input.split(separator)[0..-2]
.map
.with_index { |element, index| correct_element(dimensions[index], element) if dimensions[index] }
.compact
end

def possible_paths(states)
states.map do |state|
state.join separator
end
states.map { |state| state.join(separator) }
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
private

# parses the elements in each dimension
class ParseDimensions
def initialize(dictionary, separator)
@dictionary = dictionary
@separator = separator
def find_suggestions(input, plausibles)
states = plausibles[0].product(*plausibles[1..-1])
paths = possible_paths(states)
leaf = input.split(separator).last

find_ideas(paths, leaf)
end

def call
leafless = remove_leaves
dimensions = find_elements leafless
dimensions.map do |elements|
elements.to_set.to_a
end
def fall_back_to_normal_spell_check(input)
return [] unless augment

::DidYouMean::SpellChecker.new(dictionary: dictionary).correct(input)
end

private
def find_ideas(paths, leaf)
paths.flat_map do |path|
names = find_leaves(path)
ideas = correct_element(names, leaf)

def remove_leaves
dictionary.map do |a|
elements = a.split(separator)
elements[0..-2]
end.to_set.to_a
ideas_to_paths(ideas, leaf, names, path)
end.compact
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
def ideas_to_paths(ideas, leaf, names, path)
if ideas.empty?
nil
elsif names.include?(leaf)
["#{path}#{separator}#{leaf}"]
else
ideas.map {|str| "#{path}#{separator}#{str}" }
end
dimensions
end

attr_reader :dictionary, :separator
end
def correct_element(names, element)
return names if names.size == 1

# identifies the elements close to element
class CorrectElement
def initialize
end
str = normalize(element)

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
return [str] if names.include?(str)

private
::DidYouMean::SpellChecker.new(dictionary: names).correct(str)
end

def normalize(leaf)
str = leaf.dup
def normalize(str)
str.downcase!
return str unless str.include? '@'
str.tr!('@', ' ')
str.tr!('@', ' ') if str.include?('@')
str
end
end
end
7 changes: 7 additions & 0 deletions test/did_you_mean/spell_checking/test_method_name_check.rb
Expand Up @@ -137,4 +137,11 @@ def test_suggests_yield
assert_correction :yield, error.corrections
assert_match "Did you mean? yield", error.to_s
end

def test_does_not_suggest_yield
error = assert_raise(NoMethodError) { 1.yeild }

assert_correction [], error.corrections
assert_not_match(/Did you mean\? +yield/, error.to_s)
end if RUBY_ENGINE != "jruby"
end
32 changes: 32 additions & 0 deletions test/did_you_mean/spell_checking/test_require_path_check.rb
@@ -0,0 +1,32 @@
require_relative '../helper'

return if !(RUBY_VERSION >= '2.8.0')

class RequirePathCheckTest < Test::Unit::TestCase
include DidYouMean::TestHelper

def test_load_error_from_require_has_suggestions
error = assert_raise LoadError do
require 'open_struct'
end

assert_correction 'ostruct', error.corrections
assert_match "Did you mean? ostruct", error.to_s
end

def test_load_error_from_require_for_nested_files_has_suggestions
error = assert_raise LoadError do
require 'net/htt'
end

assert_correction 'net/http', error.corrections
assert_match "Did you mean? net/http", error.to_s

error = assert_raise LoadError do
require 'net-http'
end

assert_correction ['net/http', 'net/https'], error.corrections
assert_match "Did you mean? net/http", error.to_s
end
end

0 comments on commit e5f5446

Please sign in to comment.