99module 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
0 commit comments