ffast 0.0.8 → 0.0.9

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.
data/docs/index.md CHANGED
@@ -284,15 +284,13 @@ Fast.capture(code('a = 1'), '(int $_)') # => 1
284
284
  And if I want to refactor a code and use `delegate <attribute>, to: <object>`, try with replace:
285
285
 
286
286
  ```ruby
287
- Fast.replace ast,
288
- '(def $_ ... (send (send nil $_) \1))',
289
- -> (node, captures) {
287
+ Fast.replace ast, '(def $_ ... (send (send nil $_) \1))' do |node, captures|
290
288
  attribute, object = captures
291
289
  replace(
292
290
  node.location.expression,
293
291
  "delegate :#{attribute}, to: :#{object}"
294
292
  )
295
- }
293
+ end
296
294
  ```
297
295
 
298
296
  ## Fast.replace_file
data/lib/fast.rb CHANGED
@@ -22,12 +22,14 @@ end
22
22
 
23
23
  # Fast is a tool to help you search in the code through the Abstract Syntax Tree
24
24
  module Fast
25
+ # Literals are shortcuts allowed inside {ExpressionParser}
25
26
  LITERAL = {
26
27
  '...' => ->(node) { node&.children&.any? },
27
28
  '_' => ->(node) { !node.nil? },
28
29
  'nil' => nil
29
30
  }.freeze
30
31
 
32
+ # Allowed tokens in the node pattern domain
31
33
  TOKENIZER = %r/
32
34
  [\+\-\/\*\\!] # operators or negation
33
35
  |
@@ -65,38 +67,82 @@ module Fast
65
67
  /x.freeze
66
68
 
67
69
  class << self
68
- def match?(ast, search, *args)
69
- Matcher.new(ast, search, *args).match?
70
+ # @return [Astrolabe::Node] from the parsed content
71
+ # @example
72
+ # Fast.ast("1") # => s(:int, 1)
73
+ # Fast.ast("a.b") # => s(:send, s(:send, nil, :a), :b)
74
+ def ast(content, buffer_name: '(string)')
75
+ buffer = Parser::Source::Buffer.new(buffer_name)
76
+ buffer.source = content
77
+ Parser::CurrentRuby.new(Astrolabe::Builder.new).parse(buffer)
78
+ end
79
+
80
+ # @return [Astrolabe::Node] parsed from file content
81
+ # caches the content based on the filename.
82
+ # @example
83
+ # Fast.ast_from_file("example.rb") # => s(...)
84
+ def ast_from_file(file)
85
+ @cache ||= {}
86
+ @cache[file] ||= ast(IO.read(file), buffer_name: file)
87
+ end
88
+
89
+ # Verify if a given AST matches with a specific pattern
90
+ # @return [Boolean] case matches ast with the current expression
91
+ # @example
92
+ # Fast.match?(Fast.ast("1"),"int") # => true
93
+ def match?(ast, pattern, *args)
94
+ Matcher.new(ast, pattern, *args).match?
70
95
  end
71
96
 
72
- def replace(ast, search, &replacement)
97
+ # Replaces content based on a pattern.
98
+ # @param &replacement gives the [Rewriter] context in the block.
99
+ # @example
100
+ # Fast.replace?(Fast.ast("a = 1"),"lvasgn") do |node|
101
+ # replace(node.location.name, 'variable_renamed')
102
+ # end # => variable_renamed = 1
103
+ # @return [String] with the new source code after apply the replacement
104
+ # @see Fast::Rewriter
105
+ def replace(ast, pattern, &replacement)
73
106
  buffer = Parser::Source::Buffer.new('replacement')
74
107
  buffer.source = ast.loc.expression.source
75
- to_replace = search(ast, search)
108
+ to_replace = search(ast, pattern)
76
109
  types = to_replace.grep(Parser::AST::Node).map(&:type).uniq
77
110
  rewriter = Rewriter.new
78
111
  rewriter.buffer = buffer
79
- rewriter.search = search
112
+ rewriter.search = pattern
80
113
  rewriter.replacement = replacement
81
114
  rewriter.affect_types(*types)
82
115
  rewriter.rewrite(buffer, ast)
83
116
  end
84
117
 
85
- def replace_file(file, search, &replacement)
118
+ # Replaces the source of an {#ast_from_file} with
119
+ # based on a search.
120
+ # @return [String] with the content of the new file
121
+ # and the same source if the pattern does not match.
122
+ def replace_file(file, pattern, &replacement)
86
123
  ast = ast_from_file(file)
