ffast 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +2 -2
- data/README.md +42 -18
- data/bin/fast +30 -9
- data/docs/command_line.md +112 -0
- data/docs/experiments.md +147 -0
- data/docs/index.md +356 -0
- data/docs/similarity_tutorial.md +174 -0
- data/docs/syntax.md +370 -0
- data/examples/let_it_be_experiment.rb +11 -0
- data/fast.gemspec +2 -2
- data/lib/fast.rb +30 -4
- data/mkdocs.yml +21 -0
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c75d9b66509133b2f2579a03d663a80ece65421f309819f6c04b67c641973816
|
4
|
+
data.tar.gz: d172f92548e5080074d9edc2ec23bbc0a3febd3cfcfe1296d743a0a219ebfb8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12f1c29903c12fe53e0b1a6b9c8c54664eb34411dbb9ae88c813359fe0df342c4a0c4168f13598a357281992ba89f7e5a1bdf8196e4217ce6b451d60d1cef364
|
7
|
+
data.tar.gz: 98e8a7f6b9a374197124b2a0b293fececd46bf396f86d6981de27193b9dc5c92532bc66763a31f446dae4d638967ea601f1348f633bbee6ca383ab5a2d63e54d
|
data/.travis.yml
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
3
|
rvm:
|
4
|
-
- 2.
|
5
|
-
before_install: gem install bundler -v 1.
|
4
|
+
- 2.5.0
|
5
|
+
before_install: gem install bundler -v 1.16.1
|
6
6
|
before_script:
|
7
7
|
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
8
8
|
- chmod +x ./cc-test-reporter
|
data/README.md
CHANGED
@@ -7,6 +7,8 @@ Fast is a "Find AST" tool to help you search in the code abstract syntax tree.
|
|
7
7
|
Ruby allow us to do the same thing in a few ways then it's hard to check
|
8
8
|
how the code is written.
|
9
9
|
|
10
|
+
Check the official documentation: https://jonatas.github.io/fast.
|
11
|
+
|
10
12
|
## Syntax for find in AST
|
11
13
|
|
12
14
|
The current version cover the following elements:
|
@@ -27,18 +29,6 @@ The syntax is inspired on [RuboCop Node Pattern](https://github.com/bbatsov/rubo
|
|
27
29
|
|
28
30
|
## Installation
|
29
31
|
|
30
|
-
Add this line to your application's Gemfile:
|
31
|
-
|
32
|
-
```ruby
|
33
|
-
gem 'ffast'
|
34
|
-
```
|
35
|
-
|
36
|
-
And then execute:
|
37
|
-
|
38
|
-
$ bundle
|
39
|
-
|
40
|
-
Or install it yourself as:
|
41
|
-
|
42
32
|
$ gem install ffast
|
43
33
|
|
44
34
|
## How it works
|
@@ -230,7 +220,7 @@ name:
|
|
230
220
|
Fast.match?(ast,'(def $_ ... (send (send nil _) \1))') # => [:name]
|
231
221
|
```
|
232
222
|
|
233
|
-
|
223
|
+
## Fast.search
|
234
224
|
|
235
225
|
Search allows you to go deeply in the AST, collecting nodes that matches with
|
236
226
|
the expression. It also returns captures if they exist.
|
@@ -245,14 +235,14 @@ If you use captures, it returns the node and the captures respectively:
|
|
245
235
|
Fast.search(code('a = 1'), '(int $_)') # => [s(:int, 1), 1]
|
246
236
|
```
|
247
237
|
|
248
|
-
|
238
|
+
## Fast.capture
|
249
239
|
|
250
240
|
To pick just the captures and ignore the nodes, use `Fast.capture`:
|
251
241
|
|
252
242
|
```ruby
|
253
243
|
Fast.capture(code('a = 1'), '(int $_)') # => 1
|
254
244
|
```
|
255
|
-
|
245
|
+
## Fast.replace
|
256
246
|
|
257
247
|
And if I want to refactor a code and use `delegate <attribute>, to: <object>`, try with replace:
|
258
248
|
|
@@ -303,7 +293,7 @@ Fast.replace_file('sample.rb', '({ lvasgn lvar } message )',
|
|
303
293
|
|
304
294
|
To manipulate ruby files, some times you'll need some extra tasks.
|
305
295
|
|
306
|
-
|
296
|
+
## Fast.ast_from_File(file)
|
307
297
|
|
308
298
|
This method parses the code and load into a AST representation.
|
309
299
|
|
@@ -311,7 +301,7 @@ This method parses the code and load into a AST representation.
|
|
311
301
|
Fast.ast_from_file('sample.rb')
|
312
302
|
```
|
313
303
|
|
314
|
-
|
304
|
+
## Fast.search_file
|
315
305
|
|
316
306
|
You can use `search_file` and pass the path for search for expressions inside
|
317
307
|
files.
|
@@ -322,7 +312,7 @@ Fast.search_file('file.rb', expression)
|
|
322
312
|
|
323
313
|
It's simple combination of `Fast.ast_from_file` with `Fast.search`.
|
324
314
|
|
325
|
-
|
315
|
+
## Fast.ruby_files_from(arguments)
|
326
316
|
|
327
317
|
You'll be probably looking for multiple ruby files, then this method fetches
|
328
318
|
all internal `.rb` files
|
@@ -342,6 +332,38 @@ $ fast '(def match?)' lib/fast.rb
|
|
342
332
|
|
343
333
|
- Use `-d` or `--debug` for enable debug mode.
|
344
334
|
- Use `--ast` to output the AST instead of the original code
|
335
|
+
- Use `--pry` to jump debugging the first result with pry
|
336
|
+
- Use `-c` to search from code example
|
337
|
+
- Use `-s` to search similar code
|
338
|
+
|
339
|
+
```
|
340
|
+
$ fast '(block (send nil it))' spec --pry
|
341
|
+
```
|
342
|
+
And inside pry session, you can use `result` as the first result or `results`
|
343
|
+
to use all occurrences found.
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
results.map{|e|e.children[0].children[2]}
|
347
|
+
# => [s(:str, "parses ... as Find"),
|
348
|
+
# s(:str, "parses $ as Capture"),
|
349
|
+
# s(:str, "parses quoted values as strings"),
|
350
|
+
# s(:str, "parses {} as Any"),
|
351
|
+
# s(:str, "parses [] as All"), ...]
|
352
|
+
```
|
353
|
+
|
354
|
+
Getting all `it` blocks without description:
|
355
|
+
|
356
|
+
$ fast '(block (send nil it (nil)) (args ) (!str)) ) )' spec
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
# spec/fast_spec.rb:166
|
360
|
+
it { expect(described_class).to be_match(s(:int, 1), '(...)') }
|
361
|
+
# spec/fast_spec.rb:167
|
362
|
+
it { expect(described_class).to be_match(s(:int, 1), '(_ _)') }
|
363
|
+
# spec/fast_spec.rb:168
|
364
|
+
it { expect(described_class).to be_match(code['"string"'], '(str "string")') }
|
365
|
+
# ... more results
|
366
|
+
```
|
345
367
|
|
346
368
|
## Experiments
|
347
369
|
|
@@ -406,3 +428,5 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/jonata
|
|
406
428
|
|
407
429
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
408
430
|
|
431
|
+
|
432
|
+
See more on the [official documentation](https://jonatas.github.io/fast).
|
data/bin/fast
CHANGED
@@ -7,37 +7,58 @@ require 'fast'
|
|
7
7
|
require 'coderay'
|
8
8
|
|
9
9
|
arguments = ARGV
|
10
|
-
pattern = arguments.shift
|
11
10
|
show_sexp = arguments.delete('--ast')
|
12
11
|
pry = arguments.delete('--pry')
|
12
|
+
from_code = arguments.delete('--code') || arguments.delete('-c')
|
13
|
+
similar = arguments.delete('--similar') || arguments.delete('-s')
|
13
14
|
debug = arguments.delete('--debug') || arguments.delete('-d')
|
14
|
-
files = Fast.ruby_files_from(*arguments || '.')
|
15
15
|
|
16
|
-
pattern =
|
16
|
+
pattern = arguments.shift
|
17
|
+
|
18
|
+
if similar || from_code
|
19
|
+
ast = Fast.ast(pattern)
|
20
|
+
if similar
|
21
|
+
puts "Looking for code similar to #{pattern}" if debug
|
22
|
+
pattern = Fast.expression_from(ast)
|
23
|
+
elsif from_code
|
24
|
+
pattern = ast.to_sexp
|
25
|
+
puts 'The generated expression from AST was:', pattern if debug
|
26
|
+
end
|
27
|
+
end
|
28
|
+
arguments << '.' if arguments.empty?
|
29
|
+
|
30
|
+
files = Fast.ruby_files_from(*arguments)
|
17
31
|
|
18
|
-
|
32
|
+
expression = Fast.expression(pattern)
|
33
|
+
|
34
|
+
puts "Expression: #{expression.map(&:to_s).join(' ')}" if debug
|
19
35
|
|
20
36
|
files.each do |file|
|
21
37
|
results =
|
22
38
|
if debug
|
23
|
-
Fast.debug { Fast.search_file(
|
39
|
+
Fast.debug { Fast.search_file(expression, file) }
|
24
40
|
else
|
25
41
|
begin
|
26
|
-
Fast.search_file(
|
42
|
+
Fast.search_file(expression, file)
|
27
43
|
rescue Parser::SyntaxError
|
28
|
-
|
44
|
+
if debug
|
45
|
+
puts "Ops! An error occurred trying to search in #{expression.inspect} in #{file}",
|
46
|
+
$ERROR_INFO,
|
47
|
+
$ERROR_POSITION
|
48
|
+
end
|
29
49
|
end
|
30
50
|
end
|
31
51
|
|
32
52
|
next unless results
|
33
53
|
|
34
|
-
results.each do |
|
54
|
+
results.each do |result|
|
35
55
|
next if result.nil? || result == []
|
56
|
+
|
36
57
|
if pry
|
37
58
|
require 'pry'
|
38
59
|
binding.pry # rubocop:disable Lint/Debugger
|
39
60
|
else
|
40
|
-
Fast.report(
|
61
|
+
Fast.report(result, file: file, show_sexp: show_sexp)
|
41
62
|
end
|
42
63
|
end
|
43
64
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# Command line
|
2
|
+
|
3
|
+
It will also inject a executable named `fast` and you can use it to search and
|
4
|
+
find code using the concept:
|
5
|
+
|
6
|
+
```
|
7
|
+
$ fast '(def match?)' lib/fast.rb
|
8
|
+
```
|
9
|
+
|
10
|
+
- Use `-d` or `--debug` for enable debug mode.
|
11
|
+
- Use `--ast` to output the AST instead of the original code
|
12
|
+
- Use `--pry` to jump debugging the first result with pry
|
13
|
+
- Use `-c` to search from code example
|
14
|
+
- Use `-s` to search similar code
|
15
|
+
|
16
|
+
## `--pry`
|
17
|
+
|
18
|
+
$ fast '(block (send nil it))' spec --pry
|
19
|
+
|
20
|
+
And inside pry session, you can use `result` as the first result or `results`
|
21
|
+
to use all occurrences found.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
results.map{|e|e.children[0].children[2]}
|
25
|
+
# => [s(:str, "parses ... as Find"),
|
26
|
+
# s(:str, "parses $ as Capture"),
|
27
|
+
# s(:str, "parses quoted values as strings"),
|
28
|
+
# s(:str, "parses {} as Any"),
|
29
|
+
# s(:str, "parses [] as All"), ...]
|
30
|
+
```
|
31
|
+
|
32
|
+
Getting all `it` blocks without description:
|
33
|
+
|
34
|
+
$ fast '(block (send nil it (nil)) (args ) (!str)) ) )' spec
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
# spec/fast_spec.rb:166
|
38
|
+
it { expect(described_class).to be_match(s(:int, 1), '(...)') }
|
39
|
+
# spec/fast_spec.rb:167
|
40
|
+
it { expect(described_class).to be_match(s(:int, 1), '(_ _)') }
|
41
|
+
# spec/fast_spec.rb:168
|
42
|
+
it { expect(described_class).to be_match(code['"string"'], '(str "string")') }
|
43
|
+
# ... more results
|
44
|
+
```
|
45
|
+
|
46
|
+
## `--debug`
|
47
|
+
|
48
|
+
This option will print all matching details while validating each node.
|
49
|
+
|
50
|
+
```
|
51
|
+
$ echo 'object.method' > sample.rb
|
52
|
+
$ fast -d '(send (send nil _) _)' sample.rb
|
53
|
+
```
|
54
|
+
|
55
|
+
It will bring details of the expression compiled and each node being validated:
|
56
|
+
|
57
|
+
```
|
58
|
+
Expression: f[send] [#<Fast::Find:0x00007f8c53047158 @token="send">, #<Fast::Find:0x00007f8c530470e0 @token="nil">, #<Fast::Find:0x00007f8c53047090 @token="_">] f[_]
|
59
|
+
send == (send
|
60
|
+
(send nil :object) :method) # => true
|
61
|
+
f[send] == (send
|
62
|
+
(send nil :object) :method) # => true
|
63
|
+
send == (send nil :object) # => true
|
64
|
+
f[send] == (send nil :object) # => true
|
65
|
+
== # => true
|
66
|
+
f[nil] == # => true
|
67
|
+
#<Proc:0x00007f8c53057af8@/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/ffast-0.0.2/lib/fast.rb:25 (lambda)> == object # => true
|
68
|
+
f[_] == object # => true
|
69
|
+
[#<Fast::Find:0x00007f8c53047158 @token="send">, #<Fast::Find:0x00007f8c530470e0 @token="nil">, #<Fast::Find:0x00007f8c53047090 @token="_">] == (send nil :object) # => true
|
70
|
+
#<Proc:0x00007f8c53057af8@/Users/jonatasdp/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/ffast-0.0.2/lib/fast.rb:25 (lambda)> == method # => true
|
71
|
+
f[_] == method # => true
|
72
|
+
# sample.rb:1
|
73
|
+
object.method
|
74
|
+
```
|
75
|
+
|
76
|
+
## `-s` for similarity
|
77
|
+
|
78
|
+
Sometimes you want to search for some similar code like `(send (send (send nil _) _) _)` and we could simply say `a.b.c`.
|
79
|
+
|
80
|
+
The option `-s` build an expression from the code ignoring final values.
|
81
|
+
|
82
|
+
$ echo 'object.method' > sample.rb
|
83
|
+
$ fast -s 'a.b' sample.rb
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# sample.rb:1
|
87
|
+
object.method
|
88
|
+
```
|
89
|
+
|
90
|
+
See also [Code Similarity](simi)ilarity_tutorial.md) tutorial.
|
91
|
+
|
92
|
+
# `-c` to search from code example
|
93
|
+
|
94
|
+
You can search for the exact expression with `-c`
|
95
|
+
|
96
|
+
$ fast -c 'object.method' sample.rb
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# sample.rb:1
|
100
|
+
object.method
|
101
|
+
```
|
102
|
+
|
103
|
+
Combining with `-d`, in the header you can see the generated expression.
|
104
|
+
|
105
|
+
```
|
106
|
+
$ fast -d -c 'object.method' sample.rb | head -n 3
|
107
|
+
|
108
|
+
The generated expression from AST was:
|
109
|
+
(send
|
110
|
+
(send nil :object) :method)
|
111
|
+
```
|
112
|
+
|
data/docs/experiments.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# Experiments
|
2
|
+
|
3
|
+
Experiments allow us to play with AST and do some code transformation, execute
|
4
|
+
some code and continue combining successful transformations.
|
5
|
+
|
6
|
+
The major idea is try a new approach without any promise and if it works
|
7
|
+
continue transforming the code.
|
8
|
+
|
9
|
+
## Replace `FactoryBot#create` with `build_stubbed`.
|
10
|
+
|
11
|
+
Let's look into the following spec example:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
describe "my spec" do
|
15
|
+
let(:user) { create(:user) }
|
16
|
+
let(:address) { create(:address) }
|
17
|
+
# ...
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
Let's say we're amazed with `FactoryBot#build_stubbed` and want to build a small
|
22
|
+
bot to make the changes in a entire code base. Skip some database
|
23
|
+
touches while testing huge test suites are always a good idea.
|
24
|
+
|
25
|
+
First we can hunt for the cases we want to find:
|
26
|
+
|
27
|
+
```
|
28
|
+
$ ruby-parse -e "create(:user)"
|
29
|
+
(send nil :create
|
30
|
+
(sym :user))
|
31
|
+
```
|
32
|
+
|
33
|
+
Using `fast` in the command line to see real examples in the `spec` folder:
|
34
|
+
|
35
|
+
```
|
36
|
+
$ fast "(send nil create)" spec
|
37
|
+
```
|
38
|
+
|
39
|
+
If you don't have a real project but want to test, just create a sample ruby
|
40
|
+
file with the code example above.
|
41
|
+
|
42
|
+
Running it in a big codebase will probably find a few examples of blocks.
|
43
|
+
|
44
|
+
The next step is build a replacement of each independent occurrence to use
|
45
|
+
`build_stubbed` instead of create and combine the successful ones, run again and
|
46
|
+
combine again, until try all kind of successful replacements combined.
|
47
|
+
|
48
|
+
Considering we have the following code in `sample_spec.rb`:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
describe "my spec" do
|
52
|
+
let(:user) { create(:user) }
|
53
|
+
let(:address) { create(:address) }
|
54
|
+
# ...
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
Let's create the experiment that will contain the nodes that are target to be
|
59
|
+
executed and what we want to do when we find the node.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
experiment = Fast.experiment('RSpec/ReplaceCreateWithBuildStubbed') do
|
63
|
+
search '(send nil create)'
|
64
|
+
edit { |node| replace(node.loc.selector, 'build_stubbed') }
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
If we use `Fast.replace_file` it will replace all occurrences in the same run
|
69
|
+
and that's one of the motivations behind create the `ExperimentFile` class.
|
70
|
+
|
71
|
+
Executing a partial replacement of the first occurrence:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
experiment_file = Fast::ExperimentFile.new('sample_spec.rb', experiment) }
|
75
|
+
puts experiment_file.partial_replace(1)
|
76
|
+
```
|
77
|
+
|
78
|
+
The command will output the following code:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
describe "my spec" do
|
82
|
+
let(:user) { build_stubbed(:user) }
|
83
|
+
let(:address) { create(:address) }
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
## Remove useless before block
|
89
|
+
|
90
|
+
Imagine the following code sample:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
describe "my spec" do
|
94
|
+
before { create(:user) }
|
95
|
+
# ...
|
96
|
+
after { User.delete_all }
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
And now, we can define an experiment that removes the entire code block and run
|
101
|
+
the experimental specs.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
experiment = Fast.experiment('RSpec/RemoveUselessBeforeAfterHook') do
|
105
|
+
lookup 'spec'
|
106
|
+
search '(block (send nil {before after}))'
|
107
|
+
edit { |node| remove(node.loc.expression) }
|
108
|
+
policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
To run the experiment you can simply say:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
experiment.run
|
116
|
+
```
|
117
|
+
|
118
|
+
Or drop the code into `experiments` folder and use the `fast-experiment` command
|
119
|
+
line tool.
|
120
|
+
|
121
|
+
$ fast-experiment RSpec/RemoveUselessBeforeAfterHook spec
|
122
|
+
|
123
|
+
## DSL
|
124
|
+
|
125
|
+
- In the `lookup` you can pass files or folders.
|
126
|
+
- The `search` contains the expression you want to match
|
127
|
+
- With `edit` block you can apply the code change
|
128
|
+
- And the `policy` is executed to check if the current change is valuable
|
129
|
+
|
130
|
+
If the file contains multiple `before` or `after` blocks, each removal will
|
131
|
+
occur independently and the successfull removals will be combined as a
|
132
|
+
secondary change. The process repeates until find all possible combinations.
|
133
|
+
|
134
|
+
See more examples in [experiments](experiments) folder.
|
135
|
+
|
136
|
+
To run multiple experiments, use `fast-experiment` runner:
|
137
|
+
|
138
|
+
```
|
139
|
+
fast-experiment <experiment-names> <files-or-folders>
|
140
|
+
```
|
141
|
+
|
142
|
+
You can limit experiments or file escope:
|
143
|
+
|
144
|
+
```
|
145
|
+
fast-experiment RSpec/RemoveUselessBeforeAfterHook spec/models/**/*_spec.rb
|
146
|
+
```
|
147
|
+
|