ffast 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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