ffast 0.0.2 → 0.0.3
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 +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
|
+
|