ffast 0.2.2 → 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 (54) 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 +102 -15
  6. data/README.md +21 -7
  7. data/bin/console +1 -1
  8. data/bin/fast-experiment +3 -0
  9. data/bin/fast-mcp +7 -0
  10. data/fast.gemspec +1 -3
  11. data/lib/fast/cli.rb +58 -26
  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 +16 -4
  20. data/lib/fast/source.rb +116 -0
  21. data/lib/fast/source_rewriter.rb +153 -0
  22. data/lib/fast/sql/rewriter.rb +36 -7
  23. data/lib/fast/sql.rb +15 -17
  24. data/lib/fast/summary.rb +435 -0
  25. data/lib/fast/version.rb +1 -1
  26. data/lib/fast.rb +140 -83
  27. data/mkdocs.yml +19 -4
  28. data/requirements-docs.txt +3 -0
  29. metadata +16 -59
  30. data/docs/command_line.md +0 -238
  31. data/docs/editors-integration.md +0 -46
  32. data/docs/experiments.md +0 -155
  33. data/docs/git.md +0 -115
  34. data/docs/ideas.md +0 -70
  35. data/docs/index.md +0 -404
  36. data/docs/pry-integration.md +0 -27
  37. data/docs/research.md +0 -93
  38. data/docs/shortcuts.md +0 -323
  39. data/docs/similarity_tutorial.md +0 -176
  40. data/docs/sql-support.md +0 -253
  41. data/docs/syntax.md +0 -395
  42. data/docs/videos.md +0 -16
  43. data/docs/walkthrough.md +0 -135
  44. data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
  45. data/examples/experimental_replacement.rb +0 -46
  46. data/examples/find_usage.rb +0 -26
  47. data/examples/let_it_be_experiment.rb +0 -11
  48. data/examples/method_complexity.rb +0 -37
  49. data/examples/search_duplicated.rb +0 -15
  50. data/examples/similarity_research.rb +0 -58
  51. data/examples/simple_rewriter.rb +0 -6
  52. data/experiments/let_it_be_experiment.rb +0 -9
  53. data/experiments/remove_useless_hook.rb +0 -9
  54. data/experiments/replace_create_with_build_stubbed.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce52e9ab5a9d9da32683b21584af707cf5c87e460f3bd74297e9038ea38c5494
4
- data.tar.gz: 2b68a433982c97f278829b95234aed4ea5e4aec19264f15f9bfbe8615da19c9a
3
+ metadata.gz: da30bc7abc983e2ac54c1c4d1f454878d75dce0dd285a0e245d6ed54d73e4040
4
+ data.tar.gz: f286ce93d5ce2970d1c3a528d423bf8b8ed9239140f798f895a67e0aa1b8e2d2
5
5
  SHA512:
6
- metadata.gz: 246e52812d6f332636363e9f8dd960fa5e5ad3ecc50666007ae62041a212efc0e17f79dc01a675e15cd862f04bcc6771ac2ede3c8fd98c656b0efe7f34b67ffc
7
- data.tar.gz: 641af33dbc8858631ae458bc53a4c1b7ce08c44da6788d0cdf7c8c3cae571a9bcb20630f3f9527aeea9a6023a920ac8fe8b0df83672b8280ab8415977114ff4b
6
+ metadata.gz: 83c218849cd4a9184b34f9d760d537cae650fe8a96428aa83fdeff12ed6afc57129fff3ce4a0c6d6924225e178f80c114afb021e19ca41bc2677c85b0668ee99
7
+ data.tar.gz: 6d4499e8a11398c3f9bc8a04adcd5efd65ef1b385c9bc37370edc31f9375b8d844e8ed61981e33831b2b83961edd4245059a38167c2f7a5ffbc52dd49acb9fff
@@ -0,0 +1,27 @@
1
+ name: Release Gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*' # Triggers when a new tag like v0.2.0 is pushed
7
+
8
+ jobs:
9
+ release:
10
+ name: Build and Release
11
+ runs-on: ubuntu-22.04
12
+ permissions:
13
+ contents: read # Required to checkout the code
14
+ id-token: write # Required for RubyGems Trusted Publishing
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up Ruby
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: '3.3'
24
+ bundler-cache: true
25
+
26
+ - name: Publish to RubyGems
27
+ uses: rubygems/release-gem@v1
@@ -0,0 +1,34 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ "master" ]
13
+ pull_request:
14
+ branches: [ "master" ]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+ runs-on: ubuntu-22.04
22
+ strategy:
23
+ matrix:
24
+ ruby-version: ['3.0', '3.1', '3.2', '3.3']
25
+
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - name: Set up Ruby
29
+ uses: ruby/setup-ruby@v1
30
+ with:
31
+ ruby-version: ${{ matrix.ruby-version }}
32
+ bundler-cache: true
33
+ - name: Run tests
34
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -5,8 +5,10 @@
5
5
  /coverage/
