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.
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