Skip to content

Commit abb4543

Browse files
Change alias from dym to retry; address self PR review feedback
- Replace dym with retry as backup alias for fix command - Add public eval_path to Context (replace instance_variable_get) - Refactor fixable? to use class methods instead of allocate/send - Use gsub to fix all occurrences (e.g. foo.zeor? && bar.zeor?) - Clean up NoMatchingPatternKeyError check - Add MAX_EDIT_DISTANCE, LastError, and fix_apply_correction comments - Update PR description with all changed files remove descripiton md Add fix command tests, extract constants, match hint styling to error message
1 parent 123b75c commit abb4543

5 files changed

Lines changed: 175 additions & 66 deletions

File tree

lib/irb.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ def eval_input
234234
end
235235
handle_exception(exc)
236236
if Command::Fix.fixable?
237-
puts "\e[2mType `fix` to rerun with the correction.\e[0m"
237+
hint = Command::Fix::HINT
238+
puts Color.colorable? ? Color.colorize(hint, [:BOLD]) : hint
238239
end
239240
@context.workspace.local_variable_set(:_, exc)
240241
end

lib/irb/command/fix.rb

Lines changed: 79 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@
99
module IRB
1010
module Command
1111
class Fix < Base
12+
# Maximum Levenshtein distance for accepting a correction (did_you_mean default).
1213
MAX_EDIT_DISTANCE = 2
1314

15+
HINT = "Type `fix` to rerun with the correction."
16+
MSG_NO_PREVIOUS_ERROR = "No previous error with Did you mean? suggestions. Try making a typo first, e.g. 1.zeor?"
17+
MSG_NOT_CORRECTABLE = "Last error is not correctable. The fix command only works with NoMethodError, NameError, KeyError, etc."
18+
MSG_RERUNNING = "Rerunning with: %s"
19+
1420
category "did_you_mean"
1521
description "Rerun the previous command with corrected spelling from Did you mean?"
1622

@@ -19,12 +25,12 @@ def execute(_arg)
1925
exception = LastError.last_exception
2026

2127
if code.nil? || exception.nil?
22-
puts "No previous error with Did you mean? suggestions. Try making a typo first, e.g. 1.zeor?"
28+
puts MSG_NO_PREVIOUS_ERROR
2329
return
2430
end
2531

2632
unless correctable?(exception)
27-
puts "Last error is not correctable. The fix command only works with NoMethodError, NameError, KeyError, etc."
33+
puts MSG_NOT_CORRECTABLE
2834
return
2935
end
3036

@@ -34,8 +40,8 @@ def execute(_arg)
3440
corrected_code = apply_correction(code, wrong_str, correction)
3541
return unless corrected_code
3642

37-
puts "Rerunning with: #{corrected_code}"
38-
eval_path = @irb_context.instance_variable_get(:@eval_path) || "(irb)"
43+
puts format(MSG_RERUNNING, corrected_code)
44+
eval_path = @irb_context.eval_path || "(irb)"
3945
result = @irb_context.workspace.evaluate(corrected_code, eval_path, LastError.last_line_no)
4046
@irb_context.set_last_value(result)
4147
@irb_context.irb.output_value if @irb_context.echo?
@@ -44,88 +50,97 @@ def execute(_arg)
4450

4551
class << self
4652
def fixable?
47-
return false if LastError.last_code.nil? || LastError.last_exception.nil?
48-
cmd = allocate
49-
cmd.instance_variable_set(:@irb_context, nil)
50-
return false unless cmd.send(:correctable?, LastError.last_exception)
51-
wrong_str, correction = cmd.send(:extract_correction, LastError.last_exception)
53+
code = LastError.last_code
54+
exception = LastError.last_exception
55+
return false if code.nil? || exception.nil?
56+
return false unless fix_correctable?(exception)
57+
wrong_str, correction = fix_extract_correction(exception)
5258
return false if correction.nil?
53-
!cmd.send(:apply_correction, LastError.last_code, wrong_str, correction).nil?
59+
!fix_apply_correction(code, wrong_str, correction).nil?
5460
end
55-
end
5661

57-
private
62+
private
5863

59-
def correctable?(exception)
60-
exception.respond_to?(:corrections) && exception.is_a?(Exception)
61-
end
64+
def fix_correctable?(exception)
65+
exception.respond_to?(:corrections) && exception.is_a?(Exception)
66+
end
6267

