ffast 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/jonatas/fast.svg?branch=master)](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
|