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
data/docs/walkthrough.md DELETED
@@ -1,135 +0,0 @@
1
- # Fast walkthrough
2
-
3
- !!! note "This is the main interactive tutorial we have on `fast`. If you're reading it on the web, please consider also try it in the command line: `fast .intro` in the terminal to get a rapid pace on reading and testing on your own computer."
4
-
5
- The objective here is give you some insights about how to use `ffast` gem in the
6
- command line.
7
-
8
- Let's start finding the main `fast.rb` file for the fast library:
9
-
10
- ```
11
- $ gem which fast
12
- ```
13
-
14
- And now, let's combine the previous expression that returns the path to the file
15
- and take a quick look into the methods `match?` in the file using a regular grep:
16
-
17
- ```
18
- $ grep "def match\?" $(gem which fast)
19
- ```
20
-
21
- Boring results, no? The code here is not easy to digest because we just see a
22
- fragment of the code block that we want.
23
- Let's make it a bit more advanced with `grep -rn` to file name and line number:
24
-
25
- ```
26
- $ grep -rn "def match\?" $(gem which fast)
27
- ```
28
-
29
- Still hard to understand the scope of the search.
30
-
31
- That's why fast exists! Now, let's take a look on how a method like this looks
32
- like from the AST perspective. Let's use `ruby-parse` for it:
33
-
34
- ```
35
- $ ruby-parse -e "def match?(node); end"
36
- ```
37
-
38
- Now, let's make the same search with `fast` node pattern:
39
-
40
- ```
41
- fast "(def match?)" $(gem which fast)
42
- ```
43
-
44
- Wow! in this case you got all the `match?` methods, but you'd like to go one level upper
45
- and understand the classes that implements the method with a single node as
46
- argument. Let's first use `^` to jump into the parent:
47
-
48
- ```
49
- fast "^(def match?)" $(gem which fast)
50
- ```
51
-
52
- As you can see it still prints some `match?` methods that are not the ones that
53
- we want, so, let's add a filter by the argument node `(args (arg node))`:
54
-
55
- ```
56
- fast "(def match? (args (arg node)))" $(gem which fast)
57
- ```
58
-
59
- Now, it looks closer to have some understanding of the scope, filtering only
60
- methods that have the name `match?` and receive `node` as a parameter.
61
-
62
- Now, let's do something different and find all methods that receives a `node` as
63
- an argument:
64
-
65
- ```
66
- fast "(def _ (args (arg node)))" $(gem which fast)
67
- ```
68
-
69
- Looks like almost all of them are the `match?` and we can also skip the `match?`
70
- methods negating the expression prefixing with `!`:
71
-
72
- ```
73
- fast "(def !match? (args (arg node)))" $(gem which fast)
74
- ```
75
-
76
- Let's move on and learn more about node pattern with the RuboCop project:
77
-
78
- ```
79
- $ VISUAL=echo gem open rubocop
80
- ```
81
-
82
- RuboCop contains `def_node_matcher` and `def_node_search`. Let's make a search
83
- for both method names wrapping the query with `{}` selector:
84
-
85
- ```
86
- fast "(send nil {def_node_matcher def_node_search})" $(VISUAL=echo gem open rubocop)
87
- ```
88
-
89
- As you can see, node pattern is widely adopted in the cops to target code.
90
- Rubocop contains a few projects with dedicated cops that can help you learn
91
- more.
92
-
93
- ## How to automate refactor using AST
94
-
95
- Moving towards to the code automation, the next step after finding some target code
96
- is refactor and change the code behavior.
97
-
98
- Let's imagine that we already found some code that we want to edit or remove. If
99
- we get the AST we can also cherry-pick any fragment of the expression to be
100
- replaced. As you can imagine, RuboCop also benefits from automatic refactoring
101
- offering the `--autocorrect` option.
102
-
103
- All the hardcore algorithms are in the [parser](https://github.com/whitequark/parser)
104
- rewriter, but we can find a lot of great examples on RuboCop project searching
105
- for the `autocorrect` method.
106
-
107
- ```
108
- fast "(def autocorrect)" $(VISUAL=echo gem open rubocop)
109
- ```
110
-
111
- Look that most part of the methods are just returning a lambda with a
112
- corrector. Now, let's use the `--ast` to get familiar with the tree details for the
113
- implementation:
114
-
115
- ```
116
- fast --ast "(def autocorrect)" $(VISUAL=echo gem open rubocop)/lib/rubocop/cop/style
117
- ```
118
-
119
- As we can see, we have a `(send (lvar corrector))` that is the interface that we
120
- can get the most interesting calls to overwrite files:
121
-
122
- ```
123
- fast "(send (lvar corrector)" $(VISUAL=echo gem open rubocop)
124
- ```
125
-
126
-
127
- ## That is all for now!
128
-
129
- I hope you enjoyed to learn by example searching with fast. If you like it,
130
- please [star the project](https://github.com/jonatas/fast/)!
131
-
132
- You can also build your own tutorials simply using markdown files like I did
133
- here, you can find this tutorial [here](https://github.com/jonatas/fast/tree/master/docs/walkthrough.md).
134
-
135
-
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fast'
4
- require 'open3'
5
- require 'ostruct'
6
- require 'fileutils'
7
-
8
- # Usage instructions:
9
- # 1. Add the following to your project's Gemfile: gem 'ffast' (yes, two "f"s)
10
- # 2. Copy this file to your Rails project root directory
11
- # 3. Run: bundle exec ruby build_stubbed_and_let_it_be_experiment.rb
12
-
13
- # List of spec files you want to experiment with. One per line.
14
- FILE_NAMES = %w[
15
- # spec/model/foo_spec.rb
16
- # spec/model/bar_spec.rb
17
- ]
18
-
19
- def execute_rspec(file_name)
20
- rspec_command = "bin/spring rspec --fail-fast --format progress #{file_name}"
21
- stdout_str, stderr_str, status = Open3.capture3(rspec_command)
22
- execution_time = /Finished in (.*?) seconds/.match(stdout_str)[1]
23
- print stderr_str.gsub(/Running via Spring preloader.*?$/, '').chomp unless status.success?
24
- OpenStruct.new(success: status.success?, execution_time: execution_time)
25
- end
26
-
27
- def delete_temp_files(original_file_name)
28
- file_path = File.dirname(original_file_name)
29
- file_name = File.basename(original_file_name)
30
- Dir.glob("#{file_path}/experiment*#{file_name}").each { |file| File.delete(file)}
31
- end
32
-
33
- FILE_NAMES.each do |original_file_name|
34
- Fast.experiment('RSpec/ReplaceCreateWithBuildStubbed') do
35
- lookup original_file_name
36
- search '(block (send nil let (sym _)) (args) $(send nil create))'
37
- edit { |_, (create)| replace(create.loc.selector, 'build_stubbed') }
38
- policy { |experiment_file_name| execute_rspec(experiment_file_name) }
39
- end.run
40
-
41
- Fast.experiment('RSpec/LetItBe') do
42
- lookup original_file_name
43
- search '(block $(send nil let! (sym _)) (args) (send nil create))'
44
- edit { |_, (let)| replace(let.loc.selector, 'let_it_be') }
45
- policy { |experiment_file_name| execute_rspec(experiment_file_name) }
46
- end.run
47
-
48
- delete_temp_files(original_file_name)
49
- end
50
-
51
-
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $LOAD_PATH << File.expand_path('../lib', __dir__)
4
- require 'fast'
5
-
6
- # It's a simple script that you can try to replace
7
- # `create` by `build_stubbed` and it moves the file if
8
- # successfully passed the specs
9
- #
10
- # $ ruby experimental_replacement.rb spec/*/*_spec.rb
11
- def experimental_spec(file, name)
12
- parts = file.split('/')
13
- dir = parts[0..-2]
14
- filename = "experiment_#{name}_#{parts[-1]}"
15
- File.join(*dir, filename)
16
- end
17
-
18
- def experiment(file, name, search, replacement)
19
- ast = Fast.ast_from_file(file)
20
-
21
- results = Fast.search(ast, search)
22
- unless results.empty?
23
- new_content = Fast.replace_file(file, search, replacement)
24
- new_spec = experimental_spec(file, name)
25
- return if File.exist?(new_spec)
26
- File.open(new_spec, 'w+') { |f| f.puts new_content }
27
- if system("bin/spring rspec --fail-fast #{new_spec}")
28
- system "mv #{new_spec} #{file}"
29
- puts "✅ #{file}"
30
- else
31
- system "rm #{new_spec}"
32
- puts "🔴 #{file}"
33
- end
34
- end
35
- rescue StandardError
36
- # Avoid stop because weird errors like encoding issues
37
- puts "🔴🔴 🔴 #{file}: #{$ERROR_INFO}"
38
- end
39
-
40
- ARGV.each do |file|
41
- [
42
- # Thread.new { experiment(file, 'build_stubbed', '(send nil create)', ->(node) { replace(node.location.selector, 'build_stubbed') }) },
43
- experiment(file, 'seed', '(send nil create)', ->(node) { replace(node.location.selector, 'seed') })
44
-
45
- ] # .each(&:join)
46
- end
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # List files that matches with some expression
5
- # Usage:
6
- #
7
- # ruby examples/find_usage.rb defs
8
- #
9
- # Or be explicit about directory or folder:
10
- #
11
- # ruby examples/find_usage.rb defs lib/
12
- $LOAD_PATH.unshift(File.expand_path('lib', __dir__))
13
-
14
- require 'fast'
15
- require 'coderay'
16
-
17
- arguments = ARGV
18
- pattern = arguments.shift
19
- files = Fast.ruby_files_from(arguments.any? ? arguments : '.')
20
- files.select do |file|
21
- begin
22
- puts file if Fast.search_file(pattern, file).any?
23
- rescue Parser::SyntaxError
24
- []
25
- end
26
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require '../../fast/lib/fast'
4
-
5
- # For specs using `let(:something) { create ... }` it tries to use `let_it_be` instead
6
- Fast.experiment('RSpec/LetItBe') do
7
- lookup 'spec/models'
8
- search '(block $(send nil let (sym _)) (args) (send nil create))'
9
- edit { |_, (let)| replace(let.loc.selector, 'let_it_be') }
10
- policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
11
- end.run
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $LOAD_PATH << File.expand_path('../lib', __dir__)
4
- require 'fast'
5
-
6
- def node_size(node)
7
- return 1 unless node.respond_to?(:children)
8
- children = node.children
9
- return 1 if children.empty? || children.length == 1
10
- nodes, syms = children.partition { |e| e.respond_to?(:children) }
11
- 1 + syms.length + (nodes.map(&method(:node_size)).inject(:+) || 0)
12
- end
13
-
14
- def method_complexity(file)
15
- ast = Fast.ast_from_file(file)
16
- Fast.search(ast, '(class ...)').map do |node_class|
17
- manager_name = node_class.children.first.children.last
18
-
19
- defs = Fast.search(node_class, '(def !{initialize} ... ... )')
20
-
21
- defs.map do |node|
22
- complexity = node_size(node)
23
- method_name = node.children.first
24
- { "#{manager_name}##{method_name}" => complexity }
25
- end.inject(:merge) || {}
26
- end
27
- end
28
-
29
- files = ARGV || Dir['**/*.rb']
30
-
31
- complexities = files.map(&method(:method_complexity)).flatten.inject(:merge!)
32
-
33
- puts '| Method | Complexity |'
34
- puts '| ------ | ---------- |'
35
- complexities.sort_by { |_, v| -v }.map do |method, complexity|
36
- puts "| #{method} | #{complexity} |"
37
- end
@@ -1,15 +0,0 @@
1
- require 'fast'
2
-
3
- # Search for duplicated methods interpolating the method and collecting previous
4
- # method names. Returns true if the name already exists in the same class level.
5
- # Note that this example will work only in a single file because it does not
6
- # cover any detail on class level.
7
- def duplicated(method_name)
8
- @methods ||= []
9
- already_exists = @methods.include?(method_name)
10
- @methods << method_name
11
- already_exists
12
- end
13
-
14
- puts Fast.search_file( '(def #duplicated)', 'example.rb')
15
-
@@ -1,58 +0,0 @@
1
- require 'bundler/setup'
2
- require 'fast'
3
- require 'coderay'
4
- require 'pp'
5
- require 'set'
6
-
7
- arguments = ARGV
8
- pattern = arguments.shift || '{ block case send def defs while class if }'
9
-
10
- files = Fast.ruby_files_from(*%w(spec lib app gems)) +
11
- Dir[File.join(Gem.path.first,'**/*.rb')]
12
-
13
- total = files.count
14
- pattern = Fast.expression(pattern)
15
-
16
- similarities = {}
17
-
18
- def similarities.show pattern
19
- files = self[pattern]
20
- files.each do |file|
21
- nodes = Fast.search_file(pattern, file)
22
- nodes.each do |result|
23
- Fast.report(result, file: file)
24
- end
25
- end
26
- end
27
-
28
- def similarities.top
29
- self.transform_values(&:size)
30
- .sort_by{|search,results|search.size / results.size}
31
- .reverse.select{|k,v|v > 10}[0,10]
32
- end
33
-
34
- begin
35
- files.each_with_index do |file, i|
36
- progress = ((i / total.to_f) * 100.0).round(2)
37
- print "\r (#{i}/#{total}) #{progress}% Researching on #{file}"
38
- begin
39
- results = Fast.search_file(pattern, file) || []
40
- rescue
41
- next
42
- end
43
- results.each do |n|
44
- search = Fast.expression_from(n)
45
- similarities[search] ||= Set.new
46
- similarities[search] << file
47
- end
48
- end
49
- rescue Interrupt
50
- # require 'pry'; binding.pry
51
- end
52
-
53
- puts "mapped #{similarities.size} cases"
54
- similarities.delete_if {|k,v| k.size < 30 || v.size < 5}
55
- puts "Removing the small ones we have #{similarities.size} similarities"
56
-
57
- similarities.show similarities.top[0][0]
58
-
@@ -1,6 +0,0 @@
1
-
2
- rewriter = Fast::Rewriter.new
3
- rewriter.ast = Fast.ast("a = 1")
4
- rewriter.search ='(lvasgn _ ...)'
5
- rewriter.replacement = -> (node) { replace(node.location.name, 'variable_renamed') }
6
- puts rewriter.rewrite!
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # For specs using `let!(:something) { create ... }` it tries to use `let_it_be` instead
4
- Fast.experiment('RSpec/LetItBe') do
5
- lookup 'spec'
6
- search '(block $(send nil let! (sym _)) (args) (send nil create))'
7
- edit { |_, (let)| replace(let.loc.selector, 'let_it_be') }
8
- policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Experimentally remove a before or an after block
4
- Fast.experiment('RSpec/RemoveUselessBeforeAfterHook') do
5
- lookup 'spec'
6
- search '(block (send nil {before after}))'
7
- edit { |node| remove(node.loc.expression) }
8
- policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
9
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # For specs using `let(:something) { create ... }` it tries to use
4
- # `build_stubbed` instead
5
- Fast.experiment('RSpec/ReplaceCreateWithBuildStubbed') do
6
- lookup 'spec'
7
- search '(block (send nil let (sym _)) (args) $(send nil create))'
8
- edit { |_, (create)| replace(create.loc.selector, 'build_stubbed') }
9
- policy { |new_file| system("bin/spring rspec --format progress --fail-fast #{new_file}") }
10
- end