6
6
  /doc/
7
7
  /pkg/
8
+ /site/
8
9
  /spec/reports/
9
10
  /tmp/
11
+ .venv/
10
12
 
11
13
  # rspec failure tracking
12
14
  .rspec_status
data/Fastfile CHANGED
@@ -1,4 +1,10 @@
1
1
  # frozen_string_literal: true
2
+ begin
3
+ require 'fast/source'
4
+ rescue LoadError
5
+ nil
6
+ end
7
+
2
8
  # Fastfile is loaded when you start an expression with a dot.
3
9
  #
4
10
  # You can introduce shortcuts or methods that can be embedded during your
@@ -18,9 +24,11 @@ Fast.shortcut(:finders, '(class ... (const nil Find)', 'lib')
18
24
 
19
25
  # Simple shortcut that I used often to show how the expression parser works
20
26
  Fast.shortcut(:parser, '(class (const nil ExpressionParser)', 'lib/fast.rb')
27
+ Fast.shortcut(:sql_parser, '(def parse', 'lib/fast/sql.rb')
21
28
 
22
29
  # Use `fast .bump_version` to rewrite the version file
23
30
  Fast.shortcut :bump_version do
31
+ new_version = nil
24
32
  rewrite_file('(casgn nil VERSION (str _)', version_file) do |node|
25
33
  target = node.children.last.loc.expression
26
34
  pieces = target.source.split('.').map(&:to_i)
@@ -32,7 +40,24 @@ Fast.shortcut :bump_version do
32
40
  pieces[-(i + 1)] = 0
33
41
  end
34
42
  end
35
- replace(target, "'#{pieces.join('.')}'")
43
+ new_version = pieces.join('.')
44
+ replace(target, "'#{new_version}'")
45
+ end
46
+
47
+ print "Commit bumped version v#{new_version}? (y/n) "
48
+ if $stdin.gets.chomp.downcase == 'y'
49
+ system("git add #{version_file} && git commit -m 'Bump version to v#{new_version}'")
50
+
51
+ print "Tag version v#{new_version}? (y/n) "
52
+ if $stdin.gets.chomp.downcase == 'y'
53
+ system("git tag v#{new_version}")
54
+
55
+ print "Push commit and tag to origin? (y/n) "
56
+ if $stdin.gets.chomp.downcase == 'y'
57
+ system("git push origin HEAD")
58
+ system("git push origin v#{new_version}")
59
+ end
60
+ end
36
61
  end
37
62
  end
38
63
 
@@ -57,22 +82,9 @@ Fast.shortcut :intro do
57
82
  Fast.shortcuts[:walk].run
58
83
  end
59
84
 
60
- # Useful for `fast .walk file.md` but not required by the library.
61
- private
62
- def require_or_install_tty_md
63
- require 'tty-markdown'
64
- rescue LoadError
65
- puts 'Installing tty-markdown gem to better engage you :)'
66
- Gem.install('tty-markdown')
67
- puts 'Done! Now, back to our topic \o/'
68
- system('clear')
69
- retry
70
- end
71
-
72
85
  # Interactive command line walkthrough
