ffast 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -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&amp;feature=youtu.be&amp;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>
@@ -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.3'
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 'rake', '~> 10.0'
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
@@ -50,7 +50,7 @@ module Fast
50
50
  |
51
51
  \? # maybe expression
52
52
  |
53
- [\d\w_]+[\\!\?]? # method names or numbers
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 [Astrolabe::Node] from the parsed content
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(Astrolabe::Builder.new).parse(buffer)
140
+ Parser::CurrentRuby.new(builder_for(buffer_name)).parse(buffer)
79
141
  end
80
142
 
81
- # @return [Astrolabe::Node] parsed from file content
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<Astrolabe::Node>] that matches the pattern
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 [] if node.nil?
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<Astrolabe::Node>>] with files and results
110
- def search_all(pattern, locations = ['.'])
111
- search_pattern = method(:search_file).curry.call(pattern)
112
- group_results(search_pattern, locations)
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
- capture_pattern = method(:capture_file).curry.call(pattern)
121
- group_results(capture_pattern, locations)
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
- def group_results(search_pattern, locations)
125
- ruby_files_from(*locations)
126
- .map { |f| { f => search_pattern.call(f) } }
127
- .reject { |results| results.values.all?(&:empty?) }
128
- .inject(&:merge!)
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
- capture pattern, ast_from_file(file)
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
- # Return only captures from a search
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
- res = if (match = match?(pattern, node))
158
- match == true ? node : match
159
- else
160
- node.each_child_node
161
- .flat_map { |child| capture(pattern, child) }
162
- .compact.flatten
163
- end
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
- directories = files.select(&File.method(:directory?))
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 [Astrolabe::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}#{' ' + children_expression if node.children.any?})"
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..-1])
282
- when /^\.\w[\w\d_]+\?/ then InstanceMethodCall.new(token[1..-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..-1])
383
+ when /^%\d/ then FindFromArgument.new(token[1..])
289
384
  else Find.new(token)
290
385
  end
291
386
  end