ffast 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,10 +2,12 @@
2
2
 
3
3
  require 'fast'
4
4
 
5
+ # Allow to replace code managing multiple replacements and combining replacements.
6
+ # Useful for large codebase refactor and multiple replacements in the same file.
5
7
  module Fast
6
8
  class << self
7
9
  # Fast.experiment is a shortcut to define new experiments and allow them to
8
- # work together in experiment combinantions.
10
+ # work together in experiment combinations.
9
11
  #
10
12
  # The following experiment look into `spec` folder and try to remove
11
13
  # `before` and `after` blocks on testing code. Sometimes they're not
@@ -13,12 +15,13 @@ module Fast
13
15
  #
14
16
  # If the spec does not fail, it keeps the change.
15
17
  #
16
- # Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
17
- # lookup 'spec'
18
- # search "(block (send nil {before after}))"
19
- # edit { |node| remove(node.loc.expression) }
20
- # policy { |new_file| system("rspec --fail-fast #{new_file}") }
21
- # end
18
+ # @example Remove useless before and after block
19
+ # Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
20
+ # lookup 'spec'
21
+ # search "(block (send nil {before after}))"
22
+ # edit { |node| remove(node.loc.expression) }
23
+ # policy { |new_file| system("rspec --fail-fast #{new_file}") }
24
+ # end
22
25
  def experiment(name, &block)
23
26
  @experiments ||= {}
24
27
  @experiments[name] = Experiment.new(name, &block)
@@ -27,18 +30,64 @@ module Fast
27
30
  attr_reader :experiments
28
31
  end
29
32
 
33
+ # Fast experiment allow the user to combine single replacements and make multiple
34
+ # changes at the same time. Defining a policy is possible to check if the
35
+ # experiment was successfull and keep changing the file using a specific
36
+ # search.
37
+ #
38
+ # The experiment have a combination algorithm that recursively check what
39
+ # combinations work with what combinations. It can delay years and because of
40
+ # that it tries a first replacement targeting all the cases in a single file.
41
+ #
30
42
  # You can define experiments and build experimental files to improve some code in
31
43
  # an automated way. Let's create a hook to check if a `before` or `after` block
32
44
  # is useless in a specific spec:
33
45
  #
34
- # ```ruby
35
- # Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
36
- # lookup 'some_spec.rb'
37
- # search "(block (send nil {before after}))"
38
- # edit {|node| remove(node.loc.expression) }
39
- # policy {|new_file| system("bin/spring rspec --fail-fast #{new_file}") }
40
- # end
41
- # ```
46
+ # @example Remove useless before or after block RSpec hooks
47
+ # # Let's say you want to experimentally remove some before or after block
48
+ # # in specs to check if some of them are weak or useless:
49
+ # # RSpec.describe "something" do
50
+ # # before { @a = 1 }
51
+ # # before { @b = 1 }
52
+ # # it { expect(@b).to be_eq(1) }
53
+ # # end
54
+ # #
55
+ # # The variable `@a` is not useful for the test, if I remove the block it
56
+ # # should continue passing.
57
+ # #
58
+ # # RSpec.describe "something" do
59
+ # # before { @b = 1 }
60
+ # # it { expect(@b).to be_eq(1) }
61
+ # # end
62
+ # #
63
+ # # But removing the next `before` block will fail:
64
+ # # RSpec.describe "something" do
65
+ # # before { @a = 1 }
66
+ # # it { expect(@b).to be_eq(1) }
67
+ # # end
68
+ # # And the experiments will have a policy to check if `rspec` run without
69
+ # # fail and only execute successfull replacements.
70
+ # Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
71
+ # lookup 'spec' # all files in the spec folder
72
+ # search "(block (send nil {before after}))"
73
+ # edit {|node| remove(node.loc.expression) }
74
+ # policy {|new_file| system("rspec --fail-fast #{new_file}") }
75
+ # end
76
+ #
77
+ # @example Replace FactoryBot create with build_stubbed method
78
+ # # Let's say you want to try to automate some replacement of
79
+ # # `FactoryBot.create` to use `FactoryBot.build_stubbed`.
80
+ # # For specs let's consider the example we want to refactor:
81
+ # # let(:person) { create(:person, :with_email) }
82
+ # # And the intent is replace to use `build_stubbed` instead of `create`:
83
+ # # let(:person) { build_stubbed(:person, :with_email) }
84
+ # Fast.experiment('RSpec/ReplaceCreateWithBuildStubbed') do
85
+ # lookup 'spec'
86
+ # search '(block (send nil let (sym _)) (args) $(send nil create))'
87
+ # edit { |_, (create)| replace(create.loc.selector, 'build_stubbed') }
88
+ # policy { |new_file| system("rspec --format progress --fail-fast #{new_file}") }
89
+ # end
90
+ # @see https://asciinema.org/a/177283
42
91
  class Experiment
