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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +27 -0
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +2 -0
- data/Fastfile +102 -15
- data/README.md +21 -7
- data/bin/console +1 -1
- data/bin/fast-experiment +3 -0
- data/bin/fast-mcp +7 -0
- data/fast.gemspec +1 -3
- data/lib/fast/cli.rb +58 -26
- 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 +16 -4
- data/lib/fast/source.rb +116 -0
- data/lib/fast/source_rewriter.rb +153 -0
- data/lib/fast/sql/rewriter.rb +36 -7
- data/lib/fast/sql.rb +15 -17
- data/lib/fast/summary.rb +435 -0
- data/lib/fast/version.rb +1 -1
- data/lib/fast.rb +140 -83
- data/mkdocs.yml +19 -4
- data/requirements-docs.txt +3 -0
- metadata +16 -59
- data/docs/command_line.md +0 -238
- data/docs/editors-integration.md +0 -46
- data/docs/experiments.md +0 -155
- data/docs/git.md +0 -115
- data/docs/ideas.md +0 -70
- data/docs/index.md +0 -404
- 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/sql-support.md +0 -253
- data/docs/syntax.md +0 -395
- data/docs/videos.md +0 -16
- data/docs/walkthrough.md +0 -135
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da30bc7abc983e2ac54c1c4d1f454878d75dce0dd285a0e245d6ed54d73e4040
|
|
4
|
+
data.tar.gz: f286ce93d5ce2970d1c3a528d423bf8b8ed9239140f798f895a67e0aa1b8e2d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
-
|
|
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[
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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 # => #<
|
|
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 # => #<
|
|
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 # => #<
|
|
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
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
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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 [
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
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.
|
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
|