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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +27 -0
  3. data/.github/workflows/ruby.yml +34 -0
  4. data/.gitignore +2 -0
  5. data/Fastfile +146 -3
  6. data/README.md +244 -132
  7. data/bin/console +6 -1
  8. data/bin/fast-experiment +3 -0
  9. data/bin/fast-mcp +7 -0
  10. data/fast.gemspec +24 -7
  11. data/lib/fast/cli.rb +129 -38
  12. data/lib/fast/experiment.rb +19 -2
  13. data/lib/fast/git.rb +1 -1
  14. data/lib/fast/mcp_server.rb +317 -0
  15. data/lib/fast/node.rb +258 -0
  16. data/lib/fast/prism_adapter.rb +310 -0
  17. data/lib/fast/rewriter.rb +64 -10
  18. data/lib/fast/scan.rb +203 -0
  19. data/lib/fast/shortcut.rb +23 -6
  20. data/lib/fast/source.rb +116 -0
  21. data/lib/fast/source_rewriter.rb +153 -0
  22. data/lib/fast/sql/rewriter.rb +98 -0
  23. data/lib/fast/sql.rb +165 -0
  24. data/lib/fast/summary.rb +435 -0
  25. data/lib/fast/version.rb +1 -1
  26. data/lib/fast.rb +165 -79
  27. data/mkdocs.yml +27 -3
  28. data/requirements-docs.txt +3 -0
  29. metadata +48 -62
  30. data/docs/command_line.md +0 -238
  31. data/docs/editors-integration.md +0 -46
  32. data/docs/experiments.md +0 -153
  33. data/docs/ideas.md +0 -80
  34. data/docs/index.md +0 -402
  35. data/docs/pry-integration.md +0 -27
  36. data/docs/research.md +0 -93
  37. data/docs/shortcuts.md +0 -323
  38. data/docs/similarity_tutorial.md +0 -176
  39. data/docs/syntax.md +0 -395
  40. data/docs/videos.md +0 -16
  41. data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
  42. data/examples/experimental_replacement.rb +0 -46
  43. data/examples/find_usage.rb +0 -26
  44. data/examples/let_it_be_experiment.rb +0 -11
  45. data/examples/method_complexity.rb +0 -37
  46. data/examples/search_duplicated.rb +0 -15
  47. data/examples/similarity_research.rb +0 -58
  48. data/examples/simple_rewriter.rb +0 -6
  49. data/experiments/let_it_be_experiment.rb +0 -9
  50. data/experiments/remove_useless_hook.rb +0 -9
  51. 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 'parser'
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', '~> 3.0'
40
- spec.add_development_dependency 'rspec-its', '~> 1.2'
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', '~> 0.10', '< 0.18'
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
- def highlight(node, show_sexp: false, colorize: true)
20
- output =
21
- if node.respond_to?(:loc) && !show_sexp
22
- node.loc.expression.source
23
- else
24
- node
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 [Astrolabe::Node]
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.highlight(Fast.search(...))
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.is_a?(Parser::AST::Node)
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 = replace_args_with_shortcut(args) if args.first&.start_with?('.')
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 = [*@files].reject { |arg| arg.start_with?('-') }
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 instead of code') do
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
- shortcut = find_shortcut args.first[1..]
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
- args.one? ? shortcut.args : shortcut.merge_args(args[1..])
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
- expression,
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
- require 'fast/shortcut'
226
- Fast.load_fast_files!
313
+ unless defined? Fast::Shortcut
314
+ require 'fast/shortcut'
315
+ Fast.load_fast_files!
316
+ end
227
317
 
228
- shortcut = Fast.shortcuts[name] || Fast.shortcuts[name.to_sym]
229
- shortcut || exit_shortcut_not_found(name)
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.
@@ -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<Astrolabe::Node>]
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
- return unless perfect_combination = @ok_experiments.last # rubocop:disable Lint/AssignmentInCondition
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 < Astrolabe::Node
11
+ class Node
12
12
  # @return [Git::Base] from current directory
13
13
  def git
14
14
  require 'git' unless defined? Git