43
92
  attr_writer :files
44
93
  attr_reader :name, :replacement, :expression, :files_or_folders, :ok_if
@@ -49,36 +98,50 @@ module Fast
49
98
  instance_exec(&block)
50
99
  end
51
100
 
101
+ # It combines current experiment with {ExperimentFile#run}
102
+ # @param [String] file to be analyzed by the experiment
52
103
  def run_with(file)
53
104
  ExperimentFile.new(file, self).run
54
105
  end
55
106
 
107
+ # @param [String] expression with the node pattern to target nodes
56
108
  def search(expression)
57
109
  @expression = expression
58
110
  end
59
111
 
112
+ # @param &block yields the node that matches and return the block in the
113
+ # instance context of a [Fast::Rewriter]
60
114
  def edit(&block)
61
115
  @replacement = block
62
116
  end
63
117
 
118
+ # @param [String] that will be combined to find the {#files}
64
119
  def lookup(files_or_folders)
65
120
  @files_or_folders = files_or_folders
66
121
  end
67
122
 
123
+ # It calls the block after the replacement and use the result
124
+ # to drive the {Fast::ExperimentFile#ok_experiments} and {Fast::ExperimentFile#fail_experiments}.
125
+ # @param &block yields a temporary file with the content replaced in the current round.
68
126
  def policy(&block)
69
127
  @ok_if = block
70
128
  end
71
129
 
130
+ # @return [Array<String>] with files from {#lookup} expression.
72
131
  def files
73
132
  @files ||= Fast.ruby_files_from(@files_or_folders)
74
133
  end
75
134
 
135
+ # Iterates over all {#files} to {#run_with} them.
136
+ # @return [void]
76
137
  def run
77
138
  files.map(&method(:run_with))
78
139
  end
79
140
  end
80
141
 
81
142
  # Suggest possible combinations of occurrences to replace.
143
+ #
144
+ # Check for {#generate_combinations} to understand the strategy of each round.
82
145
  class ExperimentCombinations
83
146
  attr_reader :combinations
84
147
 
@@ -89,6 +152,10 @@ module Fast
89
152
  @occurrences_count = occurrences_count
90
153
  end
91
154
 
155
+ # Generate different combinations depending on the current round.
156
+ # * Round 1: Use {#individual_replacements}
157
+ # * Round 2: Tries {#all_ok_replacements_combined}
158
+ # * Round 3+: Follow {#ok_replacements_pair_combinations}
92
159
  def generate_combinations
93
160
  case @round
94
161
  when 1
@@ -112,8 +179,7 @@ module Fast
112
179
  [@ok_experiments.uniq.sort]
113
180
  end
114
181
 
115
- # Combining all successful individual replacements has failed. Lets divide
116
- # and conquer.
182
+ # Divide and conquer combining all successful individual replacements.
117
183
  def ok_replacements_pair_combinations
118
184
  @ok_experiments
119
185
  .combination(2)
@@ -122,9 +188,59 @@ module Fast
122
188
  end
123
189
  end
124
190
 
