ffast 0.1.5 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +114 -2
- data/.sourcelevel.yml +2 -0
- data/.travis.yml +2 -2
- data/Fastfile +3 -0
- data/README.md +10 -5
- data/docs/command_line.md +1 -0
- data/docs/editors-integration.md +46 -0
- data/docs/ideas.md +80 -0
- data/docs/index.md +5 -0
- data/docs/research.md +93 -0
- data/docs/shortcuts.md +323 -0
- data/docs/videos.md +16 -0
- data/fast.gemspec +6 -4
- data/lib/fast.rb +135 -40
- data/lib/fast/cli.rb +61 -23
- data/lib/fast/experiment.rb +1 -2
- data/lib/fast/git.rb +101 -0
- data/lib/fast/shortcut.rb +6 -9
- data/lib/fast/version.rb +1 -1
- data/mkdocs.yml +5 -0
- metadata +54 -13
data/docs/shortcuts.md
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
# Shortcuts
|
2
|
+
|
3
|
+
Shortcuts are defined on a `Fastfile` inside any ruby project.
|
4
|
+
|
5
|
+
!!!info "Use `~/Fastfile`"
|
6
|
+
You can also add one extra in your `$HOME` if you want to have something loaded always.
|
7
|
+
|
8
|
+
By default, the command line interface does not load any `Fastfile` if the
|
9
|
+
first param is not a shortcut. It should start with `.`.
|
10
|
+
|
11
|
+
I'm building several researches and I'll make the examples open here to show
|
12
|
+
several interesting cases in action.
|
13
|
+
|
14
|
+
## List your fast shortcuts
|
15
|
+
|
16
|
+
As the interface is very rudimentar, let's build a shortcut to print what
|
17
|
+
shortcuts are available. This is a good one to your `$HOME/Fastfile`:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# List all shortcut with comments
|
21
|
+
Fast.shortcut :shortcuts do
|
22
|
+
fast_files.each do |file|
|
23
|
+
lines = File.readlines(file).map{|line|line.chomp.gsub(/\s*#/,'').strip}
|
24
|
+
result = capture_file('(send ... shortcut $(sym _', file)
|
25
|
+
result = [result] unless result.is_a?Array
|
26
|
+
result.each do |capture|
|
27
|
+
target = capture.loc.expression
|
28
|
+
puts "fast .#{target.source[1..-1].ljust(30)} # #{lines[target.line-2]}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
And using it on `fast` project that loads both `~/Fastfile` and the Fastfile from the project:
|
35
|
+
|
36
|
+
```
|
37
|
+
fast .version # Let's say you'd like to show the version that is over the version file
|
38
|
+
fast .parser # Simple shortcut that I used often to show how the expression parser works
|
39
|
+
fast .bump_version # Use `fast .bump_version` to rewrite the version file
|
40
|
+
fast .shortcuts # List all shortcut with comments
|
41
|
+
```
|
42
|
+
|
43
|
+
## Search for references
|
44
|
+
|
45
|
+
I always miss bringing something simple as `grep keyword` where I can leave a simple string and it can
|
46
|
+
search in all types of nodes and report interesting things about it.
|
47
|
+
|
48
|
+
Let's consider a very flexible search that can target any code related to some
|
49
|
+
keyword. Considering that we're talking about code indentifiers:
|
50
|
+
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# Search all references about some keyword or regular expression
|
54
|
+
Fast.shortcut(:ref) do
|
55
|
+
require 'fast/cli'
|
56
|
+
Kernel.class_eval do
|
57
|
+
def matches_args? identifier
|
58
|
+
search = ARGV.last
|
59
|
+
regex = Regexp.new(search, Regexp::IGNORECASE)
|
60
|
+
case identifier
|
61
|
+
when Symbol, String
|
62
|
+
regex.match?(identifier) || identifier.to_s.include?(search)
|
63
|
+
when Astrolabe::Node
|
64
|
+
regex.match?(identifier.to_sexp)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
pattern = <<~FAST
|
69
|
+
{
|
70
|
+
({class def sym str} #matches_args?)'
|
71
|
+
({const send} nil #matches_args?)'
|
72
|
+
}
|
73
|
+
FAST
|
74
|
+
Fast::Cli.run!([pattern, '.', '--parallel'])
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
## Rails: Show validations from models
|
79
|
+
|
80
|
+
If the shortcut does not define a block, it works as a holder for arguments from
|
81
|
+
the command line.
|
82
|
+
|
83
|
+
Let's say you always use `fast "(send nil {validate validates})" app/models` to
|
84
|
+
check validations in the models. You can define a shortcut to hold the args and
|
85
|
+
avoid retyping long lines:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# Show validations from app/models
|
89
|
+
Fast.shortcut(:validations, "(send nil {validate validates})", "app/models")
|
90
|
+
```
|
91
|
+
And you can reuse the search with the shortcut starting with a `.`:
|
92
|
+
|
93
|
+
```
|
94
|
+
fast .validations
|
95
|
+
```
|
96
|
+
And it will also accept params if you want to filter a specific file:
|
97
|
+
|
98
|
+
```
|
99
|
+
fast .validations app/models/user.rb
|
100
|
+
```
|
101
|
+
|
102
|
+
!!! info "Note that you can also use flags in the command line shortcuts"
|
103
|
+
|
104
|
+
Let's say you also want to use `fast --headless` you can add it to the params:
|
105
|
+
|
106
|
+
> Fast.shortcut(:validations, "(send nil {validate validates})", "app/models", "--headless")
|
107
|
+
|
108
|
+
## Automated Refactor: Bump version
|
109
|
+
|
110
|
+
Let's start with a [real usage](https://github.com/jonatas/fast/blob/master/Fastfile#L20-L34)
|
111
|
+
to bump a new version of the gem.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
Fast.shortcut :bump_version do
|
115
|
+
rewrite_file('(casgn nil VERSION (str _)', 'lib/fast/version.rb') do |node|
|
116
|
+
target = node.children.last.loc.expression
|
117
|
+
pieces = target.source.split('.').map(&:to_i)
|
118
|
+
pieces.reverse.each_with_index do |fragment, i|
|
119
|
+
if fragment < 9
|
120
|
+
pieces[-(i + 1)] = fragment + 1
|
121
|
+
break
|
122
|
+
else
|
123
|
+
pieces[-(i + 1)] = 0
|
124
|
+
end
|
125
|
+
end
|
126
|
+
replace(target, "'#{pieces.join('.')}'")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
And then the change is done in the `lib/fast/version.rb`:
|
132
|
+
|
133
|
+
```diff
|
134
|
+
module Fast
|
135
|
+
- VERSION = '0.1.6'
|
136
|
+
+ VERSION = '0.1.7'
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
## RSpec: Find unused shared contexts
|
141
|
+
|
142
|
+
If you build shared contexts often, probably you can forget some left overs.
|
143
|
+
|
144
|
+
The objective of the shortcut is find leftovers from shared contexts.
|
145
|
+
|
146
|
+
First, the objective is capture all names of the `RSpec.shared_context` or
|
147
|
+
`shared_context` declared in the `spec/support` folder.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
Fast.capture_all('(block (send {nil,_} shared_context (str $_)))', Fast.ruby_files_from('spec/support'))
|
151
|
+
```
|
152
|
+
|
153
|
+
Then, we need to check all the specs and search for `include_context` usages to
|
154
|
+
confirm if all defined contexts are being used:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
specs = Fast.ruby_files_from('spec').select{|f|f !~ %r{spec/support/}}
|
158
|
+
Fast.search_all("(send nil include_context (str #register_usage)", specs)
|
159
|
+
```
|
160
|
+
|
161
|
+
Note that we created a new reference to `#register_usage` and we need to define the method too:
|
162
|
+
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
@used = []
|
166
|
+
def register_usage context_name
|
167
|
+
@used << context_name
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
Wrapping up everything in a shortcut:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
# Show unused shared contexts
|
175
|
+
Fast.shortcut(:unused_shared_contexts) do
|
176
|
+
puts "Checking shared contexts"
|
177
|
+
Kernel.class_eval do
|
178
|
+
@used = []
|
179
|
+
def register_usage context_name
|
180
|
+
@used << context_name
|
181
|
+
end
|
182
|
+
def show_report! defined_contexts
|
183
|
+
unused = defined_contexts.values.flatten - @used
|
184
|
+
if unused.any?
|
185
|
+
puts "Unused shared contexts", unused
|
186
|
+
else
|
187
|
+
puts "Good job! all the #{defined_contexts.size} contexts are used!"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
specs = ruby_files_from('spec/').select{|f|f !~ %r{spec/support/}}
|
192
|
+
search_all("(send nil include_context (str #register_usage)", specs)
|
193
|
+
defined_contexts = capture_all('(block (send {nil,_} shared_context (str $_)))', ruby_files_from('spec'))
|
194
|
+
Kernel.public_send(:show_report!, defined_contexts)
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
!!! faq "Why `#register_usage` is defined on the `Kernel`?"
|
199
|
+
Yes! note that the `#register_usage` was forced to be inside `Kernel`
|
200
|
+
because of the `shortcut` block that takes the `Fast` context to be easy
|
201
|
+
to access in the default functions. As I can define multiple shortcuts
|
202
|
+
I don't want to polute my Kernel module with other methods that are not useful.
|
203
|
+
|
204
|
+
|
205
|
+
## RSpec: Remove unused let
|
206
|
+
|
207
|
+
!!! hint "First shortcut with experiments"
|
208
|
+
If you're not familiar with automated experiments, you can read about it [here](/experiments).
|
209
|
+
|
210
|
+
The current scenario is similar in terms of search with the previous one, but more advanced
|
211
|
+
because we're going to introduce automated refactoring.
|
212
|
+
|
213
|
+
The idea is simple, if it finds a `let` in a RSpec scenario that is not referenced, it tries to experimentally remove the `let` and run the tests:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
# Experimental remove `let` that are not referenced in the spec
|
217
|
+
Fast.shortcut(:exp_remove_let) do
|
218
|
+
require 'fast/experiment'
|
219
|
+
Kernel.class_eval do
|
220
|
+
file = ARGV.last
|
221
|
+
|
222
|
+
defined_lets = Fast.capture_file('(block (send nil let (sym $_)))', file).uniq
|
223
|
+
@unreferenced= defined_lets.select do |identifier|
|
224
|
+
Fast.search_file("(send nil #{identifier})", file).empty?
|
225
|
+
end
|
226
|
+
|
227
|
+
def unreferenced_let?(identifier)
|
228
|
+
@unreferenced.include? identifier
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
experiment('RSpec/RemoveUnreferencedLet') do
|
233
|
+
lookup ARGV.last
|
234
|
+
search '(block (send nil let (sym #unreferenced_let?)))'
|
235
|
+
edit { |node| remove(node.loc.expression) }
|
236
|
+
policy { |new_file| system("bundle exec rspec --fail-fast #{new_file}") }
|
237
|
+
end.run
|
238
|
+
end
|
239
|
+
```
|
240
|
+
|
241
|
+
And it will run with a single file from command line:
|
242
|
+
|
243
|
+
```
|
244
|
+
fast .exp_remove_let spec/my_file_spec.rb
|
245
|
+
```
|
246
|
+
|
247
|
+
## FactoryBot: Replace `create` with `build_stubbed`
|
248
|
+
|
249
|
+
For performance reasons, if we can avoid touching the database the test will
|
250
|
+
always be faster.
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
# Experimental switch from `create` to `build_stubbed`
|
254
|
+
Fast.shortcut(:exp_build_stubbed) do
|
255
|
+
require 'fast/experiment'
|
256
|
+
Fast.experiment('FactoryBot/UseBuildStubbed') do
|
257
|
+
lookup ARGV.last
|
258
|
+
search '(send nil create)'
|
259
|
+
edit { |node| replace(node.loc.selector, 'build_stubbed') }
|
260
|
+
policy { |new_file| system("bundle exec rspec --fail-fast #{new_file}") }
|
261
|
+
end.run
|
262
|
+
end
|
263
|
+
```
|
264
|
+
## RSpec: Use `let_it_be` instead of `let`
|
265
|
+
|
266
|
+
The `let_it_be` is a simple helper from
|
267
|
+
[TestProf](https://test-prof.evilmartians.io/#/let_it_be) gem that can speed up
|
268
|
+
the specs by caching some factories using like a `before_all` approach.
|
269
|
+
|
270
|
+
This experiment hunts for `let(...) { create(...) }` and switch the `let` to
|
271
|
+
`let_it_be`:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
# Experimental replace `let(_)` with `let_it_be` case it calls `create` inside the block
|
275
|
+
Fast.shortcut(:exp_let_it_be) do
|
276
|
+
require 'fast/experiment'
|
277
|
+
Fast.experiment('FactoryBot/LetItBe') do
|
278
|
+
lookup ARGV.last
|
279
|
+
search '(block (send nil let (sym _)) (args) (send nil create))'
|
280
|
+
edit { |node| replace(node.children.first.loc.selector, 'let_it_be') }
|
281
|
+
policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
|
282
|
+
end.run
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
## RSpec: Remove `before` or `after` blocks
|
287
|
+
|
288
|
+
From time to time, we forget some left overs like `before` or `after` blocks
|
289
|
+
that even removing from the code, the tests still passes. This experiment
|
290
|
+
removes the before/after blocks and check if the test passes.
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
# Experimental remove `before` or `after` blocks.
|
294
|
+
Fast.shortcut(:exp_remove_before_after) do
|
295
|
+
require 'fast/experiment'
|
296
|
+
Fast.experiment('RSpec/RemoveBeforeAfter') do
|
297
|
+
lookup ARGV.last
|
298
|
+
search '(block (send nil {before after}))'
|
299
|
+
edit { |node| remove(node.loc.expression) }
|
300
|
+
policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
|
301
|
+
end.run
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
## RSpec: Show message chains
|
306
|
+
|
307
|
+
I often forget the syntax and need to search for message chains on specs, so I
|
308
|
+
created an shortcut for it.
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
# Show RSpec message chains
|
312
|
+
Fast.shortcut(:message_chains, '^^(send nil receive_message_chain)', 'spec')
|
313
|
+
```
|
314
|
+
|
315
|
+
## RSpec: Show nested assertions
|
316
|
+
|
317
|
+
I love to use nested assertions and I often need examples to refer to them:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
# Show RSpec nested assertions with .and
|
321
|
+
Fast.shortcut(:nested_assertions, '^^(send ... and)', 'spec')
|
322
|
+
```
|
323
|
+
|
data/docs/videos.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Videos
|
2
|
+
|
3
|
+
- [Ruby Kaigi TakeOut 2020: Grepping Ruby code like a boss](https://www.youtube.com/watch?v=YzcYXB4L2so&feature=youtu.be&t=11855)
|
4
|
+
|
5
|
+
<iframe width="560" height="315" src="https://www.youtube.com/embed/YzcYXB4L2so?start=11855" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
6
|
+
|
7
|
+
Also, similar livecoding session at [RubyConf Brazil 2019 (Portuguese)](https://www.eventials.com/locaweb/jonatas-paganini-live-coding-grepping-ruby-code-like-a-boss/#_=_).
|
8
|
+
|
9
|
+
- Introduction to [inline code](https://www.youtube.com/watch?v=KQXglNLUv7o).
|
10
|
+
<iframe width="560" height="315" src="https://www.youtube.com/embed/KQXglNLUv7o" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
11
|
+
|
12
|
+
- [Making local variables inline](https://www.youtube.com/watch?v=JD44nhegCRs)
|
13
|
+
<iframe width="560" height="315" src="https://www.youtube.com/embed/YN0s9kV1A2A" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
14
|
+
|
15
|
+
- [Making methods inline](https://www.youtube.com/watch?v=JD44nhegCRs)
|
16
|
+
<iframe width="560" height="315" src="https://www.youtube.com/embed/YN0s9kV1A2A" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
data/fast.gemspec
CHANGED
@@ -7,7 +7,7 @@ require 'fast/version'
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = 'ffast'
|
9
9
|
spec.version = Fast::VERSION
|
10
|
-
spec.required_ruby_version = '>= 2.
|
10
|
+
spec.required_ruby_version = '>= 2.6'
|
11
11
|
spec.authors = ['Jônatas Davi Paganini']
|
12
12
|
spec.email = ['jonatasdp@gmail.com']
|
13
13
|
spec.homepage = 'https://jonatas.github.io/fast/'
|
@@ -26,18 +26,20 @@ Gem::Specification.new do |spec|
|
|
26
26
|
|
27
27
|
spec.add_dependency 'astrolabe'
|
28
28
|
spec.add_dependency 'coderay'
|
29
|
+
spec.add_dependency 'parallel'
|
29
30
|
spec.add_dependency 'parser'
|
30
|
-
spec.add_dependency 'pry'
|
31
31
|
|
32
32
|
spec.add_development_dependency 'bundler'
|
33
33
|
spec.add_development_dependency 'guard'
|
34
34
|
spec.add_development_dependency 'guard-livereload'
|
35
35
|
spec.add_development_dependency 'guard-rspec'
|
36
|
-
spec.add_development_dependency '
|
36
|
+
spec.add_development_dependency 'pry'
|
37
|
+
spec.add_development_dependency 'git'
|
38
|
+
spec.add_development_dependency 'rake'
|
37
39
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
38
40
|
spec.add_development_dependency 'rspec-its', '~> 1.2'
|
39
41
|
spec.add_development_dependency 'rubocop'
|
40
42
|
spec.add_development_dependency 'rubocop-performance'
|
41
43
|
spec.add_development_dependency 'rubocop-rspec'
|
42
|
-
spec.add_development_dependency 'simplecov'
|
44
|
+
spec.add_development_dependency 'simplecov', '~> 0.10', '< 0.18'
|
43
45
|
end
|
data/lib/fast.rb
CHANGED
@@ -50,7 +50,7 @@ module Fast
|
|
50
50
|
|
|
51
51
|
\? # maybe expression
|
52
52
|
|
|
53
|
-
[\d\w_]+[
|
53
|
+
[\d\w_]+[=\\!\?]? # method names or numbers
|
54
54
|
|
|
55
55
|
\(|\) # parens `(` and `)` for tuples
|
56
56
|
|
|
@@ -67,18 +67,86 @@ module Fast
|
|
67
67
|
%\d # bind extra arguments to the expression
|
68
68
|
/x.freeze
|
69
69
|
|
70
|
+
# Set some convention methods from file.
|
71
|
+
class Node < Astrolabe::Node
|
72
|
+
# @return [String] with path of the file or simply buffer name.
|
73
|
+
def buffer_name
|
74
|
+
expression.source_buffer.name
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Parser::Source::Range] from the expression
|
78
|
+
def expression
|
79
|
+
location.expression
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [String] with the content of the #expression
|
83
|
+
def source
|
84
|
+
expression.source
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Boolean] true if a file exists with the #buffer_name
|
88
|
+
def from_file?
|
89
|
+
File.exist?(buffer_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Array<String>] with authors from the current expression range
|
93
|
+
def blame_authors
|
94
|
+
`git blame -L #{expression.first_line},#{expression.last_line} #{buffer_name}`.lines.map do |line|
|
95
|
+
line.split('(')[1].split(/\d+/).first.strip
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String] with the first element from #blame_authors
|
100
|
+
def author
|
101
|
+
blame_authors.first
|
102
|
+
end
|
103
|
+
|
104
|
+
# Search recursively into a node and its children using a pattern.
|
105
|
+
# @param [String] pattern
|
106
|
+
# @param [Array] *args extra arguments to interpolate in the pattern.
|
107
|
+
# @return [Array<Fast::Node>>] with files and results
|
108
|
+
def search(pattern, *args)
|
109
|
+
Fast.search(pattern, self, *args)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Captures elements from search recursively
|
113
|
+
# @param [String] pattern
|
114
|
+
# @param [Array] *args extra arguments to interpolate in the pattern.
|
115
|
+
# @return [Array<Fast::Node>>] with files and results
|
116
|
+
def capture(pattern, *args)
|
117
|
+
Fast.capture(pattern, self, *args)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Custom builder allow us to set a buffer name for each Node
|
122
|
+
class Builder < Astrolabe::Builder
|
123
|
+
attr_writer :buffer_name
|
124
|
+
# Generates {Node} from the given information.
|
125
|
+
#
|
126
|
+
# @return [Node] the generated node
|
127
|
+
def n(type, children, source_map)
|
128
|
+
Node.new(type, children, location: source_map, buffer_name: @buffer_name)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
70
132
|
class << self
|
71
|
-
# @return [
|
133
|
+
# @return [Fast::Node] from the parsed content
|
72
134
|
# @example
|
73
135
|
# Fast.ast("1") # => s(:int, 1)
|
74
136
|
# Fast.ast("a.b") # => s(:send, s(:send, nil, :a), :b)
|
75
137
|
def ast(content, buffer_name: '(string)')
|
76
138
|
buffer = Parser::Source::Buffer.new(buffer_name)
|
77
139
|
buffer.source = content
|
78
|
-
Parser::CurrentRuby.new(
|
140
|
+
Parser::CurrentRuby.new(builder_for(buffer_name)).parse(buffer)
|
79
141
|
end
|
80
142
|
|
81
|
-
|
143
|
+
def builder_for(buffer_name)
|
144
|
+
builder = Builder.new
|
145
|
+
builder.buffer_name = buffer_name
|
146
|
+
builder
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Fast::Node] parsed from file content
|
82
150
|
# caches the content based on the filename.
|
83
151
|
# @example
|
84
152
|
# Fast.ast_from_file("example.rb") # => s(...)
|
@@ -96,73 +164,99 @@ module Fast
|
|
96
164
|
end
|
97
165
|
|
98
166
|
# Search with pattern directly on file
|
99
|
-
# @return [Array<
|
167
|
+
# @return [Array<Fast::Node>] that matches the pattern
|
100
168
|
def search_file(pattern, file)
|
101
169
|
node = ast_from_file(file)
|
102
|
-
return []
|
170
|
+
return [] unless node
|
171
|
+
|
103
172
|
search pattern, node
|
104
173
|
end
|
105
174
|
|
106
175
|
# Search with pattern on a directory or multiple files
|
107
176
|
# @param [String] pattern
|
108
177
|
# @param [Array<String>] *locations where to search. Default is '.'
|
109
|
-
# @return [Hash<String,Array<
|
110
|
-
def search_all(pattern, locations = ['.'])
|
111
|
-
|
112
|
-
|
178
|
+
# @return [Hash<String,Array<Fast::Node>>] with files and results
|
179
|
+
def search_all(pattern, locations = ['.'], parallel: true, on_result: nil)
|
180
|
+
group_results(build_grouped_search(:search_file, pattern, on_result),
|
181
|
+
locations, parallel: parallel)
|
113
182
|
end
|
114
183
|
|
115
184
|
# Capture with pattern on a directory or multiple files
|
116
185
|
# @param [String] pattern
|
117
186
|
# @param [Array<String>] locations where to search. Default is '.'
|
118
187
|
# @return [Hash<String,Object>] with files and captures
|
119
|
-
def capture_all(pattern, locations = ['.'])
|
120
|
-
|
121
|
-
|
188
|
+
def capture_all(pattern, locations = ['.'], parallel: true, on_result: nil)
|
189
|
+
group_results(build_grouped_search(:capture_file, pattern, on_result),
|
190
|
+
locations, parallel: parallel)
|
191
|
+
end
|
192
|
+
|
193
|
+
# @return [Proc] binding `pattern` argument from a given `method_name`.
|
194
|
+
# @param [Symbol] method_name with `:capture_file` or `:search_file`
|
195
|
+
# @param [String] pattern to match in a search to any file
|
196
|
+
# @param [Proc] on_result is a callback that can be notified soon it matches
|
197
|
+
def build_grouped_search(method_name, pattern, on_result)
|
198
|
+
search_pattern = method(method_name).curry.call(pattern)
|
199
|
+
proc do |file|
|
200
|
+
results = search_pattern.call(file)
|
201
|
+
next if results.nil? || results.empty?
|
202
|
+
|
203
|
+
on_result&.(file, results)
|
204
|
+
{ file => results }
|
205
|
+
end
|
122
206
|
end
|
123
207
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
208
|
+
# Compact grouped results by file allowing parallel processing.
|
209
|
+
# @param [Proc] group_files allows to define a search that can be executed
|
210
|
+
# parallel or not.
|
211
|
+
# @param [Proc] on_result allows to define a callback for fast feedback
|
212
|
+
# while it process several locations in parallel.
|
213
|
+
# @param [Boolean] parallel runs the `group_files` in parallel
|
214
|
+
# @return [Hash[String, Array]] with files and results
|
215
|
+
def group_results(group_files, locations, parallel: true)
|
216
|
+
files = ruby_files_from(*locations)
|
217
|
+
if parallel
|
218
|
+
require 'parallel' unless defined?(Parallel)
|
219
|
+
Parallel.map(files, &group_files)
|
220
|
+
else
|
221
|
+
files.map(&group_files)
|
222
|
+
end.compact.inject(&:merge!)
|
129
223
|
end
|
130
224
|
|
131
225
|
# Capture elements from searches in files. Keep in mind you need to use `$`
|
132
226
|
# in the pattern to make it work.
|
133
227
|
# @return [Array<Object>] captured from the pattern matched in the file
|
134
228
|
def capture_file(pattern, file)
|
135
|
-
|
229
|
+
node = ast_from_file(file)
|
230
|
+
return [] unless node
|
231
|
+
|
232
|
+
capture pattern, node
|
136
233
|
end
|
137
234
|
|
138
235
|
# Search recursively into a node and its children.
|
139
236
|
# If the node matches with the pattern it returns the node,
|
140
237
|
# otherwise it recursively collect possible children nodes
|
141
238
|
# @yield node and capture if block given
|
142
|
-
def search(pattern, node)
|
143
|
-
if (match = match?(pattern, node))
|
239
|
+
def search(pattern, node, *args)
|
240
|
+
if (match = match?(pattern, node, *args))
|
144
241
|
yield node, match if block_given?
|
145
242
|
match != true ? [node, match] : [node]
|
146
243
|
else
|
147
244
|
node.each_child_node
|
148
|
-
.flat_map { |child| search(pattern, child) }
|
245
|
+
.flat_map { |child| search(pattern, child, *args) }
|
149
246
|
.compact.flatten
|
150
247
|
end
|
151
248
|
end
|
152
249
|
|
153
|
-
#
|
250
|
+
# Only captures from a search
|
154
251
|
# @return [Array<Object>] with all captured elements.
|
155
|
-
# @return [Object] with single element when single capture.
|
156
252
|
def capture(pattern, node)
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
res = [res] unless res.is_a?(Array)
|
165
|
-
res.one? ? res.first : res
|
253
|
+
if (match = match?(pattern, node))
|
254
|
+
match == true ? node : match
|
255
|
+
else
|
256
|
+
node.each_child_node
|
257
|
+
.flat_map { |child| capture(pattern, child) }
|
258
|
+
.compact.flatten
|
259
|
+
end
|
166
260
|
end
|
167
261
|
|
168
262
|
def expression(string)
|
@@ -200,21 +294,22 @@ module Fast
|
|
200
294
|
# @param files can be file paths or directories.
|
201
295
|
# When the argument is a folder, it recursively fetches all `.rb` files from it.
|
202
296
|
def ruby_files_from(*files)
|
203
|
-
|
297
|
+
dir_filter = File.method(:directory?)
|
298
|
+
directories = files.select(&dir_filter)
|
204
299
|
|
205
300
|
if directories.any?
|
206
301
|
files -= directories
|
207
302
|
files |= directories.flat_map { |dir| Dir["#{dir}/**/*.rb"] }
|
208
303
|
files.uniq!
|
209
304
|
end
|
210
|
-
files
|
305
|
+
files.reject(&dir_filter)
|
211
306
|
end
|
212
307
|
|
213
308
|
# Extracts a node pattern expression from a given node supressing identifiers and primitive types.
|
214
309
|
# Useful to index abstract patterns or similar code structure.
|
215
310
|
# @see https://jonatas.github.io/fast/similarity_tutorial/
|
216
311
|
# @return [String] with an pattern to search from it.
|
217
|
-
# @param node [
|
312
|
+
# @param node [Fast::Node]
|
218
313
|
# @example
|
219
314
|
# Fast.expression_from(Fast.ast('1')) # => '(int _)'
|
220
315
|
# Fast.expression_from(Fast.ast('a = 1')) # => '(lvasgn _ (int _))'
|
@@ -223,7 +318,7 @@ module Fast
|
|
223
318
|
case node
|
224
319
|
when Parser::AST::Node
|
225
320
|
children_expression = node.children.map(&method(:expression_from)).join(' ')
|
226
|
-
"(#{node.type}#{
|
321
|
+
"(#{node.type}#{" #{children_expression}" if node.children.any?})"
|
227
322
|
when nil, 'nil'
|
228
323
|
'nil'
|
229
324
|
when Symbol, String, Numeric
|
@@ -278,14 +373,14 @@ module Fast
|
|
278
373
|
when '{' then Any.new(parse_until_peek('}'))
|
279
374
|
when '[' then All.new(parse_until_peek(']'))
|
280
375
|
when /^"/ then FindString.new(token[1..-2])
|
281
|
-
when /^#\w/ then MethodCall.new(token[1
|
282
|
-
when /^\.\w[\w\d_]+\?/ then InstanceMethodCall.new(token[1
|
376
|
+
when /^#\w/ then MethodCall.new(token[1..])
|
377
|
+
when /^\.\w[\w\d_]+\?/ then InstanceMethodCall.new(token[1..])
|
283
378
|
when '$' then Capture.new(parse)
|
284
379
|
when '!' then (@tokens.any? ? Not.new(parse) : Find.new(token))
|
285
380
|
when '?' then Maybe.new(parse)
|
286
381
|
when '^' then Parent.new(parse)
|
287
382
|
when '\\' then FindWithCapture.new(parse)
|
288
|
-
when /^%\d/ then FindFromArgument.new(token[1
|
383
|
+
when /^%\d/ then FindFromArgument.new(token[1..])
|
289
384
|
else Find.new(token)
|
290
385
|
end
|
291
386
|
end
|