ffast 0.0.1 → 0.0.2
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/.rubocop.yml +37 -0
- data/.travis.yml +9 -0
- data/Gemfile +2 -0
- data/Guardfile +4 -2
- data/README.md +301 -28
- data/Rakefile +5 -3
- data/TODO.md +0 -3
- data/bin/console +8 -4
- data/bin/fast +18 -32
- data/bin/fast-experiment +34 -0
- data/examples/experimental_replacement.rb +46 -0
- data/examples/find_usage.rb +26 -0
- data/examples/method_complexity.rb +37 -0
- data/experiments/let_it_be_experiment.rb +9 -0
- data/experiments/remove_useless_hook.rb +9 -0
- data/fast.gemspec +20 -18
- data/lib/fast.rb +514 -114
- metadata +51 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 238c006c217b8838c23488cbfafe15c3e694d69a
|
4
|
+
data.tar.gz: '095e4dfdba32adf0ddf52da166c27a93c8bbd3bb'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 73ce257e59e1bd96cbc62d4b23225cd50f5bfa551ca64eeb38da4693bc832b14203d2335d6e55e389b74fd5feba1e164ed2f1adbcdbeca0a14676115e1df5c02
|
7
|
+
data.tar.gz: 3133c3f337da981e728ab42ed9bf61eafc907715c3c6bb7c210ff38d6a6c73d3e87cf88625d8cac4dcfa648d3f909b3e6625a502d96c9a2316e04436086c7f8f
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# This is the configuration used to check the rubocop source code.
|
2
|
+
|
3
|
+
require:
|
4
|
+
- rubocop-rspec
|
5
|
+
|
6
|
+
AllCops:
|
7
|
+
Exclude:
|
8
|
+
- 'tmp/**/*'
|
9
|
+
- 'examples/*'
|
10
|
+
TargetRubyVersion: 2.3
|
11
|
+
|
12
|
+
Metrics/LineLength:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Metrics/BlockLength:
|
16
|
+
Exclude:
|
17
|
+
- 'spec/**/*'
|
18
|
+
|
19
|
+
Lint/InterpolationCheck:
|
20
|
+
Exclude:
|
21
|
+
- 'spec/**/*'
|
22
|
+
|
23
|
+
Metrics/MethodLength:
|
24
|
+
CountComments: false # count full line comments?
|
25
|
+
Max: 12
|
26
|
+
|
27
|
+
Metrics/ModuleLength:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Layout/MultilineMethodCallIndentation:
|
31
|
+
EnforcedStyle: 'indented'
|
32
|
+
|
33
|
+
RSpec/NestedGroups:
|
34
|
+
Max: 4
|
35
|
+
|
36
|
+
RSpec/ExampleLength:
|
37
|
+
Max: 20
|
data/.travis.yml
CHANGED
@@ -3,3 +3,12 @@ language: ruby
|
|
3
3
|
rvm:
|
4
4
|
- 2.3.3
|
5
5
|
before_install: gem install bundler -v 1.13.7
|
6
|
+
before_script:
|
7
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
8
|
+
- chmod +x ./cc-test-reporter
|
9
|
+
- ./cc-test-reporter before-build
|
10
|
+
after_script:
|
11
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
12
|
+
script:
|
13
|
+
- bundle exec rubocop
|
14
|
+
- bundle exec rspec
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# A sample Guardfile
|
2
4
|
# More info at https://github.com/guard/guard#readme
|
3
5
|
|
@@ -19,8 +21,8 @@ guard 'livereload' do
|
|
19
21
|
watch(%r{lib/.+\.rb$})
|
20
22
|
end
|
21
23
|
|
22
|
-
guard :rspec, cmd:
|
23
|
-
require
|
24
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
25
|
+
require 'guard/rspec/dsl'
|
24
26
|
dsl = Guard::RSpec::Dsl.new(self)
|
25
27
|
|
26
28
|
# Feel free to open issues for suggestions and improvements
|
data/README.md
CHANGED
@@ -4,21 +4,33 @@
|
|
4
4
|
|
5
5
|
Fast is a "Find AST" tool to help you search in the code abstract syntax tree.
|
6
6
|
|
7
|
-
|
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.
|
8
9
|
|
9
|
-
|
10
|
-
your current code.
|
10
|
+
## Syntax for find in AST
|
11
11
|
|
12
|
-
|
12
|
+
The current version cover the following elements:
|
13
13
|
|
14
|
-
|
14
|
+
- `()` to represent a **node** search
|
15
|
+
- `{}` is for **any** matches like **union** conditions with **or** operator
|
16
|
+
- `[]` is for **all** matches like **intersect** conditions with **and** operator
|
17
|
+
- `$` is for **capture** current expression
|
18
|
+
- `_` is **something** not nil
|
19
|
+
- `nil` matches exactly **nil**
|
20
|
+
- `...` is a **node** with children
|
21
|
+
- `^` is to get the **parent node** of an expression
|
22
|
+
- `?` is for **maybe**
|
23
|
+
- `\1` to use the first **previous captured** element
|
24
|
+
- `""` surround the value with double quotes to match literal strings
|
25
|
+
|
26
|
+
The syntax is inspired on [RuboCop Node Pattern](https://github.com/bbatsov/rubocop/blob/master/lib/rubocop/node_pattern.rb).
|
15
27
|
|
16
28
|
## Installation
|
17
29
|
|
18
30
|
Add this line to your application's Gemfile:
|
19
31
|
|
20
32
|
```ruby
|
21
|
-
gem '
|
33
|
+
gem 'ffast'
|
22
34
|
```
|
23
35
|
|
24
36
|
And then execute:
|
@@ -27,44 +39,119 @@ And then execute:
|
|
27
39
|
|
28
40
|
Or install it yourself as:
|
29
41
|
|
30
|
-
$ gem install
|
42
|
+
$ gem install ffast
|
31
43
|
|
32
44
|
## How it works
|
33
45
|
|
34
46
|
The idea is search in abstract tree using a simple expression build with an array:
|
35
47
|
|
36
|
-
|
48
|
+
A simple integer in ruby:
|
37
49
|
|
38
50
|
```ruby
|
39
|
-
|
51
|
+
1
|
40
52
|
```
|
41
53
|
|
42
|
-
|
54
|
+
Is represented by:
|
43
55
|
|
44
56
|
```ruby
|
45
|
-
|
46
|
-
s(:op_asgn,
|
47
|
-
s(:lvasgn, :a),
|
48
|
-
:+,
|
49
|
-
s(:int, 1)
|
50
|
-
)
|
57
|
+
s(:int, 1)
|
51
58
|
```
|
52
59
|
|
53
60
|
Basically `s` represents `Parser::AST::Node` and the node has a `#type` and `#children`.
|
54
61
|
|
55
|
-
|
62
|
+
```ruby
|
63
|
+
def s(type, *children)
|
64
|
+
Parser::AST::Node.new(type, children)
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
A local variable assignment:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
value = 42
|
72
|
+
```
|
73
|
+
|
74
|
+
Can be represented with:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
ast = s(:lvasgn, :value, s(:int, 42))
|
78
|
+
```
|
79
|
+
|
80
|
+
Now, lets find local variable named `value` with an value `42`:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
Fast.match?(ast, '(lvasgn value (int 42))') # true
|
84
|
+
```
|
85
|
+
|
86
|
+
Lets abstract a bit and allow some integer value using `_` as a shortcut:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
Fast.match?(ast, '(lvasgn value (int _))') # true
|
90
|
+
```
|
91
|
+
|
92
|
+
Lets abstract more and allow float or integer:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
Fast.match?(ast, '(lvasgn value ({float int} _))') # true
|
96
|
+
```
|
97
|
+
|
98
|
+
Or combine multiple assertions using `[]` to join conditions:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
Fast.match?(ast, '(lvasgn value ([!str !hash !array] _))') # true
|
102
|
+
```
|
103
|
+
|
104
|
+
Matches all local variables not string **and** not hash **and** not array.
|
105
|
+
|
106
|
+
We can match "a node with children" using `...`:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
Fast.match?(ast, '(lvasgn value ...)') # true
|
110
|
+
```
|
111
|
+
|
112
|
+
You can use `$` to capture a node:
|
56
113
|
|
57
114
|
```ruby
|
58
|
-
Fast.match?(ast,
|
115
|
+
Fast.match?(ast, '(lvasgn value $...)') # => [s(:int, 42)]
|
59
116
|
```
|
60
117
|
|
61
|
-
|
118
|
+
Or match whatever local variable assignment combining both `_` and `...`:
|
62
119
|
|
63
120
|
```ruby
|
64
|
-
Fast.match?(ast,
|
121
|
+
Fast.match?(ast, '(lvasgn _ ...)') # true
|
65
122
|
```
|
66
123
|
|
67
|
-
You can
|
124
|
+
You can also use captures in any levels you want:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
Fast.match?(ast, '(lvasgn $_ $...)') # [:value, s(:int, 42)]
|
128
|
+
```
|
129
|
+
|
130
|
+
Keep in mind that `_` means something not nil and `...` means a node with
|
131
|
+
children.
|
132
|
+
|
133
|
+
Then, if do you get a method declared:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
def my_method
|
137
|
+
call_other_method
|
138
|
+
end
|
139
|
+
```
|
140
|
+
It will be represented with the following structure:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
ast =
|
144
|
+
s(:def, :my_method,
|
145
|
+
s(:args),
|
146
|
+
s(:send, nil, :call_other_method))
|
147
|
+
```
|
148
|
+
|
149
|
+
Keep an eye on the node `(args)`.
|
150
|
+
|
151
|
+
Then you know you can't use `...` but you can match with `(_)` to match with
|
152
|
+
such case.
|
153
|
+
|
154
|
+
Let's test a few other examples. You can go deeply with the arrays. Let's suppose we have a hardcore call to
|
68
155
|
`a.b.c.d` and the following AST represents it:
|
69
156
|
|
70
157
|
```ruby
|
@@ -78,7 +165,8 @@ ast =
|
|
78
165
|
:d)
|
79
166
|
```
|
80
167
|
|
81
|
-
You can search using sub-arrays
|
168
|
+
You can search using sub-arrays with **pure values**, or **shortcuts** or
|
169
|
+
**procs**:
|
82
170
|
|
83
171
|
```ruby
|
84
172
|
Fast.match?(ast, [:send, [:send, '...'], :d]) # => true
|
@@ -86,11 +174,20 @@ Fast.match?(ast, [:send, [:send, '...'], :c]) # => false
|
|
86
174
|
Fast.match?(ast, [:send, [:send, [:send, '...'], :c], :d]) # => true
|
87
175
|
```
|
88
176
|
|
89
|
-
|
177
|
+
Shortcuts like `...` and `_` are just literals for procs. Then you can use
|
178
|
+
procs directly too:
|
90
179
|
|
91
180
|
```ruby
|
92
|
-
|
93
|
-
|
181
|
+
Fast.match?(ast, [:send, [ -> (node) { node.type == :send }, [:send, '...'], :c], :d]) # => true
|
182
|
+
```
|
183
|
+
|
184
|
+
And also work with expressions:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
Fast.match?(
|
188
|
+
ast,
|
189
|
+
'(send (send (send (send nil $_) $_) $_) $_)'
|
190
|
+
) # => [:a, :b, :c, :d]
|
94
191
|
```
|
95
192
|
|
96
193
|
If something does not work you can debug with a block:
|
@@ -106,22 +203,198 @@ int == (int 1) # => true
|
|
106
203
|
1 == 1 # => true
|
107
204
|
```
|
108
205
|
|
109
|
-
##
|
206
|
+
## Use previous captures in search
|
207
|
+
|
208
|
+
Imagine you're looking for a method that is just delegating something to
|
209
|
+
another method, like:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
def name
|
213
|
+
person.name
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
This can be represented as the following AST:
|
218
|
+
|
219
|
+
```
|
220
|
+
(def :name
|
221
|
+
(args)
|
222
|
+
(send
|
223
|
+
(send nil :person) :name))
|
224
|
+
```
|
225
|
+
|
226
|
+
Then, let's build a search for methods that calls an attribute with the same
|
227
|
+
name:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
Fast.match?(ast,'(def $_ ... (send (send nil _) \1))') # => [:name]
|
231
|
+
```
|
232
|
+
|
233
|
+
### Fast.search
|
234
|
+
|
235
|
+
Search allows you to go deeply in the AST, collecting nodes that matches with
|
236
|
+
the expression. It also returns captures if they exist.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
Fast.search(code('a = 1'), '(int _)') # => s(:int, 1)
|
240
|
+
```
|
241
|
+
|
242
|
+
If you use captures, it returns the node and the captures respectively:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
Fast.search(code('a = 1'), '(int $_)') # => [s(:int, 1), 1]
|
246
|
+
```
|
247
|
+
|
248
|
+
### Fast.capture
|
249
|
+
|
250
|
+
To pick just the captures and ignore the nodes, use `Fast.capture`:
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
Fast.capture(code('a = 1'), '(int $_)') # => 1
|
254
|
+
```
|
255
|
+
### Fast.replace
|
256
|
+
|
257
|
+
And if I want to refactor a code and use `delegate <attribute>, to: <object>`, try with replace:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
Fast.replace ast,
|
261
|
+
'(def $_ ... (send (send nil $_) \1))',
|
262
|
+
-> (node, captures) {
|
263
|
+
attribute, object = captures
|
264
|
+
replace(
|
265
|
+
node.location.expression,
|
266
|
+
"delegate :#{attribute}, to: :#{object}"
|
267
|
+
)
|
268
|
+
}
|
269
|
+
```
|
270
|
+
|
271
|
+
### Replacing file
|
272
|
+
|
273
|
+
Now let's imagine we have real files like `sample.rb` with the following code:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
def good_bye
|
277
|
+
message = ["good", "bye"]
|
278
|
+
puts message.join(' ')
|
279
|
+
end
|
280
|
+
```
|
281
|
+
|
282
|
+
And we decide to remove the `message` variable and put it inline with the `puts`.
|
283
|
+
|
284
|
+
Basically, we need to find the local variable assignment, store the value in
|
285
|
+
memory. Remove the assignment expression and use the value where the variable
|
286
|
+
is being called.
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
assignment = nil
|
290
|
+
Fast.replace_file('sample.rb', '({ lvasgn lvar } message )',
|
291
|
+
-> (node, _) {
|
292
|
+
if node.type == :lvasgn
|
293
|
+
assignment = node.children.last
|
294
|
+
remove(node.location.expression)
|
295
|
+
elsif node.type == :lvar
|
296
|
+
replace(node.location.expression, assignment.location.expression.source)
|
297
|
+
end
|
298
|
+
}
|
299
|
+
)
|
300
|
+
```
|
301
|
+
|
302
|
+
## Other useful functions
|
303
|
+
|
304
|
+
To manipulate ruby files, some times you'll need some extra tasks.
|
305
|
+
|
306
|
+
### Fast.ast_from_File(file)
|
307
|
+
|
308
|
+
This method parses the code and load into a AST representation.
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
Fast.ast_from_file('sample.rb')
|
312
|
+
```
|
313
|
+
|
314
|
+
### Fast.search_file
|
315
|
+
|
316
|
+
You can use `search_file` and pass the path for search for expressions inside
|
317
|
+
files.
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
Fast.search_file('file.rb', expression)
|
321
|
+
```
|
322
|
+
|
323
|
+
It's simple combination of `Fast.ast_from_file` with `Fast.search`.
|
324
|
+
|
325
|
+
### Fast.ruby_files_from(arguments)
|
326
|
+
|
327
|
+
You'll be probably looking for multiple ruby files, then this method fetches
|
328
|
+
all internal `.rb` files
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
Fast.ruby_files_from(['lib']) # => ["lib/fast.rb"]
|
332
|
+
```
|
333
|
+
|
334
|
+
## `fast` in the command line
|
110
335
|
|
111
336
|
It will also inject a executable named `fast` and you can use it to search and
|
112
|
-
find code
|
337
|
+
find code using the concept:
|
113
338
|
|
114
339
|
```
|
115
|
-
$ fast '(
|
340
|
+
$ fast '(def match?)' lib/fast.rb
|
116
341
|
```
|
117
342
|
|
118
343
|
- Use `-d` or `--debug` for enable debug mode.
|
119
344
|
- Use `--ast` to output the AST instead of the original code
|
120
345
|
|
346
|
+
## Experiments
|
347
|
+
|
348
|
+
You can define experiments and build experimental research to improve some code in
|
349
|
+
an automated way.
|
350
|
+
|
351
|
+
Let's create an experiment to try to remove `before` or `after` blocks
|
352
|
+
and run specs. If the spec pass without need the hook, the hook is useless.
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
|
356
|
+
lookup 'spec'
|
357
|
+
search "(block (send nil {before after}))"
|
358
|
+
edit {|node| remove(node.loc.expression) }
|
359
|
+
policy {|new_file| system("bin/spring rspec --fail-fast #{new_file}") }
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
- In the `lookup` you can pass files or folders.
|
364
|
+
- The `search` contains the expression you want to match
|
365
|
+
- With `edit` block you can apply the code change
|
366
|
+
- And the `policy` is executed to check if the current change is valuable
|
367
|
+
|
368
|
+
If the file contains multiple `before` or `after` blocks, each removal will
|
369
|
+
occur independently and the successfull removals will be combined as a
|
370
|
+
secondary change. The process repeates until find all possible combinations.
|
371
|
+
|
372
|
+
See more examples in [experiments](experiments) folder.
|
373
|
+
|
374
|
+
To run multiple experiments, use `fast-experiment` runner:
|
375
|
+
|
376
|
+
```
|
377
|
+
fast-experiment <experiment-names> <files-or-folders>
|
378
|
+
```
|
379
|
+
|
380
|
+
You can limit experiments or file escope:
|
381
|
+
|
382
|
+
```
|
383
|
+
fast-experiment RSpec/RemoveUselessBeforeAfterHook spec/models/**/*_spec.rb
|
384
|
+
```
|
385
|
+
|
121
386
|
## Development
|
122
387
|
|
123
388
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
124
389
|
|
390
|
+
On the console we have a few functions like `s` and `code` to make it easy ;)
|
391
|
+
|
392
|
+
$ bin/console
|
393
|
+
|
394
|
+
```ruby
|
395
|
+
code("a = 1") # => s(:lvasgn, s(:int, 1))
|
396
|
+
```
|
397
|
+
|
125
398
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
126
399
|
|
127
400
|
## Contributing
|