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/docs/shortcuts.md DELETED
@@ -1,323 +0,0 @@
1
- # Shortcuts
2
-
3
- Shortcuts are defined on a `Fastfile` inside any ruby project.
4
-
5
- !!!info "Use `~/Fastfile`"
6
- You can also add one extra in your `$HOME` if you want to have something loaded always.
7
-
8
- By default, the command line interface does not load any `Fastfile` if the
9
- first param is not a shortcut. It should start with `.`.
10
-
11
- I'm building several researches and I'll make the examples open here to show
12
- several interesting cases in action.
13
-
14
- ## List your fast shortcuts
15
-
16
- As the interface is very rudimentar, let's build a shortcut to print what
17
- shortcuts are available. This is a good one to your `$HOME/Fastfile`:
18
-
19
- ```ruby
20
- # List all shortcut with comments
21
- Fast.shortcut :shortcuts do
22
- fast_files.each do |file|
23
- lines = File.readlines(file).map{|line|line.chomp.gsub(/\s*#/,'').strip}
24
- result = capture_file('(send ... shortcut $(sym _', file)
25
- result = [result] unless result.is_a?Array
26
- result.each do |capture|
27
- target = capture.loc.expression
28
- puts "fast .#{target.source[1..-1].ljust(30)} # #{lines[target.line-2]}"
29
- end
30
- end
31
- end
32
- ```
33
-
34
- And using it on `fast` project that loads both `~/Fastfile` and the Fastfile from the project:
35
-
36
- ```
37
- fast .version # Let's say you'd like to show the version that is over the version file
38
- fast .parser # Simple shortcut that I used often to show how the expression parser works
39
- fast .bump_version # Use `fast .bump_version` to rewrite the version file
40
- fast .shortcuts # List all shortcut with comments
41
- ```
42
-
43
- ## Search for references
44
-
45
- I always miss bringing something simple as `grep keyword` where I can leave a simple string and it can
46
- search in all types of nodes and report interesting things about it.
47
-
48
- Let's consider a very flexible search that can target any code related to some
49
- keyword. Considering that we're talking about code indentifiers:
50
-
51
-
52
- ```ruby
53
- # Search all references about some keyword or regular expression
54
- Fast.shortcut(:ref) do
55
- require 'fast/cli'
56
- Kernel.class_eval do
57
- def matches_args? identifier
58
- search = ARGV.last
59
- regex = Regexp.new(search, Regexp::IGNORECASE)
60
- case identifier
61
- when Symbol, String
62
- regex.match?(identifier) || identifier.to_s.include?(search)
63
- when Astrolabe::Node
64
- regex.match?(identifier.to_sexp)
65
- end
66
- end
67
- end
68
- pattern = <<~FAST
69
- {
70
- ({class def sym str} #matches_args?)'
71
- ({const send} nil #matches_args?)'
72
- }
73
- FAST
74
- Fast::Cli.run!([pattern, '.', '--parallel'])
75
- end
76
- ```
77
-
78
- ## Rails: Show validations from models
79
-
80
- If the shortcut does not define a block, it works as a holder for arguments from
81
- the command line.
82
-
83
- Let's say you always use `fast "(send nil {validate validates})" app/models` to
84
- check validations in the models. You can define a shortcut to hold the args and
85
- avoid retyping long lines:
86
-
87
- ```ruby
88
- # Show validations from app/models
89
- Fast.shortcut(:validations, "(send nil {validate validates})", "app/models")
90
- ```
91
- And you can reuse the search with the shortcut starting with a `.`:
92
-
93
- ```
94
- fast .validations
95
- ```
96
- And it will also accept params if you want to filter a specific file:
97
-
98
- ```
99
- fast .validations app/models/user.rb
100
- ```
101
-
102
- !!! info "Note that you can also use flags in the command line shortcuts"
103
-
104
- Let's say you also want to use `fast --headless` you can add it to the params:
105
-
106
- > Fast.shortcut(:validations, "(send nil {validate validates})", "app/models", "--headless")
107
-
108
- ## Automated Refactor: Bump version
109
-
110
- Let's start with a [real usage](https://github.com/jonatas/fast/blob/master/Fastfile#L20-L34)
111
- to bump a new version of the gem.
112
-
113
- ```ruby
114
- Fast.shortcut :bump_version do
115
- rewrite_file('(casgn nil VERSION (str _)', 'lib/fast/version.rb') do |node|
116
- target = node.children.last.loc.expression
117
- pieces = target.source.split('.').map(&:to_i)
118
- pieces.reverse.each_with_index do |fragment, i|
119
- if fragment < 9
120
- pieces[-(i + 1)] = fragment + 1
121
- break
122
- else
123
- pieces[-(i + 1)] = 0
124
- end
125
- end
126
- replace(target, "'#{pieces.join('.')}'")
127
- end
128
- end
129
- ```
130
-
131
- And then the change is done in the `lib/fast/version.rb`:
132
-
133
- ```diff
134
- module Fast
135
- - VERSION = '0.1.6'
136
- + VERSION = '0.1.7'
137
- end
138
- ```
139
-
140
- ## RSpec: Find unused shared contexts
141
-
142
- If you build shared contexts often, probably you can forget some left overs.
143
-
144
- The objective of the shortcut is find leftovers from shared contexts.
145
-
146
- First, the objective is capture all names of the `RSpec.shared_context` or
147
- `shared_context` declared in the `spec/support` folder.
148
-
149
- ```ruby
150
- Fast.capture_all('(block (send {nil,_} shared_context (str $_)))', Fast.ruby_files_from('spec/support'))
151
- ```
152
-
153
- Then, we need to check all the specs and search for `include_context` usages to
154
- confirm if all defined contexts are being used:
155
-
156
- ```ruby
157
- specs = Fast.ruby_files_from('spec').select{|f|f !~ %r{spec/support/}}
158
- Fast.search_all("(send nil include_context (str #register_usage)", specs)
159
- ```
160
-
161
- Note that we created a new reference to `#register_usage` and we need to define the method too:
162
-
163
-
164
- ```ruby
165
- @used = []
166
- def register_usage context_name
167
- @used << context_name
168
- end
169
- ```
170
-
171
- Wrapping up everything in a shortcut:
172
-
173
- ```ruby
174
- # Show unused shared contexts
175
- Fast.shortcut(:unused_shared_contexts) do
176
- puts "Checking shared contexts"
177
- Kernel.class_eval do
178
- @used = []
179
- def register_usage context_name
180
- @used << context_name
181
- end
182
- def show_report! defined_contexts
183
- unused = defined_contexts.values.flatten - @used
184
- if unused.any?
185
- puts "Unused shared contexts", unused
186
- else
187
- puts "Good job! all the #{defined_contexts.size} contexts are used!"
188
- end
189
- end
190
- end
191
- specs = ruby_files_from('spec/').select{|f|f !~ %r{spec/support/}}
192
- search_all("(send nil include_context (str #register_usage)", specs)
193
- defined_contexts = capture_all('(block (send {nil,_} shared_context (str $_)))', ruby_files_from('spec'))
194
- Kernel.public_send(:show_report!, defined_contexts)
195
- end
196
- ```
197
-
198
- !!! faq "Why `#register_usage` is defined on the `Kernel`?"
199
- Yes! note that the `#register_usage` was forced to be inside `Kernel`
200
- because of the `shortcut` block that takes the `Fast` context to be easy
201
- to access in the default functions. As I can define multiple shortcuts
202
- I don't want to polute my Kernel module with other methods that are not useful.
203
-
204
-
205
- ## RSpec: Remove unused let
206
-
207
- !!! hint "First shortcut with experiments"
208
- If you're not familiar with automated experiments, you can read about it [here](/experiments).
209
-
210
- The current scenario is similar in terms of search with the previous one, but more advanced
211
- because we're going to introduce automated refactoring.
212
-
213
- The idea is simple, if it finds a `let` in a RSpec scenario that is not referenced, it tries to experimentally remove the `let` and run the tests:
214
-
215
- ```ruby
216
- # Experimental remove `let` that are not referenced in the spec
217
- Fast.shortcut(:exp_remove_let) do
218
- require 'fast/experiment'
219
- Kernel.class_eval do
220
- file = ARGV.last
221
-
222
- defined_lets = Fast.capture_file('(block (send nil let (sym $_)))', file).uniq
223
- @unreferenced= defined_lets.select do |identifier|
224
- Fast.search_file("(send nil #{identifier})", file).empty?
225
- end
226
-
227
- def unreferenced_let?(identifier)
228
- @unreferenced.include? identifier
229
- end
230
- end
231
-
232
- experiment('RSpec/RemoveUnreferencedLet') do
233
- lookup ARGV.last
234
- search '(block (send nil let (sym #unreferenced_let?)))'
235
- edit { |node| remove(node.loc.expression) }
236
- policy { |new_file| system("bundle exec rspec --fail-fast #{new_file}") }
237
- end.run
238
- end
239
- ```
240
-
241
- And it will run with a single file from command line:
242
-
243
- ```
244
- fast .exp_remove_let spec/my_file_spec.rb
245
- ```
246
-
247
- ## FactoryBot: Replace `create` with `build_stubbed`
248
-
249
- For performance reasons, if we can avoid touching the database the test will
250
- always be faster.
251
-
252
- ```ruby
253
- # Experimental switch from `create` to `build_stubbed`
254
- Fast.shortcut(:exp_build_stubbed) do
255
- require 'fast/experiment'
256
- Fast.experiment('FactoryBot/UseBuildStubbed') do
257
- lookup ARGV.last
258
- search '(send nil create)'
259
- edit { |node| replace(node.loc.selector, 'build_stubbed') }
260
- policy { |new_file| system("bundle exec rspec --fail-fast #{new_file}") }
261
- end.run
262
- end
263
- ```
264
- ## RSpec: Use `let_it_be` instead of `let`
265
-
266
- The `let_it_be` is a simple helper from
267
- [TestProf](https://test-prof.evilmartians.io/#/let_it_be) gem that can speed up
268
- the specs by caching some factories using like a `before_all` approach.
269
-
270
- This experiment hunts for `let(...) { create(...) }` and switch the `let` to
271
- `let_it_be`:
272
-
273
- ```ruby
274
- # Experimental replace `let(_)` with `let_it_be` case it calls `create` inside the block
275
- Fast.shortcut(:exp_let_it_be) do
276
- require 'fast/experiment'
277
- Fast.experiment('FactoryBot/LetItBe') do
278
- lookup ARGV.last
279
- search '(block (send nil let (sym _)) (args) (send nil create))'
280
- edit { |node| replace(node.children.first.loc.selector, 'let_it_be') }
281
- policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
282
- end.run
283
- end
284
- ```
285
-
286
- ## RSpec: Remove `before` or `after` blocks
287
-
288
- From time to time, we forget some left overs like `before` or `after` blocks
289
- that even removing from the code, the tests still passes. This experiment
290
- removes the before/after blocks and check if the test passes.
291
-
292
- ```ruby
293
- # Experimental remove `before` or `after` blocks.
294
- Fast.shortcut(:exp_remove_before_after) do
295
- require 'fast/experiment'
296
- Fast.experiment('RSpec/RemoveBeforeAfter') do
297
- lookup ARGV.last
298
- search '(block (send nil {before after}))'
299
- edit { |node| remove(node.loc.expression) }
300
- policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
301
- end.run
302
- end
303
- ```
304
-
305
- ## RSpec: Show message chains
306
-
307
- I often forget the syntax and need to search for message chains on specs, so I
308
- created an shortcut for it.
309
-
310
- ```ruby
311
- # Show RSpec message chains
312
- Fast.shortcut(:message_chains, '^^(send nil receive_message_chain)', 'spec')
313
- ```
314
-
315
- ## RSpec: Show nested assertions
316
-
317
- I love to use nested assertions and I often need examples to refer to them:
318
-
319
- ```ruby
320
- # Show RSpec nested assertions with .and
321
- Fast.shortcut(:nested_assertions, '^^(send ... and)', 'spec')
322
- ```
323
-
@@ -1,176 +0,0 @@
1
- # Research for code similarity
2
-
3
- This is a small tutorial to explore code similarity.
4
-
5
- Check the code example [here](https://github.com/jonatas/fast/blob/master/examples/similarity_research.rb).
6
-
7
- The major idea is register all expression styles and see if we can find some
8
- similarity between the structures.
9
-
10
- First we need to create a function that can analyze AST nodes and extract a
11
- pattern from the expression.
12
-
13
- The expression needs to generalize final node values and recursively build a
14
- pattern that can be used as a search expression.
15
-
16
- ```ruby
17
- def expression_from(node)
18
- case node
19
- when Parser::AST::Node
20
- if node.children.any?
21
- children_expression = node.children
22
- .map(&method(:expression_from))
23
- .join(' ')
24
- "(#{node.type} #{children_expression})"
25
- else
26
- "(#{node.type})"
27
- end
28
- when nil, 'nil'
29
- 'nil'
30
- when Symbol, String, Integer
31
- '_'
32
- when Array, Hash
33
- '...'
34
- else
35
- node
36
- end
37
- end
38
- ```
39
-
40
- The pattern generated only flexibilize the search allowing us to group similar nodes.
41
-
42
- Example:
43
-
44
- ```ruby
45
- expression_from(code['1']) # =>'(int _)'
46
- expression_from(code['nil']) # =>'(nil)'
47
- expression_from(code['a = 1']) # =>'(lvasgn _ (int _))'
48
- expression_from(code['def name; person.name end']) # =>'(def _ (args) (send (send nil _) _))'
49
- ```
50
-
51
- The current method can translate all kind of expressions and the next step is
52
- observe some specific node types and try to group the similarities
53
- using the pattern generated.
54
-
55
- ```ruby
56
- Fast.search_file('class', 'lib/fast.rb')
57
- ```
58
- Capturing the constant name and filtering only for symbols is easy and we can
59
- see that we have a few classes defined in the the same file.
60
-
61
- ```ruby
62
- Fast.search_file('(class (const nil $_))','lib/fast.rb').grep(Symbol)
63
- => [:Rewriter,
64
- :ExpressionParser,
65
- :Find,
66
- :FindString,
67
- :FindWithCapture,
68
- :Capture,
69
- :Parent,
70
- :Any,
71
- :All,
72
- :Not,
73
- :Maybe,
74
- :Matcher,
75
- :Experiment,
76
- :ExperimentFile]
77
- ```
78
-
79
- The idea of this inspecton is build a proof of concept to show the similarity
80
- of matcher classes because they only define a `match?` method.
81
-
82
- ```ruby
83
- patterns = Fast.search_file('class','lib/fast.rb').map{|n|Fast.expression_from(n)}
84
- ```
85
-
86
- A simple comparison between the patterns size versus `.uniq.size` can proof if
87
- the idea will work.
88
-
89
- ```ruby
90
- patterns.size == patterns.uniq.size
91
- ```
92
-
93
- It does not work for the matcher cases but we can go deeper and analyze all
94
- files required by bundler.
95
-
96
- ```ruby
97
- similarities = {}
98
- Gem.find_files('*.rb').each do |file|
99
- Fast.search_file('{block send if while case def defs class module}', file).map do |n|
100
- key = Fast.expression_from(n)
101
- similarities[key] ||= Set.new
102
- similarities[key] << file
103
- end
104
- end
105
- similarities.delete_if {|k,v|v.size < 2}
106
- ```
107
- The similarities found are the following:
108
-
109
- ```ruby
110
- {"(class (const nil _) (const nil _) nil)"=>
111
- #<Set: {"/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb",
112
- "/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/method_source-0.9.0/lib/method_source.rb",
113
- "/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/rdoc.rb",
114
- "/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb",
115
- "/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/tsort.rb"}>,
116
- "(class (const nil _) nil nil)"=>#<Set: {"/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/ripper.rb", "/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/cgi.rb"}>}
117
- ```
118
-
119
- And now we can test the expression using the command line tool through the files
120
- and observe the similarity:
121
-
122
- ```
123
- ⋊> ~ fast "(class (const nil _) (const nil _) nil)" /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/method_source-0.9.0/lib/method_source.rb /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/rdoc.rb /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/tsort.rb
124
- ```
125
-
126
- Output:
127
-
128
- ```ruby
129
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:8
130
- class DeadWorker < StandardError
131
- end
132
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:11
133
- class Break < StandardError
134
- end
135
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:14
136
- class Kill < StandardError
137
- end
138
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/method_source-0.9.0/lib/method_source.rb:16
139
- class SourceNotFoundError < StandardError; end
140
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/rdoc.rb:63
141
- class Error < RuntimeError; end
142
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/irb.rb:338
143
- class Abort < Exception;end
144
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/tsort.rb:125
145
- class Cyclic < StandardError
146
- end
147
- ```
148
-
149
- It works and now we can create a method to do what the command line tool did,
150
- grouping the patterns and inspecting the occurrences.
151
-
152
- ```ruby
153
- def similarities.show pattern
154
- files = self[pattern]
155
- files.each do |file|
156
- nodes = Fast.search_file(pattern, file)
157
- nodes.each do |result|
158
- Fast.report(result, file: file)
159
- end
160
- end
161
- end
162
- ```
163
-
164
- And calling the method exploring some "if" similarities, it prints the following
165
- results:
166
-
167
- ```ruby
168
- similarities.show "(if (send (const nil _) _ (lvar _)) nil (return (false)))"
169
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/resolv.rb:1248
170
- return false unless Name === other
171
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/fileutils.rb:138
172
- return false unless File.exist?(new)
173
- # /Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/2.5.0/matrix.rb:1862
174
- return false unless Vector === other
175
- ```
176
-