87
- replace(ast, search, &replacement)
124
+ replace(ast, pattern, &replacement)
88
125
  end
89
126
 
127
+ # Search with pattern directly on file
128
+ # @return Array<Astrolabe::Node> that matches the pattern
90
129
  def search_file(pattern, file)
91
130
  node = ast_from_file(file)
92
131
  search node, pattern
93
132
  end
94
133
 
134
+ # Capture elements from searches in files. Keep in mind you need to use `$`
135
+ # in the pattern to make it work.
136
+ # @return Array<Object> captured from the pattern matched in the file
95
137
  def capture_file(pattern, file)
96
138
  node = ast_from_file(file)
97
139
  capture node, pattern
98
140
  end
99
141
 
142
+ # Search recursively into a node and its children.
143
+ # If the node matches with the pattern it returns the node,
144
+ # otherwise it recursively collect possible children nodes
145
+ # @yield node and capture if block given
100
146
  def search(node, pattern)
101
147
  if (match = Fast.match?(node, pattern))
102
148
  yield node, match if block_given?
@@ -108,6 +154,9 @@ module Fast
108
154
  end
109
155
  end
110
156
 
157
+ # Return only captures from a search
158
+ # @return Array<Object> with all captured elements.
159
+ # If the result is only a single capture, it will return the single element.
111
160
  def capture(node, pattern)
112
161
  res =
113
162
  if (match = Fast.match?(node, pattern))
@@ -120,17 +169,8 @@ module Fast
120
169
  res&.size == 1 ? res[0] : res
121
170
  end
122
171
 
123
- def ast(content, buffer_name: '(string)')
124
- buffer = Parser::Source::Buffer.new(buffer_name)
125
- buffer.source = content
126
- Parser::CurrentRuby.new(Astrolabe::Builder.new).parse(buffer)
127
- end
128
-
129
- def ast_from_file(file)
130
- @cache ||= {}
131
- @cache[file] ||= ast(IO.read(file), buffer_name: file)
132
- end
133
-
172
+ # Highligh some source code based on the node.
173
+ # Useful for printing code with syntax highlight.
134
174
  def highlight(node, show_sexp: false)
135
175
  output =
136
176
  if node.respond_to?(:loc) && !show_sexp
@@ -141,6 +181,13 @@ module Fast
141
181
  CodeRay.scan(output, :ruby).term
142
182
  end
143
183
 
184
+ # Combines {.highlight} with files printing file name in the head with the
185
+ # source line.
186
+ # @param result [Astrolabe::Node]
187
+ # @param show_sexp [Boolean] Show string expression instead of source
188
+ # @param file [String] Show the file name and result line before content
189
+ # @example
190
+ # Fast.highlight(Fast.search(...))
144
191
  def report(result, show_sexp: nil, file: nil)
145
192
  if file
146
193
  line = result.loc.expression.line if result.is_a?(Parser::AST::Node)
@@ -152,8 +199,19 @@ module Fast
152
199
  def expression(string)
153
200
  ExpressionParser.new(string).parse
154
201
  end
202
+
155
203
  attr_accessor :debugging
156
204
 
205
+ # Utility function to inspect search details using debug block.
206
+ #
207
+ # It prints output of all matching cases.
208
+ #
209
+ # @example
210
+ # Fast.debug do
211
+ # Fast.match?(s(:int, 1), [:int, 1])
212
+ # end
213
+ # int == (int 1) # => true
214
+ # 1 == 1 # => true
157
215
  def debug
158
216
  return yield if debugging
159
217
 
@@ -169,6 +227,9 @@ module Fast
169
227
  result
170
228
  end
171
229
 
230
+ # @return Array<String> with all ruby files from arguments.
231
+ # @param *files can be files or directories.
232
+ # When the argument is a folder, it recursively fetches all `.rb` files from it.
172
233
  def ruby_files_from(*files)
173
234
  directories = files.select(&File.method(:directory?))
174
235
 
@@ -180,6 +241,15 @@ module Fast
180
241
  files
181
242
  end
182
243
 
244
+ # Extracts a node pattern expression from a given node supressing identifiers and primitive types.
245
+ # Useful to index abstract patterns or similar code structure.
246
+ # @see https://jonatas.github.io/fast/similarity_tutorial/
247
+ # @return [String] with an pattern to search from it.
248
+ # @param node [Astrolabe::Node]
249
+ # @example
250
+ # Fast.expression_from(Fast.ast('1')) # => '(int _)'
251
+ # Fast.expression_from(Fast.ast('a = 1')) # => '(lvasgn _ (int _))'
252
+ # Fast.expression_from(Fast.ast('def name; person.name end')) # => '(def _ (args) (send (send nil _) _))'
183
253
  def expression_from(node)