63-
def extract_correction(exception)
64-
corrections = exception.corrections
65-
return [nil, nil] if corrections.nil? || corrections.empty?
68+
def fix_extract_correction(exception)
69+
corrections = exception.corrections
70+
return [nil, nil] if corrections.nil? || corrections.empty?
6671

67-
wrong_str = wrong_string_from(exception)
68-
return [nil, nil] if wrong_str.nil? || wrong_str.to_s.empty?
72+
wrong_str = fix_wrong_string_from(exception)
73+
return [nil, nil] if wrong_str.nil? || wrong_str.to_s.empty?
6974

70-
# Use did_you_mean's Levenshtein when available
71-
filtered = if defined?(DidYouMean::Levenshtein)
72-
corrections.select do |c|
73-
correction_str = c.is_a?(Array) ? c.first.to_s : c.to_s
74-
DidYouMean::Levenshtein.distance(normalize(wrong_str), normalize(correction_str)) <= MAX_EDIT_DISTANCE
75+
filtered = if defined?(DidYouMean::Levenshtein)
76+
corrections.select do |c|
77+
c_str = c.is_a?(Array) ? c.first.to_s : c.to_s
78+
DidYouMean::Levenshtein.distance(fix_normalize(wrong_str), fix_normalize(c_str)) <= MAX_EDIT_DISTANCE
79+
end
80+
else
81+
corrections
7582
end
76-
else
77-
corrections
83+
84+
return [nil, nil] unless filtered.size == 1
85+
86+
correction = filtered.first
87+
correction_str = correction.is_a?(Array) ? correction.first : correction
88+
[wrong_str.to_s, correction_str]
7889
end
7990

80-
return [nil, nil] unless filtered.size == 1
91+
def fix_wrong_string_from(exception)
92+
case exception
93+
when NoMethodError then exception.name.to_s
94+
when NameError then exception.name.to_s
95+
when KeyError then exception.key.to_s
96+
when LoadError then exception.message[/cannot load such file -- (.+)/, 1]
97+
else
98+
if defined?(NoMatchingPatternKeyError) && exception.is_a?(NoMatchingPatternKeyError)
99+
exception.key.to_s
100+
end
101+
end
102+
end
81103

82-
correction = filtered.first
83-
correction_str = correction.is_a?(Array) ? correction.first : correction
84-
[wrong_str.to_s, correction_str]
85-
end
104+
def fix_normalize(str)
105+
str.to_s.downcase
106+
end
86107

