ffast 0.2.0 → 0.2.3
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/.github/workflows/release.yml +27 -0
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +2 -0
- data/Fastfile +146 -3
- data/README.md +244 -132
- data/bin/console +6 -1
- data/bin/fast-experiment +3 -0
- data/bin/fast-mcp +7 -0
- data/fast.gemspec +24 -7
- data/lib/fast/cli.rb +129 -38
- data/lib/fast/experiment.rb +19 -2
- data/lib/fast/git.rb +1 -1
- data/lib/fast/mcp_server.rb +317 -0
- data/lib/fast/node.rb +258 -0
- data/lib/fast/prism_adapter.rb +310 -0
- data/lib/fast/rewriter.rb +64 -10
- data/lib/fast/scan.rb +203 -0
- data/lib/fast/shortcut.rb +23 -6
- data/lib/fast/source.rb +116 -0
- data/lib/fast/source_rewriter.rb +153 -0
- data/lib/fast/sql/rewriter.rb +98 -0
- data/lib/fast/sql.rb +165 -0
- data/lib/fast/summary.rb +435 -0
- data/lib/fast/version.rb +1 -1
- data/lib/fast.rb +165 -79
- data/mkdocs.yml +27 -3
- data/requirements-docs.txt +3 -0
- metadata +48 -62
- data/docs/command_line.md +0 -238
- data/docs/editors-integration.md +0 -46
- data/docs/experiments.md +0 -153
- data/docs/ideas.md +0 -80
- data/docs/index.md +0 -402
- data/docs/pry-integration.md +0 -27
- data/docs/research.md +0 -93
- data/docs/shortcuts.md +0 -323
- data/docs/similarity_tutorial.md +0 -176
- data/docs/syntax.md +0 -395
- data/docs/videos.md +0 -16
- data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
- data/examples/experimental_replacement.rb +0 -46
- data/examples/find_usage.rb +0 -26
- data/examples/let_it_be_experiment.rb +0 -11
- data/examples/method_complexity.rb +0 -37
- data/examples/search_duplicated.rb +0 -15
- data/examples/similarity_research.rb +0 -58
- data/examples/simple_rewriter.rb +0 -6
- data/experiments/let_it_be_experiment.rb +0 -9
- data/experiments/remove_useless_hook.rb +0 -9
- data/experiments/replace_create_with_build_stubbed.rb +0 -10
data/fast.gemspec
CHANGED
|
@@ -17,29 +17,46 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.license = 'MIT'
|
|
18
18
|
|
|
19
19
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
20
|
-
f.match(%r{^(test|spec|features)/})
|
|
20
|
+
f.match(%r{^(test|spec|experiments|examples|features|docs|assets|stylesheets|site)/})
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
spec.post_install_message = <<~THANKS
|
|
24
|
+
|
|
25
|
+
==========================================================
|
|
26
|
+
Yay! Thanks for installing
|
|
27
|
+
|
|
28
|
+
___ __ ___
|
|
29
|
+
|__ /\ /__` |
|
|
30
|
+
| /~~\ .__/ |
|
|
31
|
+
|
|
32
|
+
To interactive learn about the gem in the terminal use:
|
|
33
|
+
|
|
34
|
+
fast .intro
|
|
35
|
+
|
|
36
|
+
More docs at: https://jonatas.github.io/fast/
|
|
37
|
+
==========================================================
|
|
38
|
+
|
|
39
|
+
THANKS
|
|
40
|
+
|
|
23
41
|
spec.bindir = 'bin'
|
|
24
42
|
spec.executables = %w[fast fast-experiment]
|
|
25
43
|
spec.require_paths = %w[lib experiments]
|
|
26
44
|
|
|
27
|
-
spec.add_dependency 'astrolabe'
|
|
28
45
|
spec.add_dependency 'coderay'
|
|
29
46
|
spec.add_dependency 'parallel'
|
|
30
|
-
spec.add_dependency '
|
|
47
|
+
spec.add_dependency 'pg_query'
|
|
31
48
|
|
|
32
49
|
spec.add_development_dependency 'bundler'
|
|
50
|
+
spec.add_development_dependency 'git'
|
|
33
51
|
spec.add_development_dependency 'guard'
|
|
34
52
|
spec.add_development_dependency 'guard-livereload'
|
|
35
53
|
spec.add_development_dependency 'guard-rspec'
|
|
36
54
|
spec.add_development_dependency 'pry'
|
|
37
|
-
spec.add_development_dependency 'git'
|
|
38
55
|
spec.add_development_dependency 'rake'
|
|
39
|
-
spec.add_development_dependency 'rspec'
|
|
40
|
-
spec.add_development_dependency 'rspec-its'
|
|
56
|
+
spec.add_development_dependency 'rspec'
|
|
57
|
+
spec.add_development_dependency 'rspec-its'
|
|
41
58
|
spec.add_development_dependency 'rubocop'
|
|
42
59
|
spec.add_development_dependency 'rubocop-performance'
|
|
43
60
|
spec.add_development_dependency 'rubocop-rspec'
|
|
44
|
-
spec.add_development_dependency 'simplecov'
|
|
61
|
+
spec.add_development_dependency 'simplecov'
|
|
45
62
|
end
|
data/lib/fast/cli.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fast'
|
|
4
|
+
require 'fast/source'
|
|
4
5
|
require 'fast/version'
|
|
6
|
+
require 'fast/sql'
|
|
5
7
|
require 'coderay'
|
|
6
8
|
require 'optparse'
|
|
7
9
|
require 'ostruct'
|
|
@@ -16,50 +18,90 @@ module Fast
|
|
|
16
18
|
# Useful for printing code with syntax highlight.
|
|
17
19
|
# @param show_sexp [Boolean] prints node expression instead of code
|
|
18
20
|
# @param colorize [Boolean] skips `CodeRay` processing when false.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
node.loc
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
# @param level [Integer] defines the max depth to print the AST.
|
|
22
|
+
def highlight(node, show_sexp: false, colorize: true, sql: false, level: nil)
|
|
23
|
+
output =
|
|
24
|
+
if node.respond_to?(:loc) && !show_sexp
|
|
25
|
+
if level
|
|
26
|
+
Fast.fold_source(node, level: level)
|
|
27
|
+
else
|
|
28
|
+
wrap_source_range(node).source
|
|
29
|
+
end
|
|
30
|
+
elsif show_sexp && level && Fast.ast_node?(node)
|
|
31
|
+
Fast.fold_ast(node, level: level).to_s
|
|
32
|
+
elsif show_sexp
|
|
33
|
+
node.to_s
|
|
34
|
+
else
|
|
35
|
+
node
|
|
25
36
|
end
|
|
26
37
|
return output unless colorize
|
|
27
38
|
|
|
28
|
-
CodeRay.scan(output, :ruby).term
|
|
39
|
+
CodeRay.scan(output, sql ? :sql : :ruby).term
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Fixes initial spaces to print the line since the beginning
|
|
43
|
+
# and fixes end of the expression including heredoc strings.
|
|
44
|
+
def wrap_source_range(node)
|
|
45
|
+
expression = node.loc.expression
|
|
46
|
+
Fast::Source.range(
|
|
47
|
+
expression.source_buffer,
|
|
48
|
+
first_position_from_expression(node),
|
|
49
|
+
last_position_from_expression(node) || expression.end_pos
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# If a method call contains a heredoc, it should print the STR around it too.
|
|
54
|
+
def last_position_from_expression(node)
|
|
55
|
+
internal_heredoc = node.each_descendant(:str).select { |n| n.loc.respond_to?(:heredoc_end) }
|
|
56
|
+
internal_heredoc.map { |n| n.loc.heredoc_end.end_pos }.max if internal_heredoc.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# If a node is the first on it's line, print since the beginning of the line
|
|
60
|
+
# to show the proper whitespaces for identing the next lines of the code.
|
|
61
|
+
def first_position_from_expression(node)
|
|
62
|
+
expression = node.loc.expression
|
|
63
|
+
if node.respond_to?(:parent) && node.parent && node.parent.loc.expression.line != expression.line
|
|
64
|
+
expression.begin_pos - expression.column
|
|
65
|
+
else
|
|
66
|
+
expression.begin_pos
|
|
67
|
+
end
|
|
29
68
|
end
|
|
30
69
|
|
|
31
70
|
# Combines {.highlight} with files printing file name in the head with the
|
|
32
71
|
# source line.
|
|
33
|
-
# @param result [
|
|
72
|
+
# @param result [Fast::Node]
|
|
34
73
|
# @param show_sexp [Boolean] Show string expression instead of source
|
|
35
74
|
# @param file [String] Show the file name and result line before content
|
|
36
75
|
# @param headless [Boolean] Skip printing the file name and line before content
|
|
76
|
+
# @param level [Integer] Skip exploring deep branches of AST when showing sexp
|
|
37
77
|
# @example
|
|
38
|
-
# Fast.
|
|
39
|
-
def report(result, show_link: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true) # rubocop:disable Metrics/ParameterLists
|
|
78
|
+
# Fast.report(result, file: 'file.rb')
|
|
79
|
+
def report(result, show_link: false, show_permalink: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true, level: nil) # rubocop:disable Metrics/ParameterLists
|
|
40
80
|
if file
|
|
41
|
-
line = result.loc.expression.line if result.
|
|
81
|
+
line = result.loc.expression.line if Fast.ast_node?(result) && result.respond_to?(:loc)
|
|
42
82
|
if show_link
|
|
43
83
|
puts(result.link)
|
|
84
|
+
elsif show_permalink
|
|
85
|
+
puts(result.permalink)
|
|
44
86
|
elsif !headless
|
|
45
87
|
puts(highlight("# #{file}:#{line}", colorize: colorize))
|
|
46
88
|
end
|
|
47
89
|
end
|
|
48
|
-
puts(highlight(result, show_sexp: show_sexp, colorize: colorize)) unless bodyless
|
|
90
|
+
puts(highlight(result, show_sexp: show_sexp, colorize: colorize, level: level)) unless bodyless
|
|
49
91
|
end
|
|
50
92
|
|
|
51
93
|
# Command Line Interface for Fast
|
|
52
94
|
class Cli # rubocop:disable Metrics/ClassLength
|
|
53
|
-
attr_reader :pattern, :show_sexp, :pry, :from_code, :similar, :help
|
|
95
|
+
attr_reader :pattern, :show_sexp, :pry, :from_code, :similar, :help, :level
|
|
54
96
|
def initialize(args)
|
|
55
|
-
args =
|
|
56
|
-
|
|
57
|
-
@pattern, *@files = args.reject { |arg| arg.start_with? '-' }
|
|
97
|
+
args = args.dup
|
|
98
|
+
args = replace_args_with_shortcut(args) if shortcut_name_from(args)
|
|
58
99
|
@colorize = STDOUT.isatty
|
|
59
|
-
|
|
60
100
|
option_parser.parse! args
|
|
101
|
+
@pattern, @files = extract_pattern_and_files(args)
|
|
61
102
|
|
|
62
|
-
@files
|
|
103
|
+
@sql ||= @files.any? && @files.all? { |file| file.end_with?('.sql') }
|
|
104
|
+
require 'fast/sql' if @sql
|
|
63
105
|
end
|
|
64
106
|
|
|
65
107
|
def option_parser # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
@@ -69,19 +111,32 @@ module Fast
|
|
|
69
111
|
@debug = true
|
|
70
112
|
end
|
|
71
113
|
|
|
114
|
+
opts.on('-l', '--level LEVELS', 'Maximum depth to print the AST') do |level|
|
|
115
|
+
@level = level.to_i
|
|
116
|
+
end
|
|
117
|
+
|
|
72
118
|
opts.on('--ast', 'Print AST instead of code') do
|
|
73
119
|
@show_sexp = true
|
|
74
120
|
end
|
|
75
121
|
|
|
76
|
-
opts.on('--link', 'Print link to repository URL
|
|
122
|
+
opts.on('--link', 'Print link to repository URL') do
|
|
77
123
|
require 'fast/git'
|
|
78
124
|
@show_link = true
|
|
79
125
|
end
|
|
80
126
|
|
|
127
|
+
opts.on('--permalink', 'Print permalink to repository URL') do
|
|
128
|
+
require 'fast/git'
|
|
129
|
+
@show_permalink = true
|
|
130
|
+
end
|
|
131
|
+
|
|
81
132
|
opts.on('-p', '--parallel', 'Paralelize search') do
|
|
82
133
|
@parallel = true
|
|
83
134
|
end
|
|
84
135
|
|
|
136
|
+
opts.on("--sql", "Use SQL instead of Ruby") do
|
|
137
|
+
@sql = true
|
|
138
|
+
end
|
|
139
|
+
|
|
85
140
|
opts.on('--captures', 'Print only captures of the patterns and skip node results') do
|
|
86
141
|
@captures = true
|
|
87
142
|
end
|
|
@@ -99,24 +154,18 @@ module Fast
|
|
|
99
154
|
require 'pry'
|
|
100
155
|
end
|
|
101
156
|
|
|
102
|
-
opts.on('-c', '--code', 'Create a pattern from code example') do
|
|
103
|
-
if @pattern
|
|
104
|
-
@from_code = true
|
|
105
|
-
@pattern = Fast.ast(@pattern).to_sexp
|
|
106
|
-
debug 'Expression from AST:', @pattern
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
157
|
opts.on('-s', '--similar', 'Search for similar code.') do
|
|
111
158
|
@similar = true
|
|
112
|
-
@pattern = Fast.expression_from(Fast.ast(@pattern))
|
|
113
|
-
debug "Looking for code similar to #{@pattern}"
|
|
114
159
|
end
|
|
115
160
|
|
|
116
161
|
opts.on('--no-color', 'Disable color output') do
|
|
117
162
|
@colorize = false
|
|
118
163
|
end
|
|
119
164
|
|
|
165
|
+
opts.on('--from-code', 'From code') do
|
|
166
|
+
@from_code = true
|
|
167
|
+
end
|
|
168
|
+
|
|
120
169
|
opts.on_tail('--version', 'Show version') do
|
|
121
170
|
puts Fast::VERSION
|
|
122
171
|
exit
|
|
@@ -129,13 +178,14 @@ module Fast
|
|
|
129
178
|
end
|
|
130
179
|
|
|
131
180
|
def replace_args_with_shortcut(args)
|
|
132
|
-
|
|
181
|
+
shortcut_name = shortcut_name_from(args)
|
|
182
|
+
shortcut = find_shortcut(shortcut_name)
|
|
133
183
|
|
|
134
184
|
if shortcut.single_run_with_block?
|
|
135
185
|
shortcut.run
|
|
136
186
|
exit
|
|
137
187
|
else
|
|
138
|
-
|
|
188
|
+
shortcut.args
|
|
139
189
|
end
|
|
140
190
|
end
|
|
141
191
|
|
|
@@ -151,6 +201,25 @@ module Fast
|
|
|
151
201
|
|
|
152
202
|
if @help || @files.empty? && @pattern.nil?
|
|
153
203
|
puts option_parser.help
|
|
204
|
+
return
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if @similar
|
|
208
|
+
ast = Fast.public_send( @sql ? :parse_sql : :ast, @pattern)
|
|
209
|
+
@pattern = Fast.expression_from(ast)
|
|
210
|
+
debug "Search similar to #{@pattern}"
|
|
211
|
+
elsif @from_code
|
|
212
|
+
ast = Fast.public_send( @sql ? :parse_sql : :ast, @pattern)
|
|
213
|
+
@pattern = ast.to_sexp
|
|
214
|
+
if @sql
|
|
215
|
+
@pattern.gsub!(/\b-\b/,'_')
|
|
216
|
+
end
|
|
217
|
+
debug "Search from code to #{@pattern}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if @files.empty?
|
|
221
|
+
ast ||= Fast.public_send( @sql ? :parse_sql : :ast, @pattern)
|
|
222
|
+
puts Fast.highlight(ast, show_sexp: @show_sexp, colorize: @colorize, sql: @sql, level: @level)
|
|
154
223
|
else
|
|
155
224
|
search
|
|
156
225
|
end
|
|
@@ -181,7 +250,7 @@ module Fast
|
|
|
181
250
|
# @yieldparam [String, Array] with file and respective search results
|
|
182
251
|
def execute_search(&on_result)
|
|
183
252
|
Fast.public_send(search_method_name,
|
|
184
|
-
|
|
253
|
+
@pattern,
|
|
185
254
|
@files,
|
|
186
255
|
parallel: parallel?,
|
|
187
256
|
on_result: on_result)
|
|
@@ -212,21 +281,43 @@ module Fast
|
|
|
212
281
|
Fast.report(result,
|
|
213
282
|
file: file,
|
|
214
283
|
show_link: @show_link,
|
|
284
|
+
show_permalink: @show_permalink,
|
|
215
285
|
show_sexp: @show_sexp,
|
|
216
286
|
headless: @headless,
|
|
217
287
|
bodyless: @bodyless,
|
|
218
|
-
colorize: @colorize
|
|
288
|
+
colorize: @colorize,
|
|
289
|
+
level: @level)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def shortcut_name_from(args)
|
|
293
|
+
command = args.find { |arg| !arg.start_with?('-') }
|
|
294
|
+
return unless command&.start_with?('.')
|
|
295
|
+
|
|
296
|
+
command[1..]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def extract_pattern_and_files(args)
|
|
300
|
+
return [nil, []] if args.empty?
|
|
301
|
+
|
|
302
|
+
files_start = args.index { |arg| File.exist?(arg) || File.directory?(arg) }
|
|
303
|
+
if files_start
|
|
304
|
+
[args[0...files_start].join(' '), args[files_start..]]
|
|
305
|
+
else
|
|
306
|
+
[args.join(' '), []]
|
|
307
|
+
end
|
|
219
308
|
end
|
|
220
309
|
|
|
221
310
|
# Find shortcut by name. Preloads all `Fastfiles` before start.
|
|
222
311
|
# @param name [String]
|
|
223
|
-
# @return [Fast::Shortcut]
|
|
224
312
|
def find_shortcut(name)
|
|
225
|
-
|
|
226
|
-
|
|
313
|
+
unless defined? Fast::Shortcut
|
|
314
|
+
require 'fast/shortcut'
|
|
315
|
+
Fast.load_fast_files!
|
|
316
|
+
end
|
|
227
317
|
|
|
228
|
-
shortcut = Fast.shortcuts[name
|
|
229
|
-
|
|
318
|
+
shortcut = Fast.shortcuts[name.to_sym]
|
|
319
|
+
exit_shortcut_not_found(name) unless shortcut
|
|
320
|
+
shortcut
|
|
230
321
|
end
|
|
231
322
|
|
|
232
323
|
# Exit process with warning message bolding the shortcut that was not found.
|
data/lib/fast/experiment.rb
CHANGED
|
@@ -91,6 +91,7 @@ module Fast
|
|
|
91
91
|
class Experiment
|
|
92
92
|
attr_writer :files
|
|
93
93
|
attr_reader :name, :replacement, :expression, :files_or_folders, :ok_if
|
|
94
|
+
attr_accessor :autoclean
|
|
94
95
|
|
|
95
96
|
def initialize(name, &block)
|
|
96
97
|
@name = name
|
|
@@ -137,6 +138,10 @@ module Fast
|
|
|
137
138
|
def run
|
|
138
139
|
files.map(&method(:run_with))
|
|
139
140
|
end
|
|
141
|
+
|
|
142
|
+
def autoclean?
|
|
143
|
+
!!@autoclean
|
|
144
|
+
end
|
|
140
145
|
end
|
|
141
146
|
|
|
142
147
|
# Suggest possible combinations of occurrences to replace.
|
|
@@ -285,7 +290,7 @@ module Fast
|
|
|
285
290
|
@fail_experiments << combination
|
|
286
291
|
end
|
|
287
292
|
|
|
288
|
-
# @return [Array<
|
|
293
|
+
# @return [Array<Fast::Node>]
|
|
289
294
|
def search_cases
|
|
290
295
|
Fast.search(experiment.expression, @ast) || []
|
|
291
296
|
end
|
|
@@ -323,15 +328,25 @@ module Fast
|
|
|
323
328
|
filename
|
|
324
329
|
end
|
|
325
330
|
|
|
331
|
+
def cleanup_generated_files!
|
|
332
|
+
Dir.glob(File.join(File.dirname(@file), "experiment_*_#{File.basename(@file)}")).each do |generated_file|
|
|
333
|
+
File.delete(generated_file) if File.exist?(generated_file)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
326
337
|
def done!
|
|
327
338
|
count_executed_combinations = @fail_experiments.size + @ok_experiments.size
|
|
328
339
|
puts "Done with #{@file} after #{count_executed_combinations} combinations"
|
|
329
|
-
|
|
340
|
+
unless perfect_combination = @ok_experiments.last # rubocop:disable Lint/AssignmentInCondition
|
|
341
|
+
cleanup_generated_files! if experiment.autoclean?
|
|
342
|
+
return
|
|
343
|
+
end
|
|
330
344
|
|
|
331
345
|
puts 'The following changes were applied to the file:'
|
|
332
346
|
`diff #{experimental_filename(perfect_combination)} #{@file}`
|
|
333
347
|
puts "mv #{experimental_filename(perfect_combination)} #{@file}"
|
|
334
348
|
`mv #{experimental_filename(perfect_combination)} #{@file}`
|
|
349
|
+
cleanup_generated_files! if experiment.autoclean?
|
|
335
350
|
end
|
|
336
351
|
|
|
337
352
|
# Increase the `@round` by 1 to {ExperimentCombinations#generate_combinations}.
|
|
@@ -346,6 +361,7 @@ module Fast
|
|
|
346
361
|
end
|
|
347
362
|
|
|
348
363
|
def run
|
|
364
|
+
cleanup_generated_files! if experiment.autoclean?
|
|
349
365
|
while (combinations = build_combinations).any?
|
|
350
366
|
if combinations.size > 1000
|
|
351
367
|
puts "Ignoring #{@file} because it has #{combinations.size} possible combinations"
|
|
@@ -378,6 +394,7 @@ module Fast
|
|
|
378
394
|
else
|
|
379
395
|
failed_with(combination)
|
|
380
396
|
puts "🔴 #{experimental_file} - Combination: #{combination}"
|
|
397
|
+
File.delete(experimental_file) if experiment.autoclean? && File.exist?(experimental_file)
|
|
381
398
|
end
|
|
382
399
|
end
|
|
383
400
|
end
|
data/lib/fast/git.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Fast
|
|
|
8
8
|
# @example
|
|
9
9
|
# require 'fast/git'
|
|
10
10
|
# Fast.ast_from_file('lib/fast.rb').git_log.first.author.name # => "Jonatas Davi Paganini"
|
|
11
|
-
class Node
|
|
11
|
+
class Node
|
|
12
12
|
# @return [Git::Base] from current directory
|
|
13
13
|
def git
|
|
14
14
|
require 'git' unless defined? Git
|