184
254
  case node
185
255
  when Parser::AST::Node
@@ -195,9 +265,22 @@ module Fast
195
265
  end
196
266
  end
197
267
 
198
- # Rewriter encapsulates `#match_index` allowing to rewrite only specific matching occurrences
199
- # into the file. It empowers the `Fast.experiment` and offers some useful insights for running experiments.
268
+ # Rewriter encapsulates {Rewriter#match_index} to allow
269
+ # {ExperimentFile.partial_replace} in a {Fast::ExperimentFile}.
270
+ # @see https://www.rubydoc.info/github/whitequark/parser/Parser/TreeRewriter
271
+ # @note the standalone class needs to combines {Rewriter#affect_types} to properly generate the `on_<node-type>` methods depending on the expression being used.
272
+ # @example Simple Rewriter
273
+ # ast = Fast.ast("a = 1")
274
+ # buffer = Parser::Source::Buffer.new('replacement')
275
+ # buffer.source = ast.loc.expression.source
276
+ # rewriter = Rewriter.new
277
+ # rewriter.buffer = buffer
278
+ # rewriter.search ='(lvasgn _ ...)'
279
+ # rewriter.replacement = -> (node) { replace(node.location.name, 'variable_renamed') }
280
+ # rewriter.affect_types(:lvasgn)
281
+ # rewriter.rewrite(buffer, ast) # => "variable_renamed = 1"
200
282
  class Rewriter < Parser::TreeRewriter
283
+ # @return [Integer] with occurrence index
201
284
  attr_reader :match_index
202
285
  attr_accessor :buffer, :search, :replacement
203
286
  def initialize(*args)
@@ -209,6 +292,8 @@ module Fast
209
292
  Fast.match?(node, search)
210
293
  end
211
294
 
295
+ # Generate methods for all affected types.
296
+ # @see Fast.replace
212
297
  def affect_types(*types) # rubocop:disable Metrics/MethodLength
213
298
  types.map do |type|
214
299
  self.class.send :define_method, "on_#{type}" do |node|
@@ -227,21 +312,47 @@ module Fast
227
312
  end
228
313
 
229
314
  # ExpressionParser empowers the AST search in Ruby.
230
- # You can check a few classes inheriting `Fast::Find` and adding extra behavior.
231
- # Parens encapsulates node search: `(node_type children...)` .
315
+ # All classes inheriting Fast::Find have a grammar shortcut that is processed here.
316
+ #
232
317
  # Exclamation Mark to negate: `!(int _)` is equivalent to a `not integer` node.
233
318
  # Curly Braces allows [Any]: `({int float} _)` or `{(int _) (float _)}`.
234
319
  # Square Braquets allows [All]: [(int _) !(int 0)] # all integer less zero.
235
320
  # Dollar sign can be used to capture values: `(${int float} _)` will capture the node type.
321
+ #
322
+ # @example find a simple int node
323
+ # Fast.expression("int")
324
+ # # => #<Fast::Find:0x00007ffae39274e0 @token="int">
325
+ # @example parens make the expression an array of Fast::Find and children classes
326
+ # Fast.expression("(int _)")
327
+ # # => [#<Fast::Find:0x00007ffae3a860e8 @token="int">, #<Fast::Find:0x00007ffae3a86098 @token="_">]
328
+ # @example not int token
329
+ # Fast.expression("!int")
330
+ # # => #<Fast::Not:0x00007ffae43f35b8 @token=#<Fast::Find:0x00007ffae43f35e0 @token="int">>
331
+ # @example int or float token
332
+ # Fast.expression("{int float}")
333
+ # # => #<Fast::Any:0x00007ffae43bbf00 @token=[
334
+ # # #<Fast::Find:0x00007ffae43bbfa0 @token="int">,
335
+ # # #<Fast::Find:0x00007ffae43bbf50 @token="float">
336
+ # # #]>
337
+ # @example capture something not nil
338
+ # Fast.expression("$_")
339
+ # # => #<Fast::Capture:0x00007ffae433a860 @captures=[], @token=#<Fast::Find:0x00007ffae433a888 @token="_">>
340
+ # @example capture a hash with keys that all are not string and not symbols
341
+ # Fast.expression("(hash (pair ([!sym !str] _))")
342
+ # # => [#<Fast::Find:0x00007ffae3b45010 @token="hash">,
343
+ # # [#<Fast::Find:0x00007ffae3b44f70 @token="pair">,
344
+ # # [#<Fast::All:0x00007ffae3b44cf0 @token=[
345
+ # # #<Fast::Not:0x00007ffae3b44e30 @token=#<Fast::Find:0x00007ffae3b44e80 @token="sym">>,
346
+ # # #<Fast::Not:0x00007ffae3b44d40 @token=#<Fast::Find:0x00007ffae3b44d68 @token="str">>]>,
347
+ # # #<Fast::Find:0x00007ffae3b44ca0 @token="_">]]]")")
348
+ # @example of match using string expression
349
+ # Fast.match?(Fast.ast("{1 => 1}"),"(hash (pair ([!sym !str] _))") => true")")
236
350
  class ExpressionParser
