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.
@@ -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