to_robust 1.0.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.md +39 -0
- data/lib/global/fix.rb +72 -0
- data/lib/global/fix/add_atom.rb +28 -0
- data/lib/global/fix/atom.rb +51 -0
- data/lib/global/fix/init.rb +4 -0
- data/lib/global/fix/remove_atom.rb +16 -0
- data/lib/global/fix/swap_atom.rb +26 -0
- data/lib/global/global.rb +155 -0
- data/lib/global/init.rb +10 -0
- data/lib/global/kernel/argument_error.rb +22 -0
- data/lib/global/kernel/exception.rb +9 -0
- data/lib/global/kernel/init.rb +4 -0
- data/lib/global/kernel/no_method_error.rb +15 -0
- data/lib/global/kernel/zero_division_error.rb +21 -0
- data/lib/global/report.rb +42 -0
- data/lib/global/robust_proc.rb +48 -0
- data/lib/global/strategies/divide_by_zero_error_strategy.rb +277 -0
- data/lib/global/strategies/init.rb +5 -0
- data/lib/global/strategies/no_method_error_strategy.rb +92 -0
- data/lib/global/strategies/wrong_arguments_error_strategy.rb +174 -0
- data/lib/global/strategy.rb +67 -0
- data/lib/local/init.rb +6 -0
- data/lib/local/kernel/init.rb +1 -0
- data/lib/local/kernel/module.rb +43 -0
- data/lib/local/local.rb +43 -0
- data/lib/local/strategies/bignum_division_strategy.rb +11 -0
- data/lib/local/strategies/bignum_modulus_strategy.rb +11 -0
- data/lib/local/strategies/fixnum_coerce_strategy.rb +11 -0
- data/lib/local/strategies/fixnum_division_strategy.rb +12 -0
- data/lib/local/strategies/fixnum_modulus_strategy.rb +12 -0
- data/lib/local/strategies/float_division_strategy.rb +11 -0
- data/lib/local/strategies/float_modulus_strategy.rb +12 -0
- data/lib/local/strategies/init.rb +37 -0
- data/lib/local/strategies/numeric_division_strategy.rb +11 -0
- data/lib/local/strategies/numeric_modulus_strategy.rb +12 -0
- data/lib/local/strategies/soft_binding_strategy.rb +108 -0
- data/lib/local/strategies/swap_method_strategy.rb +32 -0
- data/lib/local/strategy.rb +18 -0
- data/lib/to_robust.rb +4 -0
- data/test/local/float_division.rb +28 -0
- data/test/local/integer_division.rb +28 -0
- data/test/local/method_binding.rb +45 -0
- metadata +109 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# Augments the functionality of NoMethodError instances so that the name of the missing method
|
2
|
+
# and the name of its context (if it has one) can be quickly and easily inspected.
|
3
|
+
class NoMethodError
|
4
|
+
|
5
|
+
# Returns the name of the missing method.
|
6
|
+
def method_name
|
7
|
+
message[/\`(.*?)'/, 1]
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns the details of the method owner (if it has one).
|
11
|
+
def method_owner
|
12
|
+
message.partition('for ')[2]
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Augments the ZeroDivisionError class to provide the line number that the ZeroDivisionError was encountered
|
2
|
+
# by some given method.
|
3
|
+
class ZeroDivisionError
|
4
|
+
|
5
|
+
# Finds the line that the zero division error was first thrown on by
|
6
|
+
# a given method (not the first line that it occurs at, since this may
|
7
|
+
# be within a non-robust method).
|
8
|
+
#
|
9
|
+
# *Parameters:*
|
10
|
+
# * method, the method to extract the line number for.
|
11
|
+
#
|
12
|
+
# *Returns:*
|
13
|
+
# The line that the error occurred on within the provided method.
|
14
|
+
def line_no(method)
|
15
|
+
backtrace.each do |b|
|
16
|
+
return b[/:(\d+):/][1...-1].to_i if b.start_with?(method.code)
|
17
|
+
end
|
18
|
+
return nil
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# A report is used to hold the details of a successful repair attempt along with
|
2
|
+
# the (partially) fixed method.
|
3
|
+
#
|
4
|
+
# If a repair was partially successful (i.e. it removed the cause of the original
|
5
|
+
# error but not all errors) then the report holds the next error that should
|
6
|
+
# be resolved.
|
7
|
+
#
|
8
|
+
# If a repair was completely succesful (i.e. the method executed without throwing
|
9
|
+
# any exceptions) then the report is used to hold the result of the method call.
|
10
|
+
#
|
11
|
+
# The storage of error and result by the report is exploited by the repair-cycle
|
12
|
+
# to prevent redundant method calls.
|
13
|
+
class ToRobust::Global::Report
|
14
|
+
|
15
|
+
attr_reader :fixed_method,
|
16
|
+
:result,
|
17
|
+
:error
|
18
|
+
|
19
|
+
# Creates a new report.
|
20
|
+
#
|
21
|
+
# *Parameters:*
|
22
|
+
# * fixed_method, the fixed form of the method.
|
23
|
+
# * opts, a hash of keyword options for this constructor.
|
24
|
+
# -> result, the result of the method call (if the repair was a complete success).
|
25
|
+
# -> error, the error encountered when calling the method after repair (if the repair was a partial success).
|
26
|
+
def initialize(fixed_method, opts = {})
|
27
|
+
@fixed_method = fixed_method
|
28
|
+
@result = opts[:result]
|
29
|
+
@error = opts[:error]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns true if the repair completely fixed the method (removing all errors).
|
33
|
+
def complete?
|
34
|
+
@error.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true if there were still further (unrelated) errors after fixing the method.
|
38
|
+
def partial?
|
39
|
+
not complete?
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# A robust procedure is a type of object which mimics the functionality of a standard lambda
|
2
|
+
# function but also adds line-specific error debugging information to exception traces.
|
3
|
+
class ToRobust::Global::RobustProc
|
4
|
+
|
5
|
+
attr_reader :headers,
|
6
|
+
:body,
|
7
|
+
:source
|
8
|
+
|
9
|
+
# Constructs a new Robust procedure.
|
10
|
+
#
|
11
|
+
# *Parameters:*
|
12
|
+
# * headers, the arguments to the procedure (as an array of argument names).
|
13
|
+
# * body, the body of the procedure.
|
14
|
+
def initialize(headers, body)
|
15
|
+
|
16
|
+
@headers = headers.freeze
|
17
|
+
@body = body.freeze
|
18
|
+
|
19
|
+
# Dynamically create the procedure as the "call" method of this object,
|
20
|
+
# and supply file and line debugging information.
|
21
|
+
@source = "def call(#{@headers.join(', ')})
|
22
|
+
#{body}
|
23
|
+
end"
|
24
|
+
|
25
|
+
instance_eval(@source, code, 0)
|
26
|
+
|
27
|
+
# Store the lines of the body in the source property as an array (using chomp
|
28
|
+
# to remove /n from the end of each line).
|
29
|
+
@source = @source.lines.map(&:chomp).freeze
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Every robust procedure has its own unique code that is used as its source
|
34
|
+
# file and is used to identify error information specific to it.
|
35
|
+
def code
|
36
|
+
"ROBUST_PROC_#{object_id}"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Makes this robust procedure object take on the function of another
|
40
|
+
# robust procedure. This allows repairs to be optionally saved.
|
41
|
+
def become(other)
|
42
|
+
@headers = other.headers
|
43
|
+
@body = other.body
|
44
|
+
@source = other.source
|
45
|
+
instance_eval(@source.join("\n"), code, 0)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
# Handles zero division errors.
|
2
|
+
class ToRobust::Global::Strategies::DivideByZeroStrategy < ToRobust::Global::Strategy
|
3
|
+
|
4
|
+
# Wraps all method calls in a safety barrier, preventing them from raising
|
5
|
+
# a ZeroDivisionError and instead causing them to return zero when a ZeroDivisionError
|
6
|
+
# is thrown.
|
7
|
+
#
|
8
|
+
# *Parameters:*
|
9
|
+
# * line, the line to wrap the method calls for.
|
10
|
+
#
|
11
|
+
# *Returns:*
|
12
|
+
# The provided line with all method calls wrapped.
|
13
|
+
def self.wrap_calls(line)
|
14
|
+
|
15
|
+
# Extract all the method calls in the line.
|
16
|
+
calls = extract_calls(line)
|
17
|
+
|
18
|
+
# Wrap each of the method calls in the original string.
|
19
|
+
calls.each_index do |x|
|
20
|
+
|
21
|
+
# Retrieve the co-ordinates of the call.
|
22
|
+
start_at, end_at = calls[x]
|
23
|
+
|
24
|
+
# Transform the method call.
|
25
|
+
line.insert start_at, "ToRobust::Global.prevent_dbz{"
|
26
|
+
line.insert end_at+30, "}"
|
27
|
+
|
28
|
+
# Modify the coordinates of all other method calls.
|
29
|
+
(x...calls.length).each do |y|
|
30
|
+
|
31
|
+
# If this method ends before another begins, shift
|
32
|
+
# both the start and end of that method.
|
33
|
+
if end_at < calls[y][0]
|
34
|
+
calls[y][0] += 30
|
35
|
+
calls[y][1] += 30
|
36
|
+
|
37
|
+
# If this X starts after Y but finishes before, then shift
|
38
|
+
# the end of Y by 13.
|
39
|
+
elsif start_at > calls[y][0] and end_at < calls[y][1]
|
40
|
+
calls[y][1] += 30
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the transformed line.
|
48
|
+
return line
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# Wraps all inline division operators in a safety barrier, preventing them from raising
|
53
|
+
# a ZeroDivisionError and instead causing them to return zero when a ZeroDivisionError
|
54
|
+
# is thrown.
|
55
|
+
#
|
56
|
+
# *Parameters:*
|
57
|
+
# * line, the line to wrap the inline division operators for.
|
58
|
+
#
|
59
|
+
# *Returns:*
|
60
|
+
# The provided line with all inline division operators wrapped.
|
61
|
+
def self.wrap_ops(line)
|
62
|
+
|
63
|
+
# Find the index of each division operator in the string.
|
64
|
+
last_index = -1
|
65
|
+
operations = []
|
66
|
+
until last_index.nil?
|
67
|
+
last_index = line.index('/', last_index+1)
|
68
|
+
operations << last_index unless last_index.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Find the start index of the left argument and the end index of
|
72
|
+
# the right argument.
|
73
|
+
operations.map! do |location|
|
74
|
+
|
75
|
+
# Find the index of the start of the left argument.
|
76
|
+
start_at = index = location
|
77
|
+
started = finished = false
|
78
|
+
bracket_depth = 0
|
79
|
+
until finished or start_at == 0
|
80
|
+
|
81
|
+
# Move to the next character.
|
82
|
+
index -= 1
|
83
|
+
char = line[index]
|
84
|
+
|
85
|
+
# Increase the bracket depth if a bracket is closed.
|
86
|
+
if char == ")"
|
87
|
+
started = true
|
88
|
+
bracket_depth += 1
|
89
|
+
|
90
|
+
# Decrease the bracket depth if a bracket is opened.
|
91
|
+
# If the resulting bracket depth is zero or less then
|
92
|
+
# the argument is finished.
|
93
|
+
elsif char == "("
|
94
|
+
started = true
|
95
|
+
bracket_depth -= 1
|
96
|
+
finished = bracket_depth <= 0
|
97
|
+
end_at = index if bracket_depth == 0
|
98
|
+
|
99
|
+
# Spaces are only accepted if they are enclosed within brackets
|
100
|
+
# or they trail the argument.
|
101
|
+
elsif char == " "
|
102
|
+
finished = (bracket_depth == 0 and started)
|
103
|
+
|
104
|
+
# Letters, digits and dots are accepted without question.
|
105
|
+
# Ensure that the argument is marked as "started" when they
|
106
|
+
# are encountered.
|
107
|
+
elsif char.match(/^[[:alnum:]]$/) or char == "."
|
108
|
+
started = true
|
109
|
+
|
110
|
+
# All other characters are only accepted if they are
|
111
|
+
# enclosed within brackets.
|
112
|
+
else
|
113
|
+
started = true
|
114
|
+
finished = true if bracket_depth <= 0
|
115
|
+
end
|
116
|
+
|
117
|
+
# Move back the starting point unless it has been found.
|
118
|
+
start_at = index unless finished
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
# Find the index of the end of the right argument.
|
123
|
+
index = end_at = location
|
124
|
+
started = finished = false
|
125
|
+
bracket_depth = 0
|
126
|
+
until finished or end_at == line.length-1
|
127
|
+
|
128
|
+
# Look at the character at the next index.
|
129
|
+
index += 1
|
130
|
+
char = line[index]
|
131
|
+
|
132
|
+
# Increase the bracket depth if a bracket is opened.
|
133
|
+
if char == "("
|
134
|
+
started = true
|
135
|
+
bracket_depth += 1
|
136
|
+
|
137
|
+
# Decrease the bracket depth if a bracket is closed.
|
138
|
+
# If the bracket closed is the last bracket left,
|
139
|
+
# then this marks the end of the argument.
|
140
|
+
elsif char == ")"
|
141
|
+
started = true
|
142
|
+
bracket_depth -= 1
|
143
|
+
finished = bracket_depth <= 0
|
144
|
+
end_at = index if bracket_depth == 0
|
145
|
+
|
146
|
+
# Spaces are only accepted if they are enclosed within brackets
|
147
|
+
# or they lead the argument.
|
148
|
+
elsif char == " "
|
149
|
+
finished = (bracket_depth == 0 and started)
|
150
|
+
|
151
|
+
# Letters, digits and dots are accepted without question.
|
152
|
+
# Ensure that the argument is marked as "started" when they
|
153
|
+
# are encountered.
|
154
|
+
elsif char.match(/^[[:alnum:]]$/) or char == "."
|
155
|
+
started = true
|
156
|
+
|
157
|
+
# All other characters are only accepted if they are
|
158
|
+
# enclosed within brackets.
|
159
|
+
else
|
160
|
+
started = true
|
161
|
+
finished = true if bracket_depth <= 0
|
162
|
+
end
|
163
|
+
|
164
|
+
# Move back the end point unless it has been found.
|
165
|
+
end_at = index unless finished
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
# Convert the index of the division operator to the start and
|
170
|
+
# end indices of the operation.
|
171
|
+
[start_at, end_at]
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
# Merge any overlapping operations into a single wrapper.
|
176
|
+
temp = []
|
177
|
+
operations.each do |x_start, x_end|
|
178
|
+
merged = false
|
179
|
+
temp.each_index do |y|
|
180
|
+
y_start, y_end = temp[y]
|
181
|
+
if x_start == y_end
|
182
|
+
temp[y][1] = x_end
|
183
|
+
merged = true
|
184
|
+
elsif x_end == y_start
|
185
|
+
temp[y][0] = x_start
|
186
|
+
merged = true
|
187
|
+
end
|
188
|
+
break if merged
|
189
|
+
end
|
190
|
+
temp << [x_start, x_end] unless merged
|
191
|
+
end
|
192
|
+
operations = temp
|
193
|
+
|
194
|
+
# Wrap each of the operations in the original string.
|
195
|
+
operations.each_index do |x|
|
196
|
+
|
197
|
+
# Retrieve the co-ordinates of the operation.
|
198
|
+
start_at, end_at = operations[x]
|
199
|
+
|
200
|
+
# Wrap the operation
|
201
|
+
line.insert start_at, "(ToRobust::Global.prevent_dbz{"
|
202
|
+
line.insert end_at+31, "})"
|
203
|
+
|
204
|
+
# Modify the coordinates of all other method calls.
|
205
|
+
#
|
206
|
+
#
|
207
|
+
#
|
208
|
+
# SURELY THIS SHOULD BE X+1?
|
209
|
+
#
|
210
|
+
#
|
211
|
+
#
|
212
|
+
(x...operations.length).each do |y|
|
213
|
+
|
214
|
+
# If this operation ends before another begins, shift
|
215
|
+
# both the start and end of that operation.
|
216
|
+
if end_at < operations[y][0]
|
217
|
+
operations[y][0] += 32
|
218
|
+
operations[y][1] += 32
|
219
|
+
|
220
|
+
# If this X starts after Y but finishes before, then shift
|
221
|
+
# the end of Y.
|
222
|
+
elsif start_at > operations[y][0] and end_at < operations[y][1]
|
223
|
+
operations[y][1] += 32
|
224
|
+
|
225
|
+
# If this Y is within X, shift the start and end of Y by the
|
226
|
+
# size of the prevention call opening.
|
227
|
+
elsif start_at < operations[y][0] and end_at > operations[y][1]
|
228
|
+
operations[y][0] += 30
|
229
|
+
operations[y][1] += 30
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
# Return the transformed line.
|
237
|
+
return line
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
# Finds all method calls and division operators that could yield a ZeroDivisionError
|
242
|
+
# on the line where the error occurred and prevents them from doing so again by
|
243
|
+
# wrapping them in a DbZ exception catcher.
|
244
|
+
#
|
245
|
+
# *Parameters:*
|
246
|
+
# * method, the method which encountered the error.
|
247
|
+
# * error, the error which occurred.
|
248
|
+
#
|
249
|
+
# *Returns:*
|
250
|
+
# A list of candidate solutions to fix the root of the error affecting the method.
|
251
|
+
def generate_candidates(method, error)
|
252
|
+
|
253
|
+
# Ensure that the error is a ZeroDivisionError.
|
254
|
+
return [] unless error.is_a? ZeroDivisionError
|
255
|
+
|
256
|
+
# Extract the contents of the line that the exception occured on.
|
257
|
+
line_no = error.line_no(method)
|
258
|
+
line_contents = method.source[line_no]
|
259
|
+
|
260
|
+
# We validate the fix by ensuring that any further errors are not
|
261
|
+
# ZeroDivisionError exceptions thrown on this line.
|
262
|
+
validator = lambda do |method, old_error, new_error|
|
263
|
+
not (new_error.is_a? ZeroDivisionError and old_error.line_no(method) == new_error.line_no(method))
|
264
|
+
end
|
265
|
+
|
266
|
+
# Compose the sole candidate fix for this error.
|
267
|
+
line_contents = self.class.wrap_ops(self.class.wrap_calls(line_contents))
|
268
|
+
return [
|
269
|
+
ToRobust::Global::Fix.new(
|
270
|
+
[ToRobust::Global::Fix::SwapAtom.new(line_no, line_contents)],
|
271
|
+
validator
|
272
|
+
)
|
273
|
+
]
|
274
|
+
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'levenshtein'
|
2
|
+
|
3
|
+
# Handles NoMethodError exceptions by mapping missing method calls to valid replacement methods within
|
4
|
+
# the same module/class whose name is within a given Levenshtein distance of the missing method name.
|
5
|
+
class ToRobust::Global::Strategies::NoMethodErrorStrategy < ToRobust::Global::Strategy
|
6
|
+
|
7
|
+
attr_accessor :max_distance
|
8
|
+
|
9
|
+
# Constructs a new NoMethodErrorStrategy.
|
10
|
+
#
|
11
|
+
# *Parameters:*
|
12
|
+
# * max_distance, the maximum allowed distance between an attempted and candidate method call.
|
13
|
+
def initialize(max_distance = 5)
|
14
|
+
@max_distance = max_distance
|
15
|
+
end
|
16
|
+
|
17
|
+
# Used to fix NoMethodError exceptions by replacing calls to missing methods with calls to
|
18
|
+
# valid methods whose name is within a given Levenshtein distance of the missing method name.
|
19
|
+
#
|
20
|
+
# *Parameters:*
|
21
|
+
# * method, the affected method.
|
22
|
+
# * error, the error whose root cause should be fixed.
|
23
|
+
#
|
24
|
+
# *Returns:*
|
25
|
+
# A (possibly empty) array of candidate fixes to the root cause of the error.
|
26
|
+
def generate_candidates(method, error)
|
27
|
+
|
28
|
+
# Ensure that the error is a NoMethodError.
|
29
|
+
return [] unless error.is_a? NoMethodError
|
30
|
+
|
31
|
+
# Retrieve details of the missing method and the contents of the line that the error
|
32
|
+
# occurred on.
|
33
|
+
line_no = error.line_no
|
34
|
+
line_contents = method.source[line_no]
|
35
|
+
missing_method_name = error.method_name
|
36
|
+
missing_method_owner = error.method_owner
|
37
|
+
|
38
|
+
# "main" methods aren't dealt with since there are too many dangerous replacement methods
|
39
|
+
# contained within main.
|
40
|
+
return [] if missing_method_owner.empty?
|
41
|
+
|
42
|
+
# Check if the missing method belongs to a module or a class.
|
43
|
+
missing_method_owner = missing_method_owner.partition(':')
|
44
|
+
missing_method_owner_type = missing_method_owner[2]
|
45
|
+
|
46
|
+
# Check if the missing method belongs to neither a module nor class (this
|
47
|
+
# shouldn't happen).
|
48
|
+
return [] unless ['Class', 'Module'].include? missing_method_owner_type
|
49
|
+
|
50
|
+
# Retrieve the object for the class / module.
|
51
|
+
missing_method_owner = Kernel.const_get(missing_method_owner[0])
|
52
|
+
|
53
|
+
# Extract a list of candidate methods from the class / module and calculate
|
54
|
+
# their levenshtein distance to the missing method. Before calculating the levenshtein
|
55
|
+
# distance to candidates, throw away any candidates whose difference in length with
|
56
|
+
# the requested method is greater than the maximum distance (since it is impossible
|
57
|
+
# that their levenshtein distance can be less or equal to the maximum distance).
|
58
|
+
if missing_method_owner_type == 'Module'
|
59
|
+
candidates = missing_method_owner.singleton_class.public_instance_methods(false)
|
60
|
+
else
|
61
|
+
candidates = missing_method_owner.public_instance_methods(false)
|
62
|
+
end
|
63
|
+
|
64
|
+
candidates.reject!{|c| (c.length - missing_method_name.length).abs > @max_distance}
|
65
|
+
candidates.map!{|c| [c.to_s, Levenshtein.distance(missing_method_name, c.to_s)]}
|
66
|
+
|
67
|
+
# Only keep candidates whose distance with the missing
|
68
|
+
# method is less or equal to the threshold. Order the remaining candidates
|
69
|
+
# before throwing away the distance information.
|
70
|
+
candidates.reject!{|c| c[1] > @max_distance}
|
71
|
+
candidates.sort {|x,y| x[1] <=> y[1]}
|
72
|
+
candidates.map!{|c| c[0]}
|
73
|
+
|
74
|
+
# We validate the exception by ensuring that any further exceptions aren't
|
75
|
+
# NoMethodError exceptions for the fixed method on the fixed line.
|
76
|
+
validator = lambda do |method, old_error, new_error|
|
77
|
+
return (not (new_error.is_a? NoMethodError and new_error.method_name == old_error.method_name and new_error.line_no == old_error.line_no))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Compose each candidate into a fix by replacing each occurence of the
|
81
|
+
# missing method name in the string with the name of the candidate method.
|
82
|
+
return candidates.map! do |c|
|
83
|
+
fixed_line = line_contents.gsub(/(\(|^|::|\.|\s|,)#{missing_method_name}\(/) {|s| s[missing_method_name] = c; s}
|
84
|
+
ToRobust::Global::Fix.new(
|
85
|
+
[ToRobust::Global::Fix::SwapAtom.new(line_no, fixed_line)],
|
86
|
+
validator
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|