ffast 0.0.5 → 0.0.6
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/README.md +286 -140
- data/docs/pry-integration.md +2 -2
- data/docs/syntax.md +35 -0
- data/examples/search_duplicated.rb +15 -0
- data/lib/fast/version.rb +1 -1
- data/lib/fast.rb +14 -0
- data/mkdocs.yml +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6f6f652de04a7f59c26a9556d136e8992f6cc5631b12a3b4a28d9e6f5784d1a
|
4
|
+
data.tar.gz: 62ae2e2bf65a375c78b2d81ac52247b2b5a60a38557540b9e0b0179cc6010e10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72e1918179ab1b17a292975da6d6a2a673937032c6b924db8994edfb607c98cc80d6542a38a25597d506e23ff0c43f18d4b6a01178623c2c3cd0be982dbc4bb0
|
7
|
+
data.tar.gz: 5f3f02826dcdc0c13f906dfc5480f693c0cd6c5f6bae135a4f5cb99f7ac583a518a68333d562ad1fe61cec55f0dca8627978b81710d4d04507c89d28e9028131
|
data/README.md
CHANGED
@@ -2,31 +2,34 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/jonatas/fast)
|
4
4
|
|
5
|
-
Fast
|
5
|
+
Fast, short for "Find AST", is a tool to search, prune, and edit Ruby ASTs.
|
6
6
|
|
7
|
-
Ruby
|
8
|
-
|
7
|
+
Ruby is a flexible language that allows us to write code in multiple different ways
|
8
|
+
to achieve the same end result, and because of this it's hard to verify how
|
9
|
+
the code was written without an AST.
|
9
10
|
|
10
|
-
Check the official documentation: https://jonatas.github.io/fast.
|
11
|
+
Check out the official documentation: https://jonatas.github.io/fast.
|
11
12
|
|
12
|
-
## Syntax for find in AST
|
13
|
+
## Token Syntax for `find` in AST
|
13
14
|
|
14
|
-
The current version
|
15
|
+
The current version of Fast covers the following token elements:
|
15
16
|
|
16
|
-
- `()`
|
17
|
-
- `{}`
|
18
|
-
- `[]`
|
19
|
-
- `$`
|
20
|
-
- `
|
21
|
-
|
22
|
-
-
|
23
|
-
-
|
24
|
-
-
|
25
|
-
-
|
26
|
-
-
|
27
|
-
- `
|
17
|
+
- `()` - represents a **node** search
|
18
|
+
- `{}` - looks for **any** element to match, like a **Set** inclusion or `any?` in Ruby
|
19
|
+
- `[]` - looks for **all** elements to match, like `all?` in Ruby.
|
20
|
+
- `$` - will **capture** the contents of the current expression like a `Regex` group
|
21
|
+
- `#<method-name>` - will call `<method-name>` with `node` as param allowing you
|
22
|
+
to build custom rules.
|
23
|
+
- `_` - represents any non-nil value, or **something** being present
|
24
|
+
- `nil` - matches exactly **nil**
|
25
|
+
- `...` - matches a **node** with children
|
26
|
+
- `^` - references the **parent node** of an expression
|
27
|
+
- `?` - represents an element which **maybe** present
|
28
|
+
- `\1` - represents a substitution for any of the **previously captured** elements
|
29
|
+
- `%1` - to bind the first extra argument in an expression
|
30
|
+
- `""` - will match a literal string with double quotes
|
28
31
|
|
29
|
-
The syntax is inspired
|
32
|
+
The syntax is inspired by the [RuboCop Node Pattern](https://github.com/bbatsov/rubocop/blob/master/lib/rubocop/node_pattern.rb).
|
30
33
|
|
31
34
|
## Installation
|
32
35
|
|
@@ -34,21 +37,31 @@ The syntax is inspired on [RuboCop Node Pattern](https://github.com/bbatsov/rubo
|
|
34
37
|
|
35
38
|
## How it works
|
36
39
|
|
37
|
-
|
40
|
+
### S-Expressions
|
38
41
|
|
39
|
-
|
42
|
+
Fast works by searching the abstract syntax tree using a series of expressions
|
43
|
+
to represent code called `s-expressions`.
|
44
|
+
|
45
|
+
> `s-expressions`, or symbolic expressions, are a way to represent nested data.
|
46
|
+
> They originate from the LISP programming language, and are frequetly used in
|
47
|
+
> other languages to represent ASTs.
|
48
|
+
|
49
|
+
### Integer Literals
|
50
|
+
|
51
|
+
For example, let's take an `Integer` in Ruby:
|
40
52
|
|
41
53
|
```ruby
|
42
54
|
1
|
43
55
|
```
|
44
56
|
|
45
|
-
|
57
|
+
It's corresponding s-expression would be:
|
46
58
|
|
47
59
|
```ruby
|
48
60
|
s(:int, 1)
|
49
61
|
```
|
50
62
|
|
51
|
-
|
63
|
+
`s` in `Fast` and `Parser` are a shorthand for creating an `Parser::AST::Node`.
|
64
|
+
Each of these nodes has a `#type` and `#children` contained in it:
|
52
65
|
|
53
66
|
```ruby
|
54
67
|
def s(type, *children)
|
@@ -56,79 +69,138 @@ def s(type, *children)
|
|
56
69
|
end
|
57
70
|
```
|
58
71
|
|
59
|
-
|
72
|
+
### Variable Assignments
|
73
|
+
|
74
|
+
Now let's take a look at a local variable assignment:
|
60
75
|
|
61
76
|
```ruby
|
62
77
|
value = 42
|
63
78
|
```
|
64
79
|
|
65
|
-
|
80
|
+
It's corresponding s-expression would be:
|
66
81
|
|
67
82
|
```ruby
|
68
83
|
ast = s(:lvasgn, :value, s(:int, 42))
|
69
84
|
```
|
70
85
|
|
71
|
-
|
86
|
+
If we wanted to find this particular assignment somewhere in our AST, we can use
|
87
|
+
Fast to look for a local variable named `value` with a value `42`:
|
72
88
|
|
73
89
|
```ruby
|
74
|
-
Fast.match?(ast, '(lvasgn value (int 42))')
|
90
|
+
Fast.match?(ast, '(lvasgn value (int 42))')
|
91
|
+
# => true
|
75
92
|
```
|
76
93
|
|
77
|
-
|
94
|
+
### Wildcard Token
|
95
|
+
|
96
|
+
If we wanted to find a variable named `value` that was assigned any integer value
|
97
|
+
we could replace `42` in our query with an underscore ( `_` ) as a shortcut:
|
78
98
|
|
79
99
|
```ruby
|
80
|
-
Fast.match?(ast, '(lvasgn value (int _))')
|
100
|
+
Fast.match?(ast, '(lvasgn value (int _))')
|
101
|
+
# => true
|
81
102
|
```
|
82
103
|
|
83
|
-
|
104
|
+
### Set Inclusion Token
|
105
|
+
|
106
|
+
If we weren't sure the type of the value we're assigning, we can use our set
|
107
|
+
inclusion token (`{}`) from earlier to tell Fast that we expect either a `Float`
|
108
|
+
or an `Integer`:
|
84
109
|
|
85
110
|
```ruby
|
86
|
-
Fast.match?(ast, '(lvasgn value ({float int} _))')
|
111
|
+
Fast.match?(ast, '(lvasgn value ({float int} _))')
|
112
|
+
# => true
|
87
113
|
```
|
88
114
|
|
89
|
-
|
115
|
+
### All Matching Token
|
116
|
+
|
117
|
+
Say we wanted to say what we expect the value's type to _not_ be, we can use the
|
118
|
+
all matching token (`[]`) to express multiple conditions that need to be true.
|
119
|
+
In this case we don't want the value to be a `String`, `Hash`, or an `Array` by
|
120
|
+
prefixing all of the types with `!`:
|
90
121
|
|
91
122
|
```ruby
|
92
123
|
Fast.match?(ast, '(lvasgn value ([!str !hash !array] _))') # true
|
93
124
|
```
|
94
125
|
|
95
|
-
|
126
|
+
### Node Child Token
|
127
|
+
|
128
|
+
We can match any node with children by using the child token ( `...` ):
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
Fast.match?(ast, '(lvasgn value ...)')
|
132
|
+
# => true
|
133
|
+
```
|
134
|
+
|
135
|
+
We could even match any local variable assignment combining both `_` and `...`:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
Fast.match?(ast, '(lvasgn _ ...)')
|
139
|
+
# => true
|
140
|
+
```
|
141
|
+
|
142
|
+
### Capturing the Value of an Expression
|
96
143
|
|
97
|
-
|
144
|
+
You can use `$` to capture the contents of an expression for later use:
|
98
145
|
|
99
146
|
```ruby
|
100
|
-
Fast.match?(ast, '(lvasgn value
|
147
|
+
Fast.match?(ast, '(lvasgn value $...)')
|
148
|
+
# => [s(:int, 42)]
|
101
149
|
```
|
102
150
|
|
103
|
-
|
151
|
+
Captures can be used in any position as many times as you want to capture whatever
|
152
|
+
information you might need:
|
104
153
|
|
105
154
|
```ruby
|
106
|
-
Fast.match?(ast, '(lvasgn
|
155
|
+
Fast.match?(ast, '(lvasgn $_ $...)')
|
156
|
+
# => [:value, s(:int, 42)]
|
107
157
|
```
|
108
158
|
|
109
|
-
|
159
|
+
> Keep in mind that `_` means something not nil and `...` means a node with
|
160
|
+
> children.
|
161
|
+
|
162
|
+
### Calling Custom Methods
|
163
|
+
|
164
|
+
You can also define custom methods to set more complicated rules. Let's say
|
165
|
+
we're looking for duplicated methods in the same class. We need to collect
|
166
|
+
method names and guarantee they are unique.
|
110
167
|
|
111
168
|
```ruby
|
112
|
-
|
169
|
+
def duplicated(method_name)
|
170
|
+
@methods ||= []
|
171
|
+
already_exists = @methods.include?(method_name)
|
172
|
+
@methods << method_name
|
173
|
+
already_exists
|
174
|
+
end
|
175
|
+
|
176
|
+
puts Fast.search_file( '(def #duplicated)', 'example.rb')
|
113
177
|
```
|
178
|
+
The same principle can be used in the node level or for debugging purposes.
|
114
179
|
|
115
|
-
|
180
|
+
```ruby
|
181
|
+
require 'pry'
|
182
|
+
def debug(node)
|
183
|
+
binding.pry
|
184
|
+
end
|
116
185
|
|
186
|
+
puts Fast.search_file('#debug', 'example.rb')
|
187
|
+
```
|
188
|
+
If you want to get only `def` nodes you can also intersect expressions with `[]`:
|
117
189
|
```ruby
|
118
|
-
Fast.
|
190
|
+
puts Fast.search_file('[ def #debug ]', 'example.rb')
|
119
191
|
```
|
120
192
|
|
121
|
-
|
122
|
-
children.
|
193
|
+
### Methods
|
123
194
|
|
124
|
-
|
195
|
+
Let's take a look at a method declaration:
|
125
196
|
|
126
197
|
```ruby
|
127
198
|
def my_method
|
128
199
|
call_other_method
|
129
200
|
end
|
130
201
|
```
|
131
|
-
|
202
|
+
|
203
|
+
It's corresponding s-expression would be:
|
132
204
|
|
133
205
|
```ruby
|
134
206
|
ast =
|
@@ -137,13 +209,14 @@ ast =
|
|
137
209
|
s(:send, nil, :call_other_method))
|
138
210
|
```
|
139
211
|
|
140
|
-
|
212
|
+
Pay close attention to the node `(args)`. We can't use `...` to match it, as it
|
213
|
+
has no children (or arguments in this case), but we _can_ match it with a wildcard
|
214
|
+
`_` as it's not `nil`.
|
141
215
|
|
142
|
-
|
143
|
-
such case.
|
216
|
+
### Call Chains
|
144
217
|
|
145
|
-
Let's
|
146
|
-
`a.b.c.d
|
218
|
+
Let's take a look at a few other examples. Sometimes you have a chain of calls on
|
219
|
+
a single `Object`, like `a.b.c.d`. Its corresponding s-expression would be:
|
147
220
|
|
148
221
|
```ruby
|
149
222
|
ast =
|
@@ -156,59 +229,79 @@ ast =
|
|
156
229
|
:d)
|
157
230
|
```
|
158
231
|
|
159
|
-
|
232
|
+
### Alternate Syntax
|
233
|
+
|
234
|
+
You can also search using nested arrays with **pure values**, or **shortcuts** or
|
160
235
|
**procs**:
|
161
236
|
|
162
237
|
```ruby
|
163
|
-
Fast.match?(ast, [:send, [:send, '...'], :d])
|
164
|
-
|
165
|
-
|
238
|
+
Fast.match?(ast, [:send, [:send, '...'], :d])
|
239
|
+
# => true
|
240
|
+
|
241
|
+
Fast.match?(ast, [:send, [:send, '...'], :c])
|
242
|
+
# => false
|
243
|
+
|
244
|
+
Fast.match?(ast, [:send, [:send, [:send, '...'], :c], :d])
|
245
|
+
# => true
|
166
246
|
```
|
167
247
|
|
168
|
-
|
169
|
-
procs directly
|
248
|
+
Shortcut tokens like child nodes `...` and wildcards `_` are just placeholders
|
249
|
+
for procs. If you want, you can even use procs directly like so:
|
170
250
|
|
171
251
|
```ruby
|
172
|
-
Fast.match?(ast, [
|
252
|
+
Fast.match?(ast, [
|
253
|
+
:send, [
|
254
|
+
-> (node) { node.type == :send },
|
255
|
+
[:send, '...'],
|
256
|
+
:c
|
257
|
+
],
|
258
|
+
:d
|
259
|
+
])
|
260
|
+
# => true
|
173
261
|
```
|
174
262
|
|
175
|
-
|
263
|
+
This also works with expressions:
|
176
264
|
|
177
265
|
```ruby
|
178
266
|
Fast.match?(
|
179
267
|
ast,
|
180
268
|
'(send (send (send (send nil $_) $_) $_) $_)'
|
181
|
-
)
|
269
|
+
)
|
270
|
+
# => [:a, :b, :c, :d]
|
182
271
|
```
|
183
272
|
|
184
|
-
|
273
|
+
### Debugging
|
274
|
+
|
275
|
+
If you find that a particular expression isn't working, you can use `debug` to
|
276
|
+
take a look at what Fast is doing:
|
185
277
|
|
186
278
|
```ruby
|
187
279
|
Fast.debug { Fast.match?(s(:int, 1), [:int, 1]) }
|
188
280
|
```
|
189
281
|
|
190
|
-
|
282
|
+
Each comparison made while searching will be logged to your console (STDOUT) as
|
283
|
+
Fast goes through the AST:
|
191
284
|
|
192
285
|
```
|
193
286
|
int == (int 1) # => true
|
194
287
|
1 == 1 # => true
|
195
288
|
```
|
196
|
-
## Bind arguments to expressions
|
197
289
|
|
198
|
-
|
199
|
-
based on dynamic decisions or other external input.
|
200
|
-
For such cases you can bind the arguments with `%` and the index start from `1`.
|
290
|
+
## Bind arguments to expressions
|
201
291
|
|
202
|
-
|
292
|
+
We can also dynamically interpolate arguments into our queries using the
|
293
|
+
interpolation token `%`. This works much like `sprintf` using indexes starting
|
294
|
+
from `1`:
|
203
295
|
|
204
296
|
```ruby
|
205
|
-
Fast.match?(code
|
297
|
+
Fast.match?(code('a = 1'), '(lvasgn %1 (int _))', :a)
|
298
|
+
# => true
|
206
299
|
```
|
207
300
|
|
208
|
-
##
|
301
|
+
## Using previous captures in search
|
209
302
|
|
210
303
|
Imagine you're looking for a method that is just delegating something to
|
211
|
-
another method, like:
|
304
|
+
another method, like this `name` method:
|
212
305
|
|
213
306
|
```ruby
|
214
307
|
def name
|
@@ -225,54 +318,68 @@ This can be represented as the following AST:
|
|
225
318
|
(send nil :person) :name))
|
226
319
|
```
|
227
320
|
|
228
|
-
|
229
|
-
name:
|
321
|
+
We can create a query that searches for such a method:
|
230
322
|
|
231
323
|
```ruby
|
232
|
-
Fast.match?(ast,'(def $_ ... (send (send nil _) \1))')
|
324
|
+
Fast.match?(ast,'(def $_ ... (send (send nil _) \1))')
|
325
|
+
# => [:name]
|
233
326
|
```
|
234
327
|
|
235
328
|
## Fast.search
|
236
329
|
|
237
|
-
Search allows you to go
|
238
|
-
|
330
|
+
Search allows you to go search the entire AST, collecting nodes that matches given
|
331
|
+
expression. Any matching node is then returned:
|
239
332
|
|
240
333
|
```ruby
|
241
|
-
Fast.search(code('a = 1'), '(int _)')
|
334
|
+
Fast.search(code('a = 1'), '(int _)')
|
335
|
+
# => s(:int, 1)
|
242
336
|
```
|
243
337
|
|
244
|
-
If you use captures,
|
338
|
+
If you use captures along with a search, both the matching nodes and the
|
339
|
+
captures will be returned:
|
245
340
|
|
246
341
|
```ruby
|
247
|
-
Fast.search(code('a = 1'), '(int $_)')
|
342
|
+
Fast.search(code('a = 1'), '(int $_)')
|
343
|
+
# => [s(:int, 1), 1]
|
248
344
|
```
|
249
345
|
|
250
346
|
## Fast.capture
|
251
347
|
|
252
|
-
To pick
|
348
|
+
To only pick captures and ignore the nodes, use `Fast.capture`:
|
253
349
|
|
254
350
|
```ruby
|
255
|
-
Fast.capture(code('a = 1'), '(int $_)')
|
351
|
+
Fast.capture(code('a = 1'), '(int $_)')
|
352
|
+
# => 1
|
256
353
|
```
|
257
354
|
## Fast.replace
|
258
355
|
|
259
|
-
|
356
|
+
<!--
|
357
|
+
Not sure how this section works, could you explain it in more detail?
|
358
|
+
|
359
|
+
It looks to capture the name of a method, and then not sure from there. Can
|
360
|
+
you provide an example AST to use there?
|
361
|
+
|
362
|
+
Delegate might be too dense of an example to use.
|
363
|
+
-->
|
364
|
+
|
365
|
+
If we want to replace code, we can use a delegate expression:
|
260
366
|
|
261
367
|
```ruby
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
368
|
+
query = '(def $_ ... (send (send nil $_) \1))'
|
369
|
+
|
370
|
+
Fast.replace ast, query, -> (node, captures) {
|
371
|
+
attribute, object = captures
|
372
|
+
|
373
|
+
replace(
|
374
|
+
node.location.expression,
|
375
|
+
"delegate :#{attribute}, to: :#{object}"
|
376
|
+
)
|
377
|
+
}
|
271
378
|
```
|
272
379
|
|
273
380
|
### Replacing file
|
274
381
|
|
275
|
-
Now let's imagine we have
|
382
|
+
Now let's imagine we have a file like `sample.rb` with the following code:
|
276
383
|
|
277
384
|
```ruby
|
278
385
|
def good_bye
|
@@ -281,33 +388,45 @@ def good_bye
|
|
281
388
|
end
|
282
389
|
```
|
283
390
|
|
284
|
-
|
391
|
+
...and we decide to inline the contents of the `message` variable right after
|
392
|
+
`puts`.
|
393
|
+
|
285
394
|
|
286
|
-
|
287
|
-
|
288
|
-
|
395
|
+
To do this we would need to:
|
396
|
+
|
397
|
+
* Remove the local variable assignment
|
398
|
+
* Store the now-removed variable's value
|
399
|
+
* Substitute the value where the variable was used before
|
289
400
|
|
290
401
|
```ruby
|
291
402
|
assignment = nil
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
403
|
+
query = '({ lvasgn lvar } message )'
|
404
|
+
|
405
|
+
Fast.replace_file('sample.rb', query, -> (node, _) {
|
406
|
+
# Find a variable assignment
|
407
|
+
if node.type == :lvasgn
|
408
|
+
assignment = node.children.last
|
409
|
+
|
410
|
+
# Remove the node responsible for the assignment
|
411
|
+
remove(node.location.expression)
|
412
|
+
# Look for the variable being used
|
413
|
+
elsif node.type == :lvar
|
414
|
+
# Replace the variable with the contents of the variable
|
415
|
+
replace(
|
416
|
+
node.location.expression,
|
417
|
+
assignment.location.expression.source
|
418
|
+
)
|
419
|
+
end
|
420
|
+
})
|
302
421
|
```
|
303
422
|
|
304
423
|
## Other useful functions
|
305
424
|
|
306
|
-
To manipulate ruby files,
|
425
|
+
To manipulate ruby files, sometimes you'll need some extra tasks.
|
307
426
|
|
308
427
|
## Fast.ast_from_File(file)
|
309
428
|
|
310
|
-
This method parses
|
429
|
+
This method parses code from a file and loads it into an AST representation.
|
311
430
|
|
312
431
|
```ruby
|
313
432
|
Fast.ast_from_file('sample.rb')
|
@@ -315,20 +434,17 @@ Fast.ast_from_file('sample.rb')
|
|
315
434
|
|
316
435
|
## Fast.search_file
|
317
436
|
|
318
|
-
You can use `search_file`
|
319
|
-
files.
|
437
|
+
You can use `search_file` to for search for expressions inside files.
|
320
438
|
|
321
439
|
```ruby
|
322
440
|
Fast.search_file(expression, 'file.rb')
|
323
441
|
```
|
324
442
|
|
325
|
-
It's
|
326
|
-
|
443
|
+
It's a combination of `Fast.ast_from_file` with `Fast.search`.
|
327
444
|
|
328
445
|
## Fast.capture_file
|
329
446
|
|
330
|
-
You can
|
331
|
-
captures:
|
447
|
+
You can use `Fast.capture_file` to only return captures:
|
332
448
|
|
333
449
|
```ruby
|
334
450
|
Fast.capture_file('(class (const nil $_))', 'lib/fast.rb')
|
@@ -337,36 +453,46 @@ captures:
|
|
337
453
|
|
338
454
|
## Fast.ruby_files_from(arguments)
|
339
455
|
|
340
|
-
|
341
|
-
all internal `.rb` files
|
456
|
+
`Fast.ruby_files_from(arguments)` can get all Ruby files in a location:
|
342
457
|
|
343
458
|
```ruby
|
344
|
-
Fast.ruby_files_from('lib')
|
459
|
+
Fast.ruby_files_from('lib')
|
460
|
+
# => ["lib/fast.rb"]
|
345
461
|
```
|
346
462
|
|
347
463
|
## `fast` in the command line
|
348
464
|
|
349
|
-
|
350
|
-
find code
|
465
|
+
Fast also comes with a command line utility called `fast`. You can use it to
|
466
|
+
search and find code much like the library version:
|
351
467
|
|
352
468
|
```
|
353
469
|
$ fast '(def match?)' lib/fast.rb
|
354
470
|
```
|
355
471
|
|
472
|
+
The CLI tool takes the following flags
|
473
|
+
|
356
474
|
- Use `-d` or `--debug` for enable debug mode.
|
357
475
|
- Use `--ast` to output the AST instead of the original code
|
358
476
|
- Use `--pry` to jump debugging the first result with pry
|
359
477
|
- Use `-c` to search from code example
|
360
478
|
- Use `-s` to search similar code
|
361
479
|
|
480
|
+
### Fast with Pry
|
481
|
+
|
482
|
+
You can use `--pry` to stop on a particular source node, and run Pry at that
|
483
|
+
location:
|
484
|
+
|
362
485
|
```
|
363
486
|
$ fast '(block (send nil it))' spec --pry
|
364
487
|
```
|
365
|
-
|
366
|
-
|
488
|
+
|
489
|
+
Inside the pry session you can access `result` for the first result that was
|
490
|
+
located, or `results` to get all of the occurrences found.
|
491
|
+
|
492
|
+
Let's take a look at `results`:
|
367
493
|
|
368
494
|
```ruby
|
369
|
-
results.map{|e|e.children[0].children[2]}
|
495
|
+
results.map { |e| e.children[0].children[2] }
|
370
496
|
# => [s(:str, "parses ... as Find"),
|
371
497
|
# s(:str, "parses $ as Capture"),
|
372
498
|
# s(:str, "parses quoted values as strings"),
|
@@ -374,9 +500,16 @@ results.map{|e|e.children[0].children[2]}
|
|
374
500
|
# s(:str, "parses [] as All"), ...]
|
375
501
|
```
|
376
502
|
|
377
|
-
|
503
|
+
### Fast with RSpec
|
504
|
+
|
505
|
+
Let's say we wanted to get all the `it` blocks in our `RSpec` code that
|
506
|
+
currently do not have descriptions:
|
378
507
|
|
379
|
-
|
508
|
+
```
|
509
|
+
$ fast '(block (send nil it (nil)) (args) (!str)) ) )' spec
|
510
|
+
```
|
511
|
+
|
512
|
+
This will return the following:
|
380
513
|
|
381
514
|
```ruby
|
382
515
|
# spec/fast_spec.rb:166
|
@@ -390,31 +523,44 @@ it { expect(described_class).to be_match(code['"string"'], '(str "string")') }
|
|
390
523
|
|
391
524
|
## Experiments
|
392
525
|
|
393
|
-
|
394
|
-
|
526
|
+
Experiments can be used to run experiments against your code in an automated
|
527
|
+
fashion. These experiments can be used to test the effectiveness of things
|
528
|
+
like performance enhancements, or if a replacement piece of code actually works
|
529
|
+
or not.
|
530
|
+
|
531
|
+
Let's create an experiment to try and remove all `before` and `after` blocks
|
532
|
+
from our specs.
|
395
533
|
|
396
|
-
|
397
|
-
and run specs. If the spec pass without need the hook, the hook is useless.
|
534
|
+
If the spec still pass we can confidently say that the hook is useless.
|
398
535
|
|
399
536
|
```ruby
|
400
537
|
Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
|
538
|
+
# Lookup our spec files
|
401
539
|
lookup 'spec'
|
540
|
+
|
541
|
+
# Look for every block starting with before or after
|
402
542
|
search "(block (send nil {before after}))"
|
403
|
-
|
404
|
-
|
543
|
+
|
544
|
+
# Remove those blocks
|
545
|
+
edit { |node| remove(node.loc.expression) }
|
546
|
+
|
547
|
+
# Create a new file, and run RSpec against that new file
|
548
|
+
policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
|
405
549
|
end
|
406
550
|
```
|
407
551
|
|
408
|
-
-
|
409
|
-
-
|
410
|
-
-
|
411
|
-
-
|
552
|
+
- `lookup` can be used to pass in files or folders.
|
553
|
+
- `search` contains the expression you want to match
|
554
|
+
- `edit` is used to apply code change
|
555
|
+
- `policy` is what we execute to verify the current change still passes
|
556
|
+
|
557
|
+
Each removal of a `before` and `after` block will occur in isolation to verify
|
558
|
+
each one of them independently of the others. Each successful removal will be
|
559
|
+
kept in a secondary change until we run out of blocks to remove.
|
412
560
|
|
413
|
-
|
414
|
-
occur independently and the successfull removals will be combined as a
|
415
|
-
secondary change. The process repeates until find all possible combinations.
|
561
|
+
You can see more examples in the [experiments](experiments) folder.
|
416
562
|
|
417
|
-
|
563
|
+
### Running Multiple Experiments
|
418
564
|
|
419
565
|
To run multiple experiments, use `fast-experiment` runner:
|
420
566
|
|
@@ -422,7 +568,7 @@ To run multiple experiments, use `fast-experiment` runner:
|
|
422
568
|
fast-experiment <experiment-names> <files-or-folders>
|
423
569
|
```
|
424
570
|
|
425
|
-
You can limit
|
571
|
+
You can limit the scope of experiments:
|
426
572
|
|
427
573
|
```
|
428
574
|
fast-experiment RSpec/RemoveUselessBeforeAfterHook spec/models/**/*_spec.rb
|
@@ -434,7 +580,9 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
434
580
|
|
435
581
|
On the console we have a few functions like `s` and `code` to make it easy ;)
|
436
582
|
|
437
|
-
|
583
|
+
```
|
584
|
+
$ bin/console
|
585
|
+
```
|
438
586
|
|
439
587
|
```ruby
|
440
588
|
code("a = 1") # => s(:lvasgn, s(:int, 1))
|
@@ -446,10 +594,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
446
594
|
|
447
595
|
Bug reports and pull requests are welcome on GitHub at https://github.com/jonatas/fast. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
448
596
|
|
449
|
-
|
450
597
|
## License
|
451
598
|
|
452
599
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
453
600
|
|
454
|
-
|
455
601
|
See more on the [official documentation](https://jonatas.github.io/fast).
|
data/docs/pry-integration.md
CHANGED
data/docs/syntax.md
CHANGED
@@ -368,3 +368,38 @@ ANSWER = 42
|
|
368
368
|
ANSWER
|
369
369
|
```
|
370
370
|
|
371
|
+
## Calling Custom Methods
|
372
|
+
|
373
|
+
Custom methods can let you into ruby doman for more complicated rules. Let's say
|
374
|
+
we're looking for duplicated methods in the same class. We need to collect
|
375
|
+
method names and guarantee they are unique.
|
376
|
+
|
377
|
+
```ruby
|
378
|
+
def duplicated(method_name)
|
379
|
+
@methods ||= []
|
380
|
+
already_exists = @methods.include?(method_name)
|
381
|
+
@methods << method_name
|
382
|
+
already_exists
|
383
|
+
end
|
384
|
+
|
385
|
+
puts Fast.search_file( '(def #duplicated)', 'example.rb')
|
386
|
+
```
|
387
|
+
The same principle can be used in the node level or for debugging purposes.
|
388
|
+
|
389
|
+
```ruby
|
390
|
+
require 'pry'
|
391
|
+
def debug(node)
|
392
|
+
binding.pry
|
393
|
+
end
|
394
|
+
|
395
|
+
puts Fast.search_file('#debug', 'example.rb')
|
396
|
+
```
|
397
|
+
If you want to get only `def` nodes you can also intersect expressions with `[]`:
|
398
|
+
```ruby
|
399
|
+
puts Fast.search_file('[ def #debug ]', 'example.rb')
|
400
|
+
```
|
401
|
+
Or if you want to debug a very specific expression you can use `()` to specify
|
402
|
+
more details of the node
|
403
|
+
```ruby
|
404
|
+
puts Fast.search_file('[ (def a) #debug ]', 'example.rb')
|
405
|
+
```
|
@@ -0,0 +1,15 @@
|
|
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
|
+
|
data/lib/fast/version.rb
CHANGED
data/lib/fast.rb
CHANGED
@@ -54,6 +54,8 @@ module Fast
|
|
54
54
|
|
|
55
55
|
\$ # capture
|
56
56
|
|
|
57
|
+
\#\w[\d\w_]+[\\!\?]? # custom method call
|
58
|
+
|
|
57
59
|
\\\d # find using captured expression
|
58
60
|
|
|
59
61
|
%\d # find using binded argument
|
@@ -258,6 +260,7 @@ module Fast
|
|
258
260
|
when '{' then Any.new(parse_until_peek('}'))
|
259
261
|
when '[' then All.new(parse_until_peek(']'))
|
260
262
|
when /^"/ then FindString.new(token[1..-2])
|
263
|
+
when /^#\w/ then MethodCall.new(token[1..-1])
|
261
264
|
when '$' then Capture.new(parse)
|
262
265
|
when '!' then (@tokens.any? ? Not.new(parse) : Find.new(token))
|
263
266
|
when '?' then Maybe.new(parse)
|
@@ -375,6 +378,17 @@ module Fast
|
|
375
378
|
end
|
376
379
|
end
|
377
380
|
|
381
|
+
# Find using custom methods
|
382
|
+
class MethodCall < Find
|
383
|
+
def initialize(method_name)
|
384
|
+
@method_name = method_name
|
385
|
+
end
|
386
|
+
|
387
|
+
def match?(node)
|
388
|
+
Kernel.send(@method_name, node)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
378
392
|
# Allow use previous captures while searching in the AST.
|
379
393
|
# Use `\\1` to point the match to the first captured element
|
380
394
|
class FindWithCapture < Find
|
data/mkdocs.yml
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ffast
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jônatas Davi Paganini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: coderay
|
@@ -213,6 +213,7 @@ files:
|
|
213
213
|
- examples/find_usage.rb
|
214
214
|
- examples/let_it_be_experiment.rb
|
215
215
|
- examples/method_complexity.rb
|
216
|
+
- examples/search_duplicated.rb
|
216
217
|
- examples/similarity_research.rb
|
217
218
|
- experiments/let_it_be_experiment.rb
|
218
219
|
- experiments/remove_useless_hook.rb
|