ffast 0.1.5 → 0.2.0
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 +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
|