351
+ # @param expression [String]
237
352
  def initialize(expression)
238
353
  @tokens = expression.scan TOKENIZER
239
354
  end
240
355
 
241
- def next_token
242
- @tokens.shift
243
- end
244
-
245
356
  # rubocop:disable Metrics/CyclomaticComplexity
246
357
  # rubocop:disable Metrics/AbcSize
247
358
  # rubocop:disable Metrics/MethodLength
@@ -262,10 +373,17 @@ module Fast
262
373
  else Find.new(token)
263
374
  end
264
375
  end
376
+
265
377
  # rubocop:enable Metrics/CyclomaticComplexity
266
378
  # rubocop:enable Metrics/AbcSize
267
379
  # rubocop:enable Metrics/MethodLength
268
380
 
381
+ private
382
+
383
+ def next_token
384
+ @tokens.shift
385
+ end
386
+
269
387
  def parse_until_peek(token)
270
388
  list = []
271
389
  list << parse until @tokens.empty? || @tokens.first == token
@@ -387,6 +505,11 @@ module Fast
387
505
 
388
506
  # Allow use previous captures while searching in the AST.
389
507
  # Use `\\1` to point the match to the first captured element
508
+ # or sequential numbers considering the order of the captures.
509
+ #
510
+ # @example check comparision of integers that will always return true
511
+ # ast = Fast.ast("1 == 1") => s(:send, s(:int, 1), :==, s(:int, 1))
512
+ # Fast.match?(ast, "(send $(int _) == \1)") # => [s(:int, 1)]
390
513
  class FindWithCapture < Find
391
514
  attr_writer :previous_captures
392
515
 
@@ -406,8 +529,14 @@ module Fast
406
529
  end
407
530
  end
408
531
 
532
+ # Allow the user to interpolate expressions from external stuff.
409
533
  # Use `%1` in the expression and the Matcher#prepare_arguments will
410
534
  # interpolate the argument in the expression.
535
+ # @example interpolate the node value 1
536
+ # Fast.match?(Fast.ast("1"), "(int %1)", 1) # => true
537
+ # Fast.match?(Fast.ast("1"), "(int %1)", 2) # => false
538
+ # @example interpolate multiple arguments
539
+ # Fast.match?(Fast.ast("1"), "(%1 %2)", :int, 1) # => true
411
540
  class FindFromArgument < Find
412
541
  attr_writer :arguments
413
542
 
@@ -430,19 +559,41 @@ module Fast
430
559
  end
431
560
  end
432
561
 
433
- # Capture some expression while searching for it:
434
- # Example: `(${int float} _)` will capture the node type
435
- # Example: `$({int float} _)` will capture the node
436
- # Example: `({int float} $_)` will capture the value
437
- # Example: `(${int float} $_)` will capture both node type and value
438
- # You can capture multiple levels
562
+ # Capture some expression while searching for it.
563
+ #
564
+ # The captures behaves exactly like Fast::Find and the only difference is that
565
+ # when it {#match?} stores #captures for future usage.
566
+ #
567
+ # @example capture int node
568
+ # capture = Fast::Capture.new("int") => #<Fast::Capture:0x00...e0 @captures=[], @token="int">
569
+ # capture.match?(Fast.ast("1")) # => [s(:int, 1)]
570
+ #
571
+ # @example binding directly in the Fast.expression
572
+ # Fast.match?(Fast.ast("1"), "(int $_)") # => [1]
573
+ #
574
+ # @example capture the value of a local variable assignment
575
+ # (${int float} _)
576
+ # @example expression to capture only the node type
577
+ # (${int float} _)
578
+ # @example expression to capture entire node
579
+ # $({int float} _)
580
+ # @example expression to capture only the node value of int or float nodes
581
+ # ({int float} $_)
582
+ # @example expression to capture both node type and value
583
+ # ($_ $_)
584
+ #
585
+ # You can capture stuff in multiple levels and
586
+ # build expressions that reference captures with Fast::FindWithCapture.
439
587
  class Capture < Find