125
- # Encapsulate the join of an Experiment with an specific file.
126
- # This is important to coordinate and regulate multiple experiments in the same file.
127
- # It can track successfull experiments and failures and suggest new combinations to keep replacing the file.
191
+ # Combines an {Fast::Experiment} with a specific file.
192
+ # It coordinates and regulate multiple replacements in the same file.
193
+ # Everytime it {#run} a file, it uses {#partial_replace} and generate a
194
+ # new file with the new content.
195
+ # It executes the {Fast::Experiment#policy} block yielding the new file. Depending on the
196
+ # policy result, it adds the occurrence to {#fail_experiments} or {#ok_experiments}.
197
+ # When all possible occurrences are replaced in isolated experiments, it
198
+ # #{build_combinations} with the winner experiments going to a next round of experiments
199
+ # with multiple partial replacements until find all possible combinations.
200
+ # @note it can easily spend days handling multiple one to one combinations,
201
+ # because of that, after the first round of replacements the algorithm goes
202
+ # replacing all winner solutions in the same shot. If it fails, it goes
203
+ # combining one to one.
204
+ # @see Fast::Experiment
205
+ # @example Temporary spec to analyze
206
+ # tempfile = Tempfile.new('some_spec.rb')
207
+ # tempfile.write <<~RUBY
208
+ # let(:user) { create(:user) }
209
+ # let(:address) { create(:address) }
210
+ # let(:phone_number) { create(:phone_number) }
211
+ # let(:country) { create(:country) }
212
+ # let(:language) { create(:language) }
213
+ # RUBY
214
+ # tempfile.close
215
+ # @example Temporary experiment to replace create with build stubbed
216
+ # experiment = Fast.experiment('RSpec/ReplaceCreateWithBuildStubbed') do
217
+ # lookup 'some_spec.rb'
218
+ # search '(send nil create)'
219
+ # edit { |node| replace(node.loc.selector, 'build_stubbed') }
220
+ # policy { |new_file| system("rspec --fail-fast #{new_file}") }
221
+ # end
222
+ # @example ExperimentFile exploring combinations and failures
223
+ # experiment_file = Fast::ExperimentFile.new(tempfile.path, experiment)
224
+ # experiment_file.build_combinations # => [1, 2, 3, 4, 5]
225
+ # experiment_file.ok_with(1)
226
+ # experiment_file.failed_with(2)
227
+ # experiment_file.ok_with(3)
228
+ # experiment_file.ok_with(4)
229
+ # experiment_file.ok_with(5)
230
+ # # Try a combination of all OK individual replacements.
231
+ # experiment_file.build_combinations # => [[1, 3, 4, 5]]
232
+ # experiment_file.failed_with([1, 3, 4, 5])
233
+ # # If the above failed, divide and conquer.
234
+ # experiment_file.build_combinations # => [[1, 3], [1, 4], [1, 5], [3, 4], [3, 5], [4, 5]]
235
+ # experiment_file.ok_with([1, 3])
236
+ # experiment_file.failed_with([1, 4])
237
+ # experiment_file.build_combinations # => [[4, 5], [1, 3, 4], [1, 3, 5]]
238
+ # experiment_file.failed_with([1, 3, 4])
239
+ # experiment_file.build_combinations # => [[4, 5], [1, 3, 5]]
240
+ # experiment_file.failed_with([4, 5])
241
+ # experiment_file.build_combinations # => [[1, 3, 5]]
242
+ # experiment_file.ok_with([1, 3, 5])
243
+ # experiment_file.build_combinations # => []
128
244
  class ExperimentFile
129
245
  attr_reader :ok_experiments, :fail_experiments, :experiment
130
246
 
@@ -137,10 +253,12 @@ module Fast
137
253
  @round = 0
138
254
  end
139
255
 
256
+ # @return [String] from {Fast::Experiment#expression}.
140
257
  def search
141
258
  experiment.expression
142
259
  end
143
260
 
