ffast 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|