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
data/docs/index.md
DELETED
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
# Fast
|
|
2
|
-
|
|
3
|
-
<center></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
|
-
|
data/docs/pry-integration.md
DELETED
|
@@ -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.
|