87-
def wrong_string_from(exception)
88-
case exception
89-
when NoMethodError
90-
exception.name.to_s
91-
when NameError
92-
exception.name.to_s
93-
when KeyError
94-
exception.key.to_s
95-
when defined?(NoMatchingPatternKeyError) && NoMatchingPatternKeyError
96-
exception.key.to_s
97-
when LoadError
98-
exception.message[/cannot load such file -- (.+)/, 1]
99-
else
108+
# Replaces wrong_str with correction in code. Uses gsub to fix all occurrences
109+
# (e.g. "foo.zeor? && bar.zeor?" both get corrected).
110+
def fix_apply_correction(code, wrong_str, correction_str)
111+
correction_display = correction_str.to_s
112+
patterns = [
113+
[wrong_str, correction_display],
114+
[":#{wrong_str}", ":#{correction_display}"],
115+
["\"#{wrong_str}\"", "\"#{correction_display}\""],
116+
["'#{wrong_str}'", "'#{correction_display}'"],
117+
]
118+
patterns.each do |wrong_pattern, correct_pattern|
119+
escaped = Regexp.escape(wrong_pattern)
120+
new_code = code.gsub(/#{escaped}/, correct_pattern)
121+
return new_code if new_code != code
122+
end
100123
nil
101124
end
102125
end
103126

104-
def normalize(str)
105-
str.to_s.downcase
127+
private
128+
129+
def correctable?(exception)
130+
self.class.send(:fix_correctable?, exception)
106131
end
107132

108-
def apply_correction(code, wrong_str, correction_str)
109-
correction_display = correction_str.to_s
110-
111-
patterns = [
112-
[wrong_str, correction_display],
113-
[":#{wrong_str}", ":#{correction_display}"],
114-
["\"#{wrong_str}\"", "\"#{correction_display}\""],
115-
["'#{wrong_str}'", "'#{correction_display}'"],
116-
]
117-
118-
patterns.each do |wrong_pattern, correct_pattern|
119-
escaped = Regexp.escape(wrong_pattern)
120-
new_code = code.sub(/#{escaped}/, correct_pattern)
121-
return new_code if new_code != code
122-
end
133+
def extract_correction(exception)
134+
self.class.send(:fix_extract_correction, exception)
135+
end
123136

124-
nil
137+
def apply_correction(code, wrong_str, correction_str)
138+
self.class.send(:fix_apply_correction, code, wrong_str, correction_str)
125139
end
126140
end
127141

128-
# Stores the last failed code and exception for the fix command
142+
# Stores the last failed code and exception for the fix command.
143+
# Not thread-safe; intended for single-threaded IRB sessions.
129144
module LastError
130145
@last_code = nil
131146
@last_exception = nil

lib/irb/context.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ def main
235235
# - the file path of the current IRB context in a binding.irb session
236236
attr_reader :irb_path
237237

238+
# Path used for evaluating code (e.g. in backtraces). Same as irb_path for
239+
# non-file contexts; for file paths, includes IRB name postfix.
240+
attr_reader :eval_path
241+
238242
# Sets @irb_path to the given +path+ as well as @eval_path
239243
# @eval_path is used for evaluating code in the context of IRB session
240244
# It's the same as irb_path, but with the IRB name postfix

lib/irb/default_commands.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def load_command(command)
253253

254254
_register_with_aliases(:irb_fix, Command::Fix,
255255
[:fix, NO_OVERRIDE],
256-
[:dym, NO_OVERRIDE]
256+
[:retry, NO_OVERRIDE]
257257
)
258258

259259
register(:cd, Command::CD)

test/irb/command/test_fix.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
require "irb"
4+
require_relative "../helper"
5+
6+
module TestIRB
7+
class FixCommandTest < TestCase
8+
def setup
9+
IRB::Command::LastError.clear
10+
end
11+
12+
def teardown
13+
IRB::Command::LastError.clear
14+
end
15+
16+
def test_fix_command_shows_hint_on_did_you_mean_error
17+
pend "did_you_mean is disabled" unless did_you_mean_available?
18+
19+
out, err = execute_lines("1.zeor?", "fix")
20+
21+
assert_match(/Did you mean\?\s+zero\?/, out)
22+
assert_match(/Type `fix` to rerun with the correction\./, out)
23+
assert_match(/Rerunning with: 1\.zero\?/, out)
24+
assert_match(/=> (true|false)/, out)
25+
assert_empty(err)
26+
end
27+
28+
def test_fix_command_reruns_with_correction
29+
pend "did_you_mean is disabled" unless did_you_mean_available?
30+
31+
out, err = execute_lines("1.zeor?", "fix")
32+
33+
assert_match(/Rerunning with: 1\.zero\?/, out)
34+
assert_empty(err)
35+
end
36+
37+
def test_fix_command_without_previous_error
38+
out, err = execute_lines("fix")
39+
40+
assert_match(/No previous error with Did you mean\? suggestions/, out)
41+
assert_empty(err)
42+
end
43+
44+
def test_fix_command_clears_after_success
45+
pend "did_you_mean is disabled" unless did_you_mean_available?
46+
47+
execute_lines("1.zeor?", "fix")
48+
out, err = execute_lines("fix")
49+
50+
assert_match(/No previous error with Did you mean\? suggestions/, out)
51+
assert_empty(err)
52+
end
53+
54+
def test_retry_alias_works
55+
pend "did_you_mean is disabled" unless did_you_mean_available?
56+
57+
out, err = execute_lines("1.zeor?", "retry")
58+
59+
assert_match(/Rerunning with: 1\.zero\?/, out)
60+
assert_empty(err)
61+
end
62+
63+
def test_last_error_stored_on_correctable_exception
64+
pend "did_you_mean is disabled" unless did_you_mean_available?
65+
66+
execute_lines("1.zeor?")
67+
68+
assert_not_nil(IRB::Command::LastError.last_code)
69+
assert_not_nil(IRB::Command::LastError.last_exception)
70+
assert_equal("1.zeor?", IRB::Command::LastError.last_code)
71+
end
72+
73+
def test_last_error_cleared_on_uncorrectable_exception
74+
execute_lines("raise 'oops'")
75+
76+
assert_nil(IRB::Command::LastError.last_code)
77+
assert_nil(IRB::Command::LastError.last_exception)
78+
end
79+
80+
private
81+
82+
def did_you_mean_available?
83+
return false unless defined?(DidYouMean)
84+
1.zeor?
85+
rescue NoMethodError => e
86+
e.respond_to?(:corrections) && !e.corrections.to_a.empty?
87+
end
88+
end
89+
end

0 commit comments

Comments
 (0)