73
86
  # fast .walk docs/walkthrough.md
74
87
  Fast.shortcut :walk do
75
- require_or_install_tty_md
76
88
  file = ARGV.last
77
89
  execute = ->(line) { system(line) }
78
90
  walk = ->(line) { line.each_char { |c| sleep(0.02) and print(c) } }
@@ -87,7 +99,82 @@ Fast.shortcut :walk do
87
99
  when /^!{3}\s/
88
100
  # Skip warnings that are only for web tutorials
89
101
  else
90
- walk[TTY::Markdown.parse(line)]
102
+ walk[Fast.render_markdown_for_terminal(line)]
91
103
  end
92
104
  end
93
105
  end
106
+
107
+ # Format SQL
108
+ Fast.shortcut :format_sql do
109
+ require 'fast/sql'
110
+ file = ARGV.last
111
+ method = File.exist?(file) ? :parse_sql_file : :parse_sql
112
+ ast = Fast.public_send(method, file)
113
+ ast = ast.first if ast.is_a? Array
114
+ eligible_kw = [:RESERVED_KEYWORD]
115
+ eligible_tokens = [:BY]
116
+
117
+ output = Fast::SQL.replace('_', ast) do |root|
118
+ sb = root.loc.expression.source_buffer
119
+ sb.tokens.each do |token|
120
+ if eligible_kw.include?(token.keyword_kind) || eligible_tokens.include?(token.token)
121
+ range = Fast::Source.range(sb, token.start, token.end)
122
+ replace(range, range.source.upcase)
123
+ end
124
+ end
125
+ end
126
+ require 'fast/cli'
127
+ puts Fast.highlight(output, sql: true)
128
+ end
129
+
130
+ # Anonymize SQL
131
+ # fast .anonymize_sql file.sql
132
+ Fast.shortcut :anonymize_sql do
133
+ require 'fast/sql'
134
+ file = ARGV.last
135
+ method = File.exist?(file) ? :parse_sql_file : :parse_sql
136
+ ast = Fast.public_send(method, file)
137
+ memo = {}
138
+
139
+ relnames = search("(relname $_)", ast).grep(String).uniq
140
+ pattern = "{relname (sval {#{relnames.map(&:inspect).join(' ')}})}"
141
+ puts "searching with #{pattern}"
142
+
143
+ content = Fast::SQL.replace(pattern, ast) do |node|
144
+ new_name = memo[node.source.tr(%|"'|, '')] ||= "x#{memo.size}"
145
+ new_name = "'#{new_name}'" if node.type == :sval
146
+ replace(node.loc.expression, new_name)
147
+ end
148
+ puts Fast.highlight(content, sql: true)
149
+ end
150
+
151
+ # Give all details in a shorter format
152
+ # fast .summary file.rb
153
+ Fast.shortcut :summary do
154
+ file = ARGV.reverse.find { |arg| !arg.start_with?('-') && File.exist?(arg) }
155
+ if file && File.exist?(file)
156
+ Fast.summary(IO.read(file), file: file, level: fast_option_value(ARGV, '-l', '--level')).summarize
157
+ else
158
+ puts "Please provide a valid file to summarize."
159
+ end
160
+ end
161
+
162
+ # Group and classify multiple Ruby files without printing full bodies
163
+ # fast .scan lib app/models --no-color
164
+ Fast.shortcut :scan do
165
+ locations = ARGV.select { |arg| !arg.start_with?('-') && File.exist?(arg) }
166
+ if locations.any?
167
+ Fast.scan(locations, level: fast_option_value(ARGV, '-l', '--level')).scan
168
+ else
169
+ puts "Please provide at least one valid file or directory to scan."
170
+ end
171
+ end
172
+
173
+ def fast_option_value(args, short_name, long_name)
174
+ args.each_with_index do |arg, index|
175
+ return args[index + 1] if arg == short_name || arg == long_name
176
+ return arg.split('=', 2).last if arg.start_with?("#{long_name}=")
177
+ end
178
+
179
+ nil
180
+ end
data/README.md CHANGED
@@ -12,6 +12,20 @@ the code was written without an AST.
12
12
 
13
13
  Check out the official documentation: https://jonatas.github.io/fast.
14
14
 
15
+ ## Documentation locally
16
+
17
+ The documentation site is built with MkDocs Material and a few Markdown
18
+ extensions. To run it locally:
19
+
20
+ ```bash
21
+ python3 -m venv .venv
22
+ source .venv/bin/activate
23
+ python3 -m pip install -r requirements-docs.txt
24
+ mkdocs serve
25
+ ```
26
+
27
+ Then open `http://127.0.0.1:8000`.
28
+
15
29
  ## Token Syntax for `find` in AST
16
30
 
17
31
  The current version of Fast covers the following token elements:
@@ -57,18 +71,18 @@ For example, let's take an `Integer` in Ruby:
57
71
  1
58
72
  ```
59
73
 
60
- It's corresponding s-expression would be:
74
+ Its corresponding s-expression would be:
61
75
 
62
76
  ```ruby
63
77
  s(:int, 1)
64
78
  ```
65
79
 
66
- `s` in `Fast` and `Parser` are a shorthand for creating an `Parser::AST::Node`.
80
+ `s` in `Fast` is a shorthand for creating a `Fast::Node`.
67
81
  Each of these nodes has a `#type` and `#children` contained in it:
68
82
 
69
83
  ```ruby
70
84
  def s(type, *children)
71
- Parser::AST::Node.new(type, children)
85
+ Fast::Node.new(type, children: children)
72
86
  end
73
87
  ```
74
88
 
@@ -198,7 +212,7 @@ def my_method
198
212
  end
199
213
  ```
200
214
 
201
- It's corresponding s-expression would be:
215
+ Its corresponding s-expression would be:
202
216
 
203
217
  ```ruby
204
218
  ast =
@@ -372,7 +386,7 @@ ast = Fast.ast("def name; person.name end")
372
386
  Generally, we use the `location.expression`:
373
387
 
374
388
  ```ruby
375
- ast.location.expression # => #<Parser::Source::Range (string) 0...25>
389
+ ast.location.expression # => #<Fast::Source::Range (string) 0...25>
376
390
  ```
377
391
 
378
392
  But location also brings some metadata about specific fragments:
@@ -383,7 +397,7 @@ ast.location.instance_variables # => [:@keyword, :@operator, :@name, :@end, :@ex
383
397
 
384
398
  Range for the keyword that identifies the method definition:
385
399
  ```ruby
386
- ast.location.keyword # => #<Parser::Source::Range (string) 0...3>
400
+ ast.location.keyword # => #<Fast::Source::Range (string) 0...3>
387
401
  ```
388
402
 
389
403
  You can always pick the source of a source range:
@@ -395,7 +409,7 @@ ast.location.keyword.source # => "def"
395
409
  Or only the method name:
396
410
 
397
411
  ```ruby
398
- ast.location.name # => #<Parser::Source::Range (string) 4...8>
412
+ ast.location.name # => #<Fast::Source::Range (string) 4...8>
399
413
  ast.location.name.source # => "name"
400
414
  ```
401
415
 
data/bin/console CHANGED
@@ -6,7 +6,7 @@ require 'fast'
6
6
  require 'fast/sql'
7
7
 
8
8
  def s(type, *children)
9
- Parser::AST::Node.new(type, children)
9
+ Fast::Node.new(type, children: children)
10
10
  end
11
11
 
12
12
  def code(string)
data/bin/fast-experiment CHANGED
@@ -8,6 +8,7 @@ require 'fast/experiment'
8
8
  require 'coderay'
9
9
 
10
10
  arguments = ARGV
11
+ autoclean = arguments.delete('--autoclean')
11
12
 
12
13
  experiment_files = Fast.ruby_files_from(File.expand_path('../experiments', __dir__))
13
14
  experiment_files.each(&method(:require))
@@ -23,6 +24,7 @@ end
23
24
  if arguments.any?
24
25
  ruby_files = arguments.all? { |e| File.exist?(e) && e.end_with?('.rb') }
25
26
  experiments.each do |experiment|
27
+ experiment.autoclean = autoclean
26
28
  if ruby_files
27
29
  experiment.files = arguments
28
30
  else
@@ -30,4 +32,5 @@ if arguments.any?
30
32
  end
31
33
  end
32
34
  end
35
+ experiments.each { |experiment| experiment.autoclean = autoclean }
33
36
  experiments.each(&:run)
data/bin/fast-mcp ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
5
+ require 'fast/mcp_server'
6
+
7
+ Fast::McpServer.run!
data/fast.gemspec CHANGED
@@ -17,7 +17,7 @@ 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|docs\/(assets|stylesheets))/})
20
+ f.match(%r{^(test|spec|experiments|examples|features|docs|assets|stylesheets|site)/})
21
21
  end
