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/index.md DELETED
@@ -1,404 +0,0 @@
1
- # Fast
2
-
3
- <center>![](assets/logo-small.png)</center>
4
-
5
- Fast is a "Find AST" tool to help you search in the code abstract syntax tree.
6
-
7
- Ruby allow us to do the same thing in a few ways then it's hard to check
8
- how the code is written.
9
-
10
- Using the AST will be easier than try to cover the multiple ways we can write
11
- the same code.
12
-
13
- You can define a string like `%||` or `''` or `""` but they will have the same
14
- AST representation.
15
-
16
- ## AST representation
17
-
18
- Each detail of the ruby syntax have a equivalent identifier and some
19
- content. The content can be another expression or a final value.
20
-
21
- Fast uses parser gem behind the scenes to parse the code into nodes.
22
-
23
- First get familiar with parser gem and understand how ruby code is represented.
24
-
25
- When you install parser gem, you will have access to `ruby-parse` and you can
26
- use it with `-e` to parse an expression directly from the command line.
27
-
28
- Example:
29
-
30
- ```
31
- ruby-parse -e 1
32
- ```
33
-
34
- It will print the following output:
35
-
36
- ```
37
- (int 1)
38
- ```
39
-
40
- And trying a number with decimals:
41
-
42
- ```
43
- ruby-parse -e 1.1
44
- (float 1)
45
- ```
46
-
47
- Building a regex that will match decimals and integer looks like something easy
48
- and with fast you use a node pattern that reminds the syntax of regular
49
- expressions.
50
-
51
- ## Syntax for find in AST
52
-
53
- The current version cover the following elements:
54
-
55
- - `()` to represent a **node** search
56
- - `{}` is for **any** matches like **union** conditions with **or** operator
57
- - `[]` is for **all** matches like **intersect** conditions with **and** operator
58
- - `$` is for **capture** current expression
59
- - `_` is **something** not nil
60
- - `nil` matches exactly **nil**
61
- - `...` is a **node** with children
62
- - `^` is to get the **parent node** of an expression
63
- - `?` is for **maybe**
64
- - `\1` to use the first **previous captured** element
65
- - `""` surround the value with double quotes to match literal strings
66
-
67
- Jump to [Syntax](syntax.md).
68
-
69
- ## ast
70
-
71
- Use `Fast.ast` to convert simple code to AST objects. You can use it as
72
- `ruby-parse` but directly from the console.
73
-
74
- ```ruby
75
- Fast.ast("1") # => s(:int, 1)
76
- Fast.ast("method") # => s(:send, nil, :method)
77
- Fast.ast("a.b") # => s(:send, s(:send, nil, :a), :b)
78
- Fast.ast("1 + 1") # => s(:send, s(:int, 1), :+, s(:int, 1))
79
- Fast.ast("a = 2") # => s(:lvasgn, :a, s(:int, 2))
80
- Fast.ast("b += 2") # => s(:op_asgn, s(:lvasgn, :b), :+, s(:int, 2))
81
- ```
82
-
83
- It uses [astrolable](https://github.com/yujinakayama/astrolabe) gem behind the scenes:
84
-
85
- ```ruby
86
- Fast.ast(Fast.ast("1")).class
87
- => Astrolabe::Node
88
- Fast.ast(Fast.ast("1")).type
89
- => :int
90
- Fast.ast(Fast.ast("1")).children
91
- => [1]
92
- ```
93
-
94
- See also [ast_from_file](#ast_from_file).
95
-
96
- ## match?
97
-
98
- `Fast.match?` is the most granular function that tries to compare a node with an
99
- expression. It returns true or false and some node captures case it find
100
- something.
101
-
102
- Let's start with a simple integer in Ruby:
103
-
104
- ```ruby
105
- 1
106
- ```
107
-
108
- The AST can be represented with the following expression:
109
-
110
- ```
111
- (int 1)
112
- ```
113
-
114
- The ast representation holds node `type` and `children`.
115
-
116
- Let's build a method `s` to represent `Parser::AST::Node` with a `#type` and `#children`.
117
-
118
- ```ruby
119
- def s(type, *children)
120
- Parser::AST::Node.new(type, children)
121
- end
122
- ```
123
-
124
- A local variable assignment:
125
-
126
- ```ruby
127
- value = 42
128
- ```
129
-
130
- Can be represented with:
131
-
132
- ```ruby
133
- ast = s(:lvasgn, :value, s(:int, 42))
134
- ```
135
-
136
- Now, lets find local variable named `value` with an value `42`:
137
-
138
- ```ruby
139
- Fast.match?('(lvasgn value (int 42))', ast) # true
140
- ```
141
-
142
- Lets abstract a bit and allow some integer value using `_` as a shortcut:
143
-
144
- ```ruby
145
- Fast.match?('(lvasgn value (int _))', ast) # true
146
- ```
147
-
148
- Lets abstract more and allow float or integer:
149
-
150
- ```ruby
151
- Fast.match?('(lvasgn value ({float int} _))', ast) # true
152
- ```
153
-
154
- Or combine multiple assertions using `[]` to join conditions:
155
-
156
- ```ruby
157
- Fast.match?('(lvasgn value ([!str !hash !array] _))', ast) # true
158
- ```
159
-
160
- Matches all local variables not string **and** not hash **and** not array.
161
-
162
- We can match "a node with children" using `...`:
163
-
164
- ```ruby
165
- Fast.match?('(lvasgn value ...)', ast) # true
166
- ```
167
-
168
- You can use `$` to capture a node:
169
-
170
- ```ruby
171
- Fast.match?('(lvasgn value $...)', ast) # => [s(:int), 42]
172
- ```
173
-
174
- Or match whatever local variable assignment combining both `_` and `...`:
175
-
176
- ```ruby
177
- Fast.match?('(lvasgn _ ...)', ast) # true
178
- ```
179
-
180
- You can also use captures in any levels you want:
181
-
182
- ```ruby
183
- Fast.match?('(lvasgn $_ $...)', ast) # [:value, s(:int), 42]
184
- ```
185
-
186
- Keep in mind that `_` means something not nil and `...` means a node with
187
- children.
188
-
189
- Then, if do you get a method declared:
190
-
191
- ```ruby
192
- def my_method
193
- call_other_method
194
- end
195
- ```
196
- It will be represented with the following structure:
197
-
198
- ```ruby
199
- ast =
200
- s(:def, :my_method,
201
- s(:args),
202
- s(:send, nil, :call_other_method))
203
- ```
204
-
205
- Keep an eye on the node `(args)`.
206
-
207
- Then you know you can't use `...` but you can match with `(_)` to match with
208
- such case.
209
-
210
- Let's test a few other examples. You can go deeply with the arrays. Let's suppose we have a hardcore call to
211
- `a.b.c.d` and the following AST represents it:
212
-
213
- ```ruby
214
- ast =
215
- s(:send,
216
- s(:send,
217
- s(:send,
218
- s(:send, nil, :a),
219
- :b),
220
- :c),
221
- :d)
222
- ```
223
-
224
- You can search using sub-arrays with **pure values**, or **shortcuts** or
225
- **procs**:
226
-
227
- ```ruby
228
- Fast.match?([:send, [:send, '...'], :d], ast) # => true
229
- Fast.match?([:send, [:send, '...'], :c], ast) # => false
230
- Fast.match?([:send, [:send, [:send, '...'], :c], :d], ast) # => true
231
- ```
232
-
233
- Shortcuts like `...` and `_` are just literals for procs. Then you can use
234
- procs directly too:
235
-
236
- ```ruby
237
- Fast.match?([:send, [ -> (node) { node.type == :send }, [:send, '...'], :c], :d], ast) # => true
238
- ```
239
-
240
- And also work with expressions:
241
-
242
- ```ruby
243
- Fast.match?('(send (send (send (send nil $_) $_) $_) $_)', ast) # => [:a, :b, :c, :d]
244
- ```
245
-
246
- If something does not work you can debug with a block:
247
-
248
- ```ruby
249
- Fast.debug { Fast.match?([:int, 1], s(:int, 1)) }
250
- ```
251
-
252
- It will output each comparison to stdout:
253
-
254
- ```
255
- int == (int 1) # => true
256
- 1 == 1 # => true
257
- ```
258
-
259
- ## search
260
-
261
- Search allows you to go deeply in the AST, collecting nodes that matches with
262
- the expression. It also returns captures if they exist.
263
-
264
- ```ruby
265
- Fast.search('(int _)', Fast.ast('a = 1')) # => s(:int, 1)
266
- ```
267
-
268
- If you use captures, it returns the node and the captures respectively:
269
-
270
- ```ruby
271
- Fast.search('(int $_)', Fast.ast('a = 1')) # => [s(:int, 1), 1]
272
- ```
273
-
274
- You can also bind external parameters in the search using extra arguments:
275
- ```ruby
276
- Fast.search('(int %1)', Fast.ast('a = 1'), 1) # => [s(:int, 1)]
277
- ```
278
-
279
- ## capture
280
-
281
- To pick just the captures and ignore the nodes, use `Fast.capture`:
282
-
283
- ```ruby
284
- Fast.capture('(int $_)', Fast.ast('a = 1')) # => 1
285
- ```
286
- ## replace
287
-
288
- And if I want to refactor a code and use `delegate <attribute>, to: <object>`, try with replace:
289
-
290
- ```ruby
291
- Fast.replace '(def $_ ... (send (send nil $_) \1))', ast do |node, captures|
292
- attribute, object = captures
293
- replace(node.location.expression, "delegate :#{attribute}, to: :#{object}")
294
- end
295
- ```
296
-
297
- ## replace_file
298
-
299
- Now let's imagine we have real files like `sample.rb` with the following code:
300
-
301
- ```ruby
302
- def good_bye
303
- message = ["good", "bye"]
304
- puts message.join(' ')
305
- end
306
- ```
307
-
308
- And we decide to remove the `message` variable and put it inline with the `puts`.
309
-
310
- Basically, we need to find the local variable assignment, store the value in
311
- memory. Remove the assignment expression and use the value where the variable
312
- is being called.
313
-
314
- ```ruby
315
- assignment = nil
316
- Fast.replace_file('({ lvasgn lvar } message )','sample.rb') do |node, _|
317
- if node.type == :lvasgn
318
- assignment = node.children.last
319
- remove(node.location.expression)
320
- elsif node.type == :lvar
321
- replace(node.location.expression, assignment.location.expression.source)
322
- end
323
- end
324
- ```
325
-
326
- It will return an output of the new source code with the changes but not save
327
- the file. You can use ()[#rewrite_file] if you're confident about the changes.
328
-
329
- ## capture_file
330
-
331
- `Fast.capture_file` can be used to combine [capture](#capture) and file system.
332
-
333
- ```ruby
334
- Fast.capture_file("$(casgn)", "lib/fast/version.rb") # => s(:casgn, nil, :VERSION, s(:str, "0.1.3"))
335
- Fast.capture_file("(casgn nil _ (str $_))", "lib/fast/version.rb") # => "0.1.3"
336
- ```
337
-
338
- ## capture_all
339
-
340
- `Fast.capture_all` can be used to combine [capture_file](#capture_file) from multiple sources:
341
-
342
- ```ruby
343
- Fast.capture_all("(casgn nil $_)") # => { "./lib/fast/version.rb"=>:VERSION, "./lib/fast.rb"=>[:LITERAL, :TOKENIZER], ...}
344
- ```
345
-
346
- The second parameter can also be passed with to filter specific folders:
347
-
348
- ```ruby
349
- Fast.capture_all("(casgn nil $_)", "lib/fast") # => {"lib/fast/shortcut.rb"=>:LOOKUP_FAST_FILES_DIRECTORIES, "lib/fast/version.rb"=>:VERSION}
350
- ```
351
-
352
-
353
- ## rewrite_file
354
-
355
- `Fast.rewrite_file` works exactly as the `replace` but it will override the file
356
- from the input.
357
-
358
- ## ast_from_file
359
-
360
- This method parses the code and load into a AST representation.
361
-
362
- ```ruby
363
- Fast.ast_from_file('sample.rb')
364
- ```
365
-
366
- ## search_file
367
-
368
- You can use `search_file` and pass the path for search for expressions inside
369
- files.
370
-
371
- ```ruby
372
- Fast.search_file(expression, 'file.rb')
373
- ```
374
-
375
- It's simple combination of `Fast.ast_from_file` with `Fast.search`.
376
-
377
- ## ruby_files_from
378
-
379
- You'll be probably looking for multiple ruby files, then this method fetches
380
- all internal `.rb` files
381
-
382
- ```ruby
383
- Fast.ruby_files_from(['lib']) # => ["lib/fast.rb"]
384
- ```
385
-
386
- ## search_all
387
-
388
- Combines the [search_file](#search_file) with [ruby_files_from](#ruby_files_from)
389
- multiple locations and returns tuples with files and results.
390
-
391
- ```ruby
392
- Fast.search_all("(def ast_from_file)")
393
- => {"./lib/fast.rb"=>[s(:def, :ast_from_file,
394
- s(:args,
395
- s(:arg, :file)),
396
- s(:begin,
397
- ```
398
-
399
- You can also override the second param and pass target files or folders:
400
-
401
- ```ruby
402
- Fast.search_all("(def _)", '../other-folder')
403
- ```
404
-
@@ -1,27 +0,0 @@
1
-
2
- You can create a custom command in pry to reuse fast in any session.
3
-
4
- Start simply dropping it on your `.pryrc`:
5
-
6
- ```ruby
7
- Pry::Commands.block_command "fast", "Fast search" do |expression, file|
8
- require "fast"
9
- files = Fast.ruby_files_from(file || '.')
10
- files.each do |f|
11
- results = Fast.search_file(expression, f)
12
- next if results.nil? || results.empty?
13
- output.puts Fast.highlight("# #{f}")
14
-
15
- results.each do |result|
16
- output.puts Fast.highlight(result)
17
- end
18
- end
19
- end
20
- ```
21
-
22
- And use it in the console:
23
-
24
- ```pry
25
- fast '(def match?)' lib/fast.rb
26
- ```
27
-
data/docs/research.md DELETED
@@ -1,93 +0,0 @@
1
-
2
- # Research
3
-
4
- I love to research about codebase as data and prototyping ideas several times
5
- doesn't fit in simple [shortcuts](/shortcuts).
6
-
7
- Here is my first research that worth sharing:
8
-
9
- ## Combining Runtime metadata with AST complex searches
10
-
11
- This example covers how to find RSpec `allow` combined with `and_return` missing
12
- the `with` clause specifying the nested parameters.
13
-
14
- Here is the [gist](https://gist.github.com/jonatas/c1e580dcb74e20d4f2df4632ceb084ef)
15
- if you want to go straight and run it.
16
-
17
- Scenario for simple example:
18
-
19
- Given I have the following class:
20
-
21
- ```ruby
22
- class Account
23
- def withdraw(value)
24
- if @total >= value
25
- @total -= value
26
- :ok
27
- else
28
- :not_allowed
29
- end
30
- end
31
- end
32
- ```
33
-
34
- And I'm testing it with `allow` and some possibilities:
35
-
36
- ```ruby
37
- # bad
38
- allow(Account).to receive(:withdraw).and_return(:ok)
39
- # good
40
- allow(Account).to receive(:withdraw).with(100).and_return(:ok)
41
- ```
42
-
43
- **Objective:** find all bad cases of **any** class that does not respect the method
44
- parameters signature.
45
-
46
- First, let's understand the method signature of a method:
47
-
48
- ```ruby
49
- Account.instance_method(:withdraw).parameters
50
- # => [[:req, :value]]
51
- ```
52
-
53
- Now, we can build a small script to use the node pattern to match the proper
54
- specs that are using such pattern and later visit their method signatures.
55
-
56
-
57
- ```ruby
58
- Fast.class_eval do
59
- # Captures class and method name when find syntax like:
60
- # `allow(...).to receive(...)` that does not end with `.with(...)`
61
- pattern_with_captures = <<~FAST
62
- (send (send nil allow (const nil $_)) to
63
- (send (send nil receive (sym $_)) !with))
64
- FAST
65
-
66
- pattern = expression(pattern_with_captures.tr('$',''))
67
-
68
- ruby_files_from('spec').each do |file|
69
- results = search_file(pattern, file) || [] rescue next
70
- results.each do |n|
71
- clazz, method = capture(n, pattern_with_captures)
72
- if klazz = Object.const_get(clazz.to_s) rescue nil
73
- if klazz.respond_to?(method)
74
- params = klazz.method(method).parameters
75
- if params.any?{|e|e.first == :req}
76
- code = n.loc.expression
77
- range = [code.first_line, code.last_line].uniq.join(",")
78
- boom_message = "BOOM! #{clazz}.#{method} does not include the REQUIRED parameters!"
79
- puts boom_message, "#{file}:#{range}", code.source
80
- end
81
- end
82
- end
83
- end
84
- end
85
- end
86
- ```
87
-
88
- !!! hint "Preload your environment **before** run the script"
89
-
90
- Keep in mind that you should run it with your environment preloaded otherwise it
91
- will skip the classes.
92
- You can add elses for `const_get` and `respond_to` and report weird cases if
93
- your environment is not preloading properly.