ffast 0.0.8 → 0.0.9

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