22
22
 
23
23
  spec.post_install_message = <<~THANKS
@@ -42,10 +42,8 @@ Gem::Specification.new do |spec|
42
42
  spec.executables = %w[fast fast-experiment]
43
43
  spec.require_paths = %w[lib experiments]
44
44
 
45
- spec.add_dependency 'astrolabe'
46
45
  spec.add_dependency 'coderay'
47
46
  spec.add_dependency 'parallel'
48
- spec.add_dependency 'parser'
49
47
  spec.add_dependency 'pg_query'
50
48
 
51
49
  spec.add_development_dependency 'bundler'
data/lib/fast/cli.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fast'
4
+ require 'fast/source'
4
5
  require 'fast/version'
5
6
  require 'fast/sql'
6
7
  require 'coderay'
@@ -17,12 +18,21 @@ module Fast
17
18
  # Useful for printing code with syntax highlight.
18
19
  # @param show_sexp [Boolean] prints node expression instead of code
19
20
  # @param colorize [Boolean] skips `CodeRay` processing when false.
20
- def highlight(node, show_sexp: false, colorize: true, sql: false)
21
- output =
22
- if node.respond_to?(:loc) && !show_sexp
23
- wrap_source_range(node).source
24
- else
25
- 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
26
36
  end
27
37
  return output unless colorize
