ffast 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|