588
+ # Stores nodes that matches with the current expression.
440
589
  attr_reader :captures
590
+
441
591
  def initialize(token)
442
592
  super
443
593
  @captures = []
444
594
  end
445
595
 
596
+ # Append the matching node to {#captures} if it matches
446
597
  def match?(node)
447
598
  @captures << node if super
448
599
  end
@@ -507,7 +658,24 @@ module Fast
507
658
  end
508
659
  end
509
660
 
510
- # Joins the AST and the search expression to create a complete match
661
+ # Joins the AST and the search expression to create a complete matcher that
662
+ # recusively check if the node pattern expression matches with the given AST.
663
+ #
664
+ ### Using captures
665
+ #
666
+ # One of the most important features of the matcher is find captures and also
667
+ # bind them on demand in case the expression is using previous captures.
668
+ #
669
+ # @example simple match
670
+ # ast = Fast.ast("a = 1")
671
+ # expression = Fast.expression("(lvasgn _ (int _))")
672
+ # Matcher.new(ast,expression).match? # true
673
+ #
674
+ # @example simple capture
675
+ # ast = Fast.ast("a = 1")
676
+ # expression = Fast.expression("(lvasgn _ (int $_))")
677
+ # Matcher.new(ast,expression).match? # => [1]
678
+ #
511
679
  class Matcher
512
680
  def initialize(ast, fast, *args)
513
681
  @ast = ast
@@ -520,19 +688,8 @@ module Fast
520
688
  prepare_arguments(@fast, args) if args.any?
521
689
  end
522
690
 
523
- def prepare_arguments(expression, arguments)
524
- case expression
525
- when Array
526
- expression.each do |item|
527
- prepare_arguments(item, arguments)
528
- end
529
- when Fast::FindFromArgument
530
- expression.arguments = arguments
531
- when Fast::Find
532
- prepare_arguments expression.token, arguments
533
- end
534
- end
535
-
691
+ # @return [true] if the @param ast recursively matches with expression.
692
+ # @return #find_captures case matches
536
693
  def match?(ast = @ast, fast = @fast)
537
694
  head, *tail = fast
538
695
  return false unless head.match?(ast)
@@ -547,13 +704,9 @@ module Fast
547
704
  end && find_captures
548
705
  end
549
706
 
550
- def prepare_token(token)
551
- case token
552
- when Fast::FindWithCapture
553
- token.previous_captures = find_captures
554
- end
555
- end
556
-
707
+ # Look recursively into @param fast to check if the expression is have
708
+ # captures.
709
+ # @return [true] if any sub expression have captures.
557
710
  def captures?(fast = @fast)
558
711
  case fast
559
712
  when Capture then true
@@ -562,6 +715,12 @@ module Fast
562
715
  end
563
716
  end
564
717
 
718
+ # Find search captures recursively.
719
+ #
720
+ # @return Array<Object> of captures from the expression
721
+ # @return true in case of no captures in the expression
722
+ # @see Fast::Capture
723
+ # @see Fast::FindFromArgument
565
724
  def find_captures(fast = @fast)
566
725
  return true if fast == @fast && !captures?(fast)
567
726
 
@@ -571,5 +730,32 @@ module Fast
571
730
  when Find then find_captures(fast.token)
572
731
  end
573
732
  end
733
+
734
+ private
735
+
736
+ # Prepare arguments case the expression needs to bind extra arguments.
737
+ # @return [void]
738
+ def prepare_arguments(expression, arguments)
739
+ case expression
740
+ when Array
741
+ expression.each do |item|
742
+ prepare_arguments(item, arguments)
743
+ end
744
+ when Fast::FindFromArgument
745
+ expression.arguments = arguments
746
+ when Fast::Find
747
+ prepare_arguments expression.token, arguments
748
+ end
749
+ end
750
+
751
+ # @param [FindWithCapture] set the current captures
752
+ # as previous captures to the current node.
753
+ # @return [void] and only set [FindWithCapture#previous_captures]
754
+ def prepare_token(token)
755
+ case token
756
+ when Fast::FindWithCapture
757
+ token.previous_captures = find_captures
758
+ end
759
+ end
574
760
  end
575
761
  end