28
38
 
@@ -33,7 +43,7 @@ module Fast
33
43
  # and fixes end of the expression including heredoc strings.
34
44
  def wrap_source_range(node)
35
45
  expression = node.loc.expression
36
- Parser::Source::Range.new(
46
+ Fast::Source.range(
37
47
  expression.source_buffer,
38
48
  first_position_from_expression(node),
39
49
  last_position_from_expression(node) || expression.end_pos
@@ -50,7 +60,7 @@ module Fast
50
60
  # to show the proper whitespaces for identing the next lines of the code.
51
61
  def first_position_from_expression(node)
52
62
  expression = node.loc.expression
53
- if node.parent && node.parent.loc.expression.line != expression.line
63
+ if node.respond_to?(:parent) && node.parent && node.parent.loc.expression.line != expression.line
54
64
  expression.begin_pos - expression.column
55
65
  else
56
66
  expression.begin_pos
@@ -59,15 +69,16 @@ module Fast
59
69
 
60
70
  # Combines {.highlight} with files printing file name in the head with the
61
71
  # source line.
62
- # @param result [Astrolabe::Node]
72
+ # @param result [Fast::Node]
63
73
  # @param show_sexp [Boolean] Show string expression instead of source
64
74
  # @param file [String] Show the file name and result line before content
65
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
66
77
  # @example
67
78
  # Fast.report(result, file: 'file.rb')
68
- def report(result, show_link: false, show_permalink: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true) # rubocop:disable Metrics/ParameterLists
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
69
80
  if file
70
- 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)
71
82
  if show_link
72
83
  puts(result.link)
73
84
  elsif show_permalink
@@ -76,23 +87,20 @@ module Fast
76
87
  puts(highlight("# #{file}:#{line}", colorize: colorize))
77
88
  end
78
89
  end
79
- 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
80
91
  end
81
92
 
82
93
  # Command Line Interface for Fast
83
94
  class Cli # rubocop:disable Metrics/ClassLength
84
- attr_reader :pattern, :show_sexp, :pry, :from_code, :similar, :help
95
+ attr_reader :pattern, :show_sexp, :pry, :from_code, :similar, :help, :level
85
96
  def initialize(args)
86
- args = replace_args_with_shortcut(args) if args.first&.start_with?('.')
87
-
88
- @pattern, *@files = args.reject { |arg| arg.start_with? '-' }
97
+ args = args.dup
98
+ args = replace_args_with_shortcut(args) if shortcut_name_from(args)
89
99
  @colorize = STDOUT.isatty
90
-
91
100
  option_parser.parse! args
101
+ @pattern, @files = extract_pattern_and_files(args)
92
102
 
93
- @files = [*@files].reject { |arg| arg.start_with?('-') }
94
103
  @sql ||= @files.any? && @files.all? { |file| file.end_with?('.sql') }
95
-
96
104
  require 'fast/sql' if @sql
97
105
  end
98
106
 
@@ -103,6 +111,10 @@ module Fast
103
111
  @debug = true
104
112
  end
105
113
 
114
+ opts.on('-l', '--level LEVELS', 'Maximum depth to print the AST') do |level|
115
+ @level = level.to_i
116
+ end
117
+
106
118
  opts.on('--ast', 'Print AST instead of code') do
107
119
  @show_sexp = true
108
120
  end
@@ -166,13 +178,14 @@ module Fast
166
178
  end
167
179
 
168
180
  def replace_args_with_shortcut(args)
169
- shortcut = find_shortcut args.first[1..]
181
+ shortcut_name = shortcut_name_from(args)
182
+ shortcut = find_shortcut(shortcut_name)
170
183
 
171
184
  if shortcut.single_run_with_block?
172
185
  shortcut.run
173
186
  exit
174
187
  else
175
- args.one? ? shortcut.args : shortcut.merge_args(args[1..])
188
+ shortcut.args
176
189
  end
177
190
  end
178
191
 
@@ -206,7 +219,7 @@ module Fast
206
219
 
207
220
  if @files.empty?
208
221
  ast ||= Fast.public_send( @sql ? :parse_sql : :ast, @pattern)
209
- puts Fast.highlight(ast, show_sexp: @show_sexp, colorize: @colorize, sql: @sql)
222
+ puts Fast.highlight(ast, show_sexp: @show_sexp, colorize: @colorize, sql: @sql, level: @level)
210
223
  else
211
224
  search
212
225
  end
@@ -272,20 +285,39 @@ module Fast
272
285
  show_sexp: @show_sexp,
273
286
  headless: @headless,
274
287
  bodyless: @bodyless,
275
- 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
276
308
  end
277
309
 
278
310
  # Find shortcut by name. Preloads all `Fastfiles` before start.
279
311
  # @param name [String]
280
- # @return [Fast::Shortcut]
281
312
  def find_shortcut(name)
282
313
  unless defined? Fast::Shortcut
283
314
  require 'fast/shortcut'
284
315
  Fast.load_fast_files!
285
316
  end
286
317
 
287
- shortcut = Fast.shortcuts[name] || Fast.shortcuts[name.to_sym]
288
- shortcut || exit_shortcut_not_found(name)
318
+ shortcut = Fast.shortcuts[name.to_sym]
319
+ exit_shortcut_not_found(name) unless shortcut
320
+ shortcut
289
321
  end
290
322
 
291
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