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,174 @@
1
+ # Robustness strategy for dealing with ArgumentError's thrown due to incorrect numbers of
2
+ # arguments being supplied to a method. Trims or pads the list of arguments so that the number
3
+ # of arguments is equal to the arity of the method (i.e. the number of provided arguments
4
+ # matches the expected number of arguments).
5
+ class ToRobust::Global::Strategies::WrongArgumentsErrorStrategy < ToRobust::Global::Strategy
6
+
7
+ # Fixes all calls to a given method on a provided line such that they all use
8
+ # an expected number of arguments. Method calls with too few parameters are padded with zeroes,
9
+ # whilst method calls with too many parameters are trimmed to the correct size.
10
+ #
11
+ # *Parameters:*
12
+ # * method, the method to fix calls to.
13
+ # * arity, the arity (expected number of parameters) of the method.
14
+ # * line, the line to fix method calls on.
15
+ #
16
+ # *Returns:*
17
+ # The fixed form of the line, with all calls to the given method meeting the argument length requirements.
18
+ def self.fix_calls(method, arity, line)
19
+
20
+ # Extract all calls to the affected method on the given line.
21
+ calls = extract_calls(line)
22
+ calls.reject! do |c|
23
+ method_full_name = line[c[0]...line.index('(', c[0])].split(/\.|::/)
24
+ method_full_name.last != method
25
+ end
26
+
27
+ # Re-order calls so that nested calls are processed first.
28
+ # First, order the calls by their starting position.
29
+ # Secondly, create a new list and insert each call at an smaller
30
+ # index to any calls that enclose it.
31
+ calls.sort!{|a,b| a[0] <=> b[0]}
32
+ temp = []
33
+ calls.each do |x|
34
+ index = insert_at = temp.length
35
+ if insert_at > 0
36
+ temp.reverse_each do |y|
37
+ index -= 1
38
+ insert_at = index if y[0] < x[0] and y[1] > x[1]
39
+ end
40
+ end
41
+ temp.insert(insert_at, x)
42
+ end
43
+ calls = temp
44
+
45
+ # Process each of the calls (in the safe order that has
46
+ # been established). Begin by extracting the current arguments
47
+ # for the method call, then either padding or shrinking those
48
+ # arguments to meet the required length.
49
+ calls.each_index do |call_index|
50
+
51
+ meth_start, meth_end = calls[call_index]
52
+ params_start = line.index('(', meth_start)+1
53
+ arg_start = params_start
54
+
55
+ # Process each character in the body of parameters.
56
+ open_brackets = 0
57
+ arguments = []
58
+ for char_index in params_start...meth_end
59
+
60
+ # Extract the character at the given point in the call.
61
+ char = line[char_index]
62
+
63
+ # Listen for bracket closings.
64
+ if char == '('
65
+ open_brackets += 1
66
+
67
+ # Listen for any bracket openings.
68
+ elsif char == ')'
69
+ open_brackets -= 1
70
+
71
+ # Check if this character marks the end of the argument.
72
+ # Only interpret it as the end of the argument if there are
73
+ # no open brackets.
74
+ elsif char == ',' and open_brackets == 0
75
+ arguments << [arg_start, char_index-1]
76
+ arg_start = char_index + 1
77
+ end
78
+
79
+ end
80
+
81
+ # Add the last argument.
82
+ arguments << [arg_start, meth_end-1]
83
+
84
+ # Shrink the arguments to the limit (better to do this before
85
+ # extracting their text contents, small optimisation), then
86
+ # retrieve their context contents (stripping any leading and
87
+ # trailing whitespace) and pad the arguments with zeroes
88
+ # where necessary.
89
+ arguments = arguments.first(arity)
90
+ arguments.map!{|a_start, a_end| line[a_start..a_end].strip}
91
+ arguments.fill('0', arguments.length...arity)
92
+
93
+ # Apply the transformation, calculate the length difference (delta)
94
+ # and re-adjust the boundaries of the remaining method calls.
95
+ transformed = "#{line[meth_start...line.index('(', meth_start)]}(#{arguments.join(',')})"
96
+ delta = transformed.length - (meth_end - meth_start + 1)
97
+ line[meth_start..meth_end] = transformed
98
+ (call_index+1...calls.length).each do |succ_call_index|
99
+
100
+ # Find the start and end points of the call.
101
+ succ_call_start, succ_call_end = calls[succ_call_index]
102
+
103
+ # Move the end point of any call containing the call that
104
+ # has been transformed.
105
+ if succ_call_start < meth_start and succ_call_end > meth_end
106
+ calls[succ_call_index][1] += delta
107
+
108
+ # Move the start and end points of each call that
109
+ # starts after the end of this call. (You could embed this
110
+ # within the if statement above, but this is nicer to read).
111
+ elsif succ_call_start > meth_end
112
+ calls[succ_call_index][0] += delta
113
+ calls[succ_call_index][1] += delta
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ # Return the transformed line.
121
+ return line
122
+
123
+ end
124
+
125
+ # Used to fix ArgumentError exceptions by ensuring that all calls to a given method
126
+ # on the line that the error occurred on use the expected number of arguments.
127
+ #
128
+ # When too few arguments are used during a method call, those arguments are padded
129
+ # with zeroes to reach the correct number of parameters.
130
+ #
131
+ # When too many arguments are supplied to a method, those arguments are trimmed to
132
+ # the number of arguments expected by the method.
133
+ #
134
+ # *Parameters:*
135
+ # * method, the affected method.
136
+ # * error, the error whose root cause should be fixed.
137
+ #
138
+ # *Returns:*
139
+ # A (possibly empty) array of candidate fixes to the root cause of the error.
140
+ def generate_candidates(method, error)
141
+
142
+ # Ensure that the error is a ArgumentError caused by an incorrect number
143
+ # of arguments being supplied to a method.
144
+ return [] unless error.is_a? ArgumentError and error.message.include? 'wrong number of arguments'
145
+
146
+ # Extract details of the error.
147
+ affected_method = error.affected_method
148
+ args_expected = error.args_expected
149
+ line_no = error.line_no
150
+ line_contents = method.source[line_no]
151
+
152
+ # We validate the fix by ensuring that any further errors are not
153
+ # ArgumentErrors for the incorrect number of arguments on the same
154
+ # method and on the same line.
155
+ validator = lambda do |method, old_error, new_error|
156
+ return true unless new_error.is_a? ArgumentError
157
+ return true unless new_error.message.include? 'wrong number of arguments'
158
+ return true unless new_error.line_no == old_error.line_no
159
+ return true unless new_error.affected_method == old_error.affected_method
160
+ return false
161
+ end
162
+
163
+ # Compose the sole candidate fix for this error.
164
+ line_contents = self.class.fix_calls(affected_method, args_expected, line_contents)
165
+ return [
166
+ ToRobust::Global::Fix.new(
167
+ [ToRobust::Global::Fix::SwapAtom.new(line_no, line_contents)],
168
+ validator
169
+ )
170
+ ]
171
+
172
+ end
173
+
174
+ end
@@ -0,0 +1,67 @@
1
+ # Strategies are used by the global robustness layer to suggest candidate fixes to errors that occur
2
+ # within a given function.
3
+ class ToRobust::Global::Strategy
4
+
5
+ # Extracts the co-ordinates of all method calls on a given line.
6
+ #
7
+ # *Parameters:*
8
+ # * line, the line to extract method calls from.
9
+ #
10
+ # *Returns:*
11
+ # An array of co-ordinate pairs (each an array - could use a range?).
12
+ def self.extract_calls(line)
13
+
14
+ # Find the co-ordinates of every method call in the string.
15
+ stack = []
16
+ calls = []
17
+ (0..line.length).each do |index|
18
+
19
+ # Get the character at this index.
20
+ char = line[index]
21
+
22
+ # If this is the start of a method call,
23
+ # add the starting index to the stack.
24
+ if char == '('
25
+ stack << index
26
+
27
+ # If this is the end of a method call,
28
+ # pop the last index off the stack and combine
29
+ # it with the current index to give the co-ordinates
30
+ # of the call.
31
+ elsif char == ')'
32
+ calls << [stack.pop, index]
33
+ end
34
+
35
+ end
36
+
37
+ # Post-process the co-ordinates so that they start from the label
38
+ # of the method call rather than the bracket opening. Do this by
39
+ # moving towards the start of the original string until the method
40
+ # definition has finished (checked by looking at the character).
41
+ calls.each_index do |i|
42
+ start_at = calls[i][0]
43
+ until start_at == 0 do
44
+ char = line[start_at-1]
45
+ break if not (char.match(/^[[:alpha:]]$/) or ['_',':','.'].include? char)
46
+ start_at -= 1
47
+ end
48
+ calls[i][0] = start_at
49
+ end
50
+
51
+ return calls
52
+
53
+ end
54
+
55
+ # Generates a list of candidate fixes to a given problem.
56
+ #
57
+ # *Parameters:*
58
+ # * method, the affected method.
59
+ # * error, the error which occurred within the method.
60
+ #
61
+ # *Returns:*
62
+ # A (possibly empty) array of candidate solutions to fix the root of the error.
63
+ def generate_candidates(method, error)
64
+ raise NotImplementedError, 'No "generate_candidates" method was implemented by this Strategy.'
65
+ end
66
+
67
+ end
data/lib/local/init.rb ADDED
@@ -0,0 +1,6 @@
1
+ module ToRobust; end
2
+
3
+ require_relative 'kernel/init.rb'
4
+ require_relative 'local.rb'
5
+ require_relative 'strategy.rb'
6
+ require_relative 'strategies/init.rb'
@@ -0,0 +1 @@
1
+ require_relative 'module.rb'
@@ -0,0 +1,43 @@
1
+ class Module
2
+
3
+ # Hides all public module methods specific to this module by prepending their names
4
+ # with "__" and making them private.
5
+ def hide_methods!
6
+ singleton_class.public_instance_methods(false).each do |original|
7
+
8
+ # Ignore method missing.
9
+ next if original == :method_missing
10
+
11
+ # Prepend "__" to the method name and make it private.
12
+ hidden = ('__' + original.to_s).to_sym
13
+ singleton_class.send(:alias_method, hidden, original)
14
+ singleton_class.send(:private, hidden)
15
+
16
+ end
17
+ end
18
+
19
+ # Unhides all previously hidden methods, restoring the object/class/module to its
20
+ # original state.
21
+ def unhide_methods!
22
+ hidden_method_symbols.each do |sym|
23
+ original = sym.to_s[2..-1].to_sym
24
+ singleton_class.send(:alias_method, original, sym)
25
+ singleton_class.send(:remove_method, sym)
26
+ singleton_class.send(:public, original)
27
+ end
28
+ end
29
+
30
+ # Returns a hash of the hidden methods of this module (indexed by the original name
31
+ # of the methods, as strings).
32
+ def hidden_methods
33
+ Hash[hidden_method_symbols.map { |sym| [sym.to_s[2..-1], method(sym)] }]
34
+ end
35
+
36
+ private
37
+
38
+ # Returns an array of the symbols for each of the hidden methods (including their "__").
39
+ def hidden_method_symbols
40
+ singleton_class.private_instance_methods(false).select { |m| m.to_s.start_with? '__' }
41
+ end
42
+
43
+ end
@@ -0,0 +1,43 @@
1
+ # The Local robustness module is a slightly tailored version of the local robustness layer
2
+ # proposed in Chris Timperley's Master's Thesis.
3
+ #
4
+ # For now patches are enabled and disabled by checking the status of the "enabled" flag in
5
+ # the Local robustness module for each individual patch.
6
+ #
7
+ # A far nicer alternative would be to implement patches as instances of a Patch class.
8
+ # This class would then contain details of the target class and method as well as a
9
+ # lambda function implementing the patched form of the method.
10
+ #
11
+ # WARNING: Thread safety is a concern.
12
+ #
13
+ # Author: Chris Timperley
14
+ module ToRobust::Local
15
+
16
+ # List of strategies.
17
+ @strategies = []
18
+ class << self
19
+ attr_reader :strategies
20
+ end
21
+
22
+ # Executes a given block under local robustness protection.
23
+ #
24
+ # *Parameters:*
25
+ # * *contexts, depickled list of context objects to protect method calls for.
26
+ # * &block, the block to execute under local robustness protection.
27
+ #
28
+ # *Returns:*
29
+ # * The result of the block execution.
30
+ def self.protected(*contexts, &block)
31
+ @strategies.each { |s| s.prepare!(contexts) }
32
+ @strategies.each { |s| s.enable! }
33
+
34
+ begin
35
+ result = block.call
36
+ ensure
37
+ @strategies.each { |s| s.disable! }
38
+ end
39
+
40
+ return result
41
+ end
42
+
43
+ end
@@ -0,0 +1,11 @@
1
+ # Ensures that integer division returns 0 if the denominator is zero.
2
+ class ToRobust::Local::Strategies::BignumDivisionStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new BignumDivisionStrategy.
5
+ def initialize
6
+ super(Bignum, :div, :__div) do |other|
7
+ other.zero? ? 0 : __div(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ # Ensures that integer division returns 0 if the denominator is zero.
2
+ class ToRobust::Local::Strategies::BignumModulusStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new BignumModulusStrategy.
5
+ def initialize
6
+ super(Bignum, :%, :__mod) do |other|
7
+ other.zero? ? 0 : __mod(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ # Coerce nil to 0.
2
+ class ToRobust::Local::Strategies::FixnumCoerceStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new FixnumCoerceStrategy.
5
+ def initialize
6
+ super(Fixnum, :coerce, :__coerce) do |other|
7
+ other.nil? ? 0 : __coerce(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ # Ensures that Fixnum division never encounters zero division errors
2
+ # by returning zero when the denominator is zero.
3
+ class ToRobust::Local::Strategies::FixnumDivisionStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
4
+
5
+ # Constructs a new FixnumDivisionStrategy.
6
+ def initialize
7
+ super(Fixnum, :/, :__div) do |other|
8
+ other.zero? ? 0 : __div(other)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,12 @@
1
+ # Ensures that the modulus operator returns zero if a divide
2
+ # by zero error would occur.
3
+ class ToRobust::Local::Strategies::FixnumModulusStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
4
+
5
+ # Constructs a new FixnumModulusStrategy.
6
+ def initialize
7
+ super(Fixnum, :%, :__mod) do |other|
8
+ other.zero? ? 0 : __mod(other)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,11 @@
1
+ # Ensures that any float divided by zero gives 0.0
2
+ class ToRobust::Local::Strategies::FloatDivisionStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
3
+
4
+ # Constructs a new FloatDivisionStrategy.
5
+ def initialize
6
+ super(Float, :/, :__fdiv) do |other|
7
+ other.zero? ? 0.0 : __fdiv(other)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ # Ensures that the modulus operator returns zero if a divide
2
+ # by zero error would occur.
3
+ class ToRobust::Local::Strategies::FloatModulusStrategy < ToRobust::Local::Strategies::SwapMethodStrategy
4
+
5
+ # Constructs a new FloatModulusStrategy.
6
+ def initialize
7
+ super(Float, :%, :__mod) do |other|
8
+ other.zero? ? 0.0 : __mod(other)
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,37 @@
1
+ module ToRobust::Local::Strategies; end
2
+
3
+ # Load all the strategy definitions.
4
+ require_relative 'swap_method_strategy.rb'
5
+
6
+ require_relative 'numeric_division_strategy.rb'
7
+ require_relative 'numeric_modulus_strategy.rb'
8
+
9
+ require_relative 'bignum_division_strategy.rb'
10
+ require_relative 'bignum_modulus_strategy.rb'
11
+
12
+ require_relative 'fixnum_division_strategy.rb'
13
+ require_relative 'fixnum_modulus_strategy.rb'
14
+ require_relative 'fixnum_coerce_strategy.rb'
15
+
16
+ require_relative 'float_division_strategy.rb'
17
+ require_relative 'float_modulus_strategy.rb'
18
+
19
+ require_relative 'soft_binding_strategy.rb'
20
+
21
+ # Instantiate and attach each of them.
22
+ # This could be done automatically (following class definition) using
23
+ # the "defined" gem, but this may cause some compatibility issues.
24
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::NumericDivisionStrategy.new
25
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::NumericModulusStrategy.new
26
+
27
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::BignumDivisionStrategy.new
28
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::BignumModulusStrategy.new
29
+
30
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FixnumDivisionStrategy.new
31
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FixnumModulusStrategy.new
32
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FixnumCoerceStrategy.new
33
+
34
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FloatModulusStrategy.new
35
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::FloatDivisionStrategy.new
36
+
37
+ ToRobust::Local.strategies << ToRobust::Local::Strategies::SoftBindingStrategy.new