261
+ # @return [String] with a derived name with the combination number.
144
262
  def experimental_filename(combination)
145
263
  parts = @file.split('/')
146
264
  dir = parts[0..-2]
@@ -148,6 +266,10 @@ module Fast
148
266
  File.join(*dir, filename)
149
267
  end
150
268
 
269
+ # Keep track of ok experiments depending on the current combination.
270
+ # It keep the combinations unique removing single replacements after the
271
+ # first round.
272
+ # @return void
151
273
  def ok_with(combination)
152
274
  @ok_experiments << combination
153
275
  return unless combination.is_a?(Array)
@@ -157,16 +279,22 @@ module Fast
157
279
  end
158
280
  end
159
281
 
282
+ # Track failed experiments to avoid run them again.
283
+ # @return [void]
160
284
  def failed_with(combination)
161
285
  @fail_experiments << combination
162
286
  end
163
287
 
288
+ # @return [Array<Astrolabe::Node>]
164
289
  def search_cases
165
290
  Fast.search(@ast, experiment.expression) || []
166
291
  end
167
292
 
168
- # rubocop:disable Metrics/AbcSize
169
- # rubocop:disable Metrics/MethodLength
293
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
294
+ #
295
+ # Execute partial replacements generating new file with the
296
+ # content replaced.
297
+ # @return [void]
170
298
  def partial_replace(*indices)
171
299
  replacement = experiment.replacement
172
300
  new_content = Fast.replace_file @file, experiment.expression do |node, *captures|
@@ -183,11 +311,15 @@ module Fast
183
311
  write_experiment_file(indices, new_content)
184
312
  new_content
185
313
  end
314
+
186
315
  # rubocop:enable Metrics/AbcSize
187
316
  # rubocop:enable Metrics/MethodLength
188
317
 
189
- def write_experiment_file(index, new_content)
190
- filename = experimental_filename(index)
318
+ # Write new file name depending on the combination
319
+ # @param [Array<Integer>] combination
320
+ # @param [String] new_content to be persisted
321
+ def write_experiment_file(combination, new_content)
322
+ filename = experimental_filename(combination)
191
323
  File.open(filename, 'w+') { |f| f.puts new_content }
192
324
  filename
193
325
  end
@@ -203,6 +335,7 @@ module Fast
203
335
  `mv #{experimental_filename(perfect_combination)} #{@file}`
204
336
  end
205
337
 
338
+ # Increase the `@round` by 1 to {ExperimentCombinations#generate_combinations}.
206
339
  def build_combinations
207
340
  @round += 1
208
341
  ExperimentCombinations.new(
@@ -226,8 +359,11 @@ module Fast
226
359
  end
227
360
  done!
228
361
  end
229
-
230
- def run_partial_replacement_with(combination) # rubocop:disable Metrics/AbcSize
362
+ #
363
+ # Writes a new file with partial replacements based on the current combination.
364
+ # Raise error if no changes was made with the given combination indices.
365
+ # @param [Array<Integer>] combinations to be replaced.
366
+ def run_partial_replacement_with(combination)
231
367
  content = partial_replace(*combination)
232
368
  experimental_file = experimental_filename(combination)
233
369
 
@@ -237,9 +373,9 @@ module Fast
237
373
 
238
374
  result = experiment.ok_if.call(experimental_file)
239
375
 
240
- if result.success
376
+ if result
241
377
  ok_with(combination)
242
- puts "✅ #{experimental_file} - Combination: #{combination} - Time: #{result.execution_time}s"
378
+ puts "✅ #{experimental_file} - Combination: #{combination}"
243
379
  else
244
380
  failed_with(combination)
245
381
  puts "🔴 #{experimental_file} - Combination: #{combination}"
data/lib/fast/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fast
4
- VERSION = '0.0.8'
4
+ VERSION = '0.0.9'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffast
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jônatas Davi Paganini
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-27 00:00:00.000000000 Z
11
+ date: 2019-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: astrolabe