to_robust 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +39 -0
  3. data/lib/global/fix.rb +72 -0
  4. data/lib/global/fix/add_atom.rb +28 -0
  5. data/lib/global/fix/atom.rb +51 -0
  6. data/lib/global/fix/init.rb +4 -0
  7. data/lib/global/fix/remove_atom.rb +16 -0
  8. data/lib/global/fix/swap_atom.rb +26 -0
  9. data/lib/global/global.rb +155 -0
  10. data/lib/global/init.rb +10 -0
  11. data/lib/global/kernel/argument_error.rb +22 -0
  12. data/lib/global/kernel/exception.rb +9 -0
  13. data/lib/global/kernel/init.rb +4 -0
  14. data/lib/global/kernel/no_method_error.rb +15 -0
  15. data/lib/global/kernel/zero_division_error.rb +21 -0
  16. data/lib/global/report.rb +42 -0
  17. data/lib/global/robust_proc.rb +48 -0
  18. data/lib/global/strategies/divide_by_zero_error_strategy.rb +277 -0
  19. data/lib/global/strategies/init.rb +5 -0
  20. data/lib/global/strategies/no_method_error_strategy.rb +92 -0
  21. data/lib/global/strategies/wrong_arguments_error_strategy.rb +174 -0
  22. data/lib/global/strategy.rb +67 -0
  23. data/lib/local/init.rb +6 -0
  24. data/lib/local/kernel/init.rb +1 -0
  25. data/lib/local/kernel/module.rb +43 -0
  26. data/lib/local/local.rb +43 -0
  27. data/lib/local/strategies/bignum_division_strategy.rb +11 -0
  28. data/lib/local/strategies/bignum_modulus_strategy.rb +11 -0
  29. data/lib/local/strategies/fixnum_coerce_strategy.rb +11 -0
  30. data/lib/local/strategies/fixnum_division_strategy.rb +12 -0
  31. data/lib/local/strategies/fixnum_modulus_strategy.rb +12 -0
  32. data/lib/local/strategies/float_division_strategy.rb +11 -0
  33. data/lib/local/strategies/float_modulus_strategy.rb +12 -0
  34. data/lib/local/strategies/init.rb +37 -0
  35. data/lib/local/strategies/numeric_division_strategy.rb +11 -0
  36. data/lib/local/strategies/numeric_modulus_strategy.rb +12 -0
  37. data/lib/local/strategies/soft_binding_strategy.rb +108 -0
  38. data/lib/local/strategies/swap_method_strategy.rb +32 -0
  39. data/lib/local/strategy.rb +18 -0
  40. data/lib/to_robust.rb +4 -0
  41. data/test/local/float_division.rb +28 -0
  42. data/test/local/integer_division.rb +28 -0
  43. data/test/local/method_binding.rb +45 -0
  44. metadata +109 -0
@@ -0,0 +1,9 @@
1
+ # Augments the functionality of the Exception class to make error debugging simpler.
2
+ class Exception
3
+
4
+ # Returns the line number that this exception was thrown on.
5
+ def line_no
6
+ backtrace[0][/:(\d+):/][1...-1].to_i
7
+ end
8
+
9
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'exception.rb'
2
+ require_relative 'no_method_error.rb'
3
+ require_relative 'argument_error.rb'
4
+ require_relative 'zero_division_error.rb'
@@ -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,5 @@
1
+ module ToRobust::Global::Strategies; end
2
+
3
+ require_relative 'divide_by_zero_error_strategy.rb'
4
+ require_relative 'no_method_error_strategy.rb'
5
+ require_relative 'wrong_arguments_error_strategy.rb'
@@ -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