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.
- checksums.yaml +4 -4
- data/README.md +191 -263
- data/docs/index.md +2 -4
- data/lib/fast.rb +239 -53
- data/lib/fast/experiment.rb +164 -28
- data/lib/fast/version.rb +1 -1
- metadata +2 -2
data/lib/fast/experiment.rb
CHANGED
@@ -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
|
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
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
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
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
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
|
-
#
|
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
|
-
#
|
126
|
-
#
|
127
|
-
#
|
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
|
-
#
|
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
|
-
|
190
|
-
|
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
|
-
|
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
|
376
|
+
if result
|
241
377
|
ok_with(combination)
|
242
|
-
puts "✅ #{experimental_file} - Combination: #{combination}
|
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
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.
|
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-
|
11
|
+
date: 2019-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: astrolabe
|