Module: Fast

Defined in:
lib/fast.rb,
lib/fast/cli.rb,
lib/fast/git.rb,
lib/fast/version.rb,
lib/fast/rewriter.rb,
lib/fast/shortcut.rb,
lib/fast/experiment.rb

Overview

Allow to replace code managing multiple replacements and combining replacements. Useful for large codebase refactor and multiple replacements in the same file.

Defined Under Namespace

Classes: All, Any, Builder, Capture, Cli, Experiment, ExperimentCombinations, ExperimentFile, ExpressionParser, Find, FindFromArgument, FindString, FindWithCapture, InstanceMethodCall, Matcher, Maybe, MethodCall, Node, Not, Parent, Rewriter, Shortcut

Constant Summary collapse

LITERAL =

Literals are shortcuts allowed inside ExpressionParser

{
  '...' => ->(node) { node&.children&.any? },
  '_' => ->(node) { !node.nil? },
  'nil' => nil
}.freeze
TOKENIZER =

Allowed tokens in the node pattern domain

%r/
  [\+\-\/\*\\!]         # operators or negation
  |
  ===?                  # == or ===
  |
  \d+\.\d*              # decimals and floats
  |
  "[^"]+"               # strings
  |
  _                     # something not nil: match
  |
  \.{3}                 # a node with children: ...
  |
  \[|\]                 # square brackets `[` and `]` for all
  |
  \^                    # node has children with
  |
  \?                    # maybe expression
  |
  [\d\w_]+[=\\!\?]?     # method names or numbers
  |
  \(|\)                 # parens `(` and `)` for tuples
  |
  \{|\}                 # curly brackets `{` and `}` for any
  |
  \$                    # capture
  |
  \#\w[\d\w_]+[\\!\?]?  # custom method call
  |
  \.\w[\d\w_]+\?       # instance method call
  |
  \\\d                  # find using captured expression
  |
  %\d                   # bind extra arguments to the expression
/x.freeze
VERSION =
'0.2.0'
LOOKUP_FAST_FILES_DIRECTORIES =

Where to search for ‘Fastfile` archives?

  1. Current directory that the command is being runned

  2. Home folder

  3. Using the ‘FAST_FILE_DIR` variable to set an extra folder

[Dir.pwd, ENV['HOME'], ENV['FAST_FILE_DIR']].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.debuggingObject

Returns the value of attribute debugging.



266
267
268
# File 'lib/fast.rb', line 266

def debugging
  @debugging
end

.experimentsObject (readonly)

Returns the value of attribute experiments.



30
31
32
# File 'lib/fast/experiment.rb', line 30

def experiments
  @experiments
end

Class Method Details

.ast(content, buffer_name: '(string)') ⇒ Fast::Node

Returns from the parsed content.

Examples:

Fast.ast("1") # => s(:int, 1)
Fast.ast("a.b") # => s(:send, s(:send, nil, :a), :b)

Returns:



137
138
139
140
141
# File 'lib/fast.rb', line 137

def ast(content, buffer_name: '(string)')
  buffer = Parser::Source::Buffer.new(buffer_name)
  buffer.source = content
  Parser::CurrentRuby.new(builder_for(buffer_name)).parse(buffer)
end

.ast_from_file(file) ⇒ Fast::Node

caches the content based on the filename.

Examples:

Fast.ast_from_file("example.rb") # => s(...)

Returns:



153
154
155
156
# File 'lib/fast.rb', line 153

def ast_from_file(file)
  @cache ||= {}
  @cache[file] ||= ast(IO.read(file), buffer_name: file)
end

.build_grouped_search(method_name, pattern, on_result) ⇒ Proc

Returns binding ‘pattern` argument from a given `method_name`.

Parameters:

  • method_name (Symbol)

    with ‘:capture_file` or `:search_file`

  • pattern (String)

    to match in a search to any file

  • on_result (Proc)

    is a callback that can be notified soon it matches

Returns:

  • (Proc)

    binding ‘pattern` argument from a given `method_name`.



197
198
199
200
201
202
203
204
205
206
# File 'lib/fast.rb', line 197

def build_grouped_search(method_name, pattern, on_result)
  search_pattern = method(method_name).curry.call(pattern)
  proc do |file|
    results = search_pattern.call(file)
    next if results.nil? || results.empty?

    on_result&.(file, results)
    { file => results }
  end
end

.builder_for(buffer_name) ⇒ Object



143
144
145
146
147
# File 'lib/fast.rb', line 143

def builder_for(buffer_name)
  builder = Builder.new
  builder.buffer_name = buffer_name
  builder
end

.capture(pattern, node) ⇒ Array<Object>

Only captures from a search

Returns:

  • (Array<Object>)

    with all captured elements.



252
253
254
255
256
257
258
259
260
# File 'lib/fast.rb', line 252

def capture(pattern, node)
  if (match = match?(pattern, node))
    match == true ? node : match
  else
    node.each_child_node
      .flat_map { |child| capture(pattern, child) }
      .compact.flatten
  end
end

.capture_all(pattern, locations = ['.'], parallel: true, on_result: nil) ⇒ Hash<String,Object>

Capture with pattern on a directory or multiple files

Parameters:

  • pattern (String)
  • locations (Array<String>) (defaults to: ['.'])

    where to search. Default is ‘.’

Returns:

  • (Hash<String,Object>)

    with files and captures



188
189
190
191
# File 'lib/fast.rb', line 188

def capture_all(pattern, locations = ['.'], parallel: true, on_result: nil)
  group_results(build_grouped_search(:capture_file, pattern, on_result),
                locations, parallel: parallel)
end

.capture_file(pattern, file) ⇒ Array<Object>

Capture elements from searches in files. Keep in mind you need to use ‘$` in the pattern to make it work.

Returns:

  • (Array<Object>)

    captured from the pattern matched in the file



228
229
230
231
232
233
# File 'lib/fast.rb', line 228

def capture_file(pattern, file)
  node = ast_from_file(file)
  return [] unless node

  capture pattern, node
end

.debugObject

Utility function to inspect search details using debug block.

It prints output of all matching cases.

int == (int 1) # => true
1 == 1 # => true

Examples:

Fast.debug do
   Fast.match?([:int, 1], s(:int, 1))
end


278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/fast.rb', line 278

def debug
  return yield if debugging

  self.debugging = true
  result = nil
  Find.class_eval do
    alias_method :original_match_recursive, :match_recursive
    alias_method :match_recursive, :debug_match_recursive
    result = yield
    alias_method :match_recursive, :original_match_recursive # rubocop:disable Lint/DuplicateMethods
  end
  self.debugging = false
  result
end

.experiment(name, &block) ⇒ Object

Fast.experiment is a shortcut to define new experiments and allow them to work together in experiment combinations.

The following experiment look into ‘spec` folder and try to remove `before` and `after` blocks on testing code. Sometimes they’re not effective and we can avoid the hard work of do it manually.

If the spec does not fail, it keeps the change.

Examples:

Remove useless before and after block

Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
  lookup 'spec'
  search "(block (send nil {before after}))"
  edit { |node| remove(node.loc.expression) }
  policy { |new_file| system("rspec --fail-fast #{new_file}") }
end


25
26
27
28
# File 'lib/fast/experiment.rb', line 25

def experiment(name, &block)
  @experiments ||= {}
  @experiments[name] = Experiment.new(name, &block)
end

.expression(string) ⇒ Object



262
263
264
# File 'lib/fast.rb', line 262

def expression(string)
  ExpressionParser.new(string).parse
end

.expression_from(node) ⇒ String

Extracts a node pattern expression from a given node supressing identifiers and primitive types. Useful to index abstract patterns or similar code structure.

Examples:

Fast.expression_from(Fast.ast('1')) # => '(int _)'
Fast.expression_from(Fast.ast('a = 1')) # => '(lvasgn _ (int _))'
Fast.expression_from(Fast.ast('def name; person.name end')) # => '(def _ (args) (send (send nil _) _))'

Parameters:

Returns:

  • (String)

    with an pattern to search from it.

See Also:



317
318
319
320
321
322
323
324
325
326
327
# File 'lib/fast.rb', line 317

def expression_from(node)
  case node
  when Parser::AST::Node
    children_expression = node.children.map(&method(:expression_from)).join(' ')
    "(#{node.type}#{" #{children_expression}" if node.children.any?})"
  when nil, 'nil'
    'nil'
  when Symbol, String, Numeric
    '_'
  end
end

.fast_filesArray<String>

Returns with existent Fastfiles from LOOKUP_FAST_FILES_DIRECTORIES.

Returns:



29
30
31
32
33
# File 'lib/fast/shortcut.rb', line 29

def fast_files
  @fast_files ||= LOOKUP_FAST_FILES_DIRECTORIES.compact
    .map { |dir| File.join(dir, 'Fastfile') }
    .select(&File.method(:exists?))
end

.group_results(group_files, locations, parallel: true) ⇒ Hash[String, Array]

Compact grouped results by file allowing parallel processing. parallel or not. while it process several locations in parallel.

Parameters:

  • group_files (Proc)

    allows to define a search that can be executed

  • on_result (Proc)

    allows to define a callback for fast feedback

  • parallel (Boolean) (defaults to: true)

    runs the ‘group_files` in parallel

Returns:

  • (Hash[String, Array])

    with files and results



215
216
217
218
219
220
221
222
223
# File 'lib/fast.rb', line 215

def group_results(group_files, locations, parallel: true)
  files = ruby_files_from(*locations)
  if parallel
    require 'parallel' unless defined?(Parallel)
    Parallel.map(files, &group_files)
  else
    files.map(&group_files)
  end.compact.inject(&:merge!)
end

.highlight(node, show_sexp: false, colorize: true) ⇒ Object

Highligh some source code based on the node. Useful for printing code with syntax highlight.

Parameters:

  • show_sexp (Boolean) (defaults to: false)

    prints node expression instead of code

  • colorize (Boolean) (defaults to: true)

    skips ‘CodeRay` processing when false.



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/fast/cli.rb', line 19

def highlight(node, show_sexp: false, colorize: true)
  output =
    if node.respond_to?(:loc) && !show_sexp
      node.loc.expression.source
    else
      node
    end
  return output unless colorize

  CodeRay.scan(output, :ruby).term
end

.load_fast_files!Object

Loads ‘Fastfiles` from fast_files list



36
37
38
# File 'lib/fast/shortcut.rb', line 36

def load_fast_files!
  fast_files.each(&method(:load))
end

.match?(pattern, ast, *args) ⇒ Boolean

Verify if a given AST matches with a specific pattern

Examples:

Fast.match?("int", Fast.ast("1")) # => true

Returns:

  • (Boolean)

    case matches ast with the current expression



162
163
164
# File 'lib/fast.rb', line 162

def match?(pattern, ast, *args)
  Matcher.new(pattern, ast, *args).match?
end

.replace(pattern, ast, source = nil, &replacement) ⇒ String

Replaces content based on a pattern.

Examples:

Fast.replace?(Fast.ast("a = 1"),"lvasgn") do |node|
  replace(node.location.name, 'variable_renamed')
end # => variable_renamed = 1

Parameters:

  • ast (Astrolabe::Node)

    with the current AST to search.

  • pattern (String)

    with the expression to be targeting nodes.

  • replacement (Proc)

    gives the [Rewriter] context in the block.

Returns:

  • (String)

    with the new source code after apply the replacement

See Also:



17
18
19
# File 'lib/fast/rewriter.rb', line 17

def replace(pattern, ast, source = nil, &replacement)
  rewriter_for(pattern, ast, source, &replacement).rewrite!
end

.replace_file(pattern, file, &replacement) ⇒ Object

Replaces the source of an ast_from_file with and the same source if the pattern does not match.



33
34
35
36
# File 'lib/fast/rewriter.rb', line 33

def replace_file(pattern, file, &replacement)
  ast = ast_from_file(file)
  replace(pattern, ast, IO.read(file), &replacement)
end

.report(result, show_link: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true) ⇒ Object

Combines highlight with files printing file name in the head with the source line.

Examples:

Fast.highlight(Fast.search(...))

Parameters:

  • result (Astrolabe::Node)
  • show_sexp (Boolean) (defaults to: false)

    Show string expression instead of source

  • file (String) (defaults to: nil)

    Show the file name and result line before content

  • headless (Boolean) (defaults to: false)

    Skip printing the file name and line before content



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/fast/cli.rb', line 39

def report(result, show_link: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true) # rubocop:disable Metrics/ParameterLists
  if file
    line = result.loc.expression.line if result.is_a?(Parser::AST::Node)
    if show_link
      puts(result.link)
    elsif !headless
      puts(highlight("# #{file}:#{line}", colorize: colorize))
    end
  end
  puts(highlight(result, show_sexp: show_sexp, colorize: colorize)) unless bodyless
end

.rewrite_file(pattern, file, &replacement) ⇒ Object

Combines #replace_file output overriding the file if the output is different from the original file content.



40
41
42
43
44
# File 'lib/fast/rewriter.rb', line 40

def rewrite_file(pattern, file, &replacement)
  previous_content = IO.read(file)
  content = replace_file(pattern, file, &replacement)
  File.open(file, 'w+') { |f| f.puts content } if content != previous_content
end

.rewriter_for(pattern, ast, source = nil, &replacement) ⇒ Fast::Rewriter

Returns:



22
23
24
25
26
27
28
29
# File 'lib/fast/rewriter.rb', line 22

def rewriter_for(pattern, ast, source = nil, &replacement)
  rewriter = Rewriter.new
  rewriter.source = source
  rewriter.ast = ast
  rewriter.search = pattern
  rewriter.replacement = replacement
  rewriter
end

.ruby_files_from(*files) ⇒ Array<String>

When the argument is a folder, it recursively fetches all ‘.rb` files from it.

Parameters:

  • files

    can be file paths or directories.

Returns:

  • (Array<String>)

    with all ruby files from arguments.



296
297
298
299
300
301
302
303
304
305
306
# File 'lib/fast.rb', line 296

def ruby_files_from(*files)
  dir_filter = File.method(:directory?)
  directories = files.select(&dir_filter)

  if directories.any?
    files -= directories
    files |= directories.flat_map { |dir| Dir["#{dir}/**/*.rb"] }
    files.uniq!
  end
  files.reject(&dir_filter)
end

.search(pattern, node, *args) { ... } ⇒ Object

Search recursively into a node and its children. If the node matches with the pattern it returns the node, otherwise it recursively collect possible children nodes

Yields:

  • node and capture if block given



239
240
241
242
243
244
245
246
247
248
# File 'lib/fast.rb', line 239

def search(pattern, node, *args)
  if (match = match?(pattern, node, *args))
    yield node, match if block_given?
    match != true ? [node, match] : [node]
  else
    node.each_child_node
      .flat_map { |child| search(pattern, child, *args) }
      .compact.flatten
  end
end

.search_all(pattern, locations = ['.'], parallel: true, on_result: nil) ⇒ Hash<String,Array<Fast::Node>>

Search with pattern on a directory or multiple files

Parameters:

  • pattern (String)
  • *locations (Array<String>)

    where to search. Default is ‘.’

Returns:

  • (Hash<String,Array<Fast::Node>>)

    with files and results



179
180
181
182
# File 'lib/fast.rb', line 179

def search_all(pattern, locations = ['.'], parallel: true, on_result: nil)
  group_results(build_grouped_search(:search_file, pattern, on_result),
                locations, parallel: parallel)
end

.search_file(pattern, file) ⇒ Array<Fast::Node>

Search with pattern directly on file

Returns:

  • (Array<Fast::Node>)

    that matches the pattern



168
169
170
171
172
173
# File 'lib/fast.rb', line 168

def search_file(pattern, file)
  node = ast_from_file(file)
  return [] unless node

  search pattern, node
end

.shortcut(identifier, *args, &block) ⇒ Object

Store predefined searches with default paths through shortcuts. define your Fastfile in you root folder or

Examples:

Shortcut for finding validations in rails models

Fast.shortcut(:validations, "(send nil {validate validates})", "app/models")


16
17
18
19
# File 'lib/fast/shortcut.rb', line 16

def shortcut(identifier, *args, &block)
  puts "identifier #{identifier.inspect} will be override" if shortcuts.key?(identifier)
  shortcuts[identifier] = Shortcut.new(*args, &block)
end

.shortcutsHash<String,Shortcut>

Stores shortcuts in a simple hash where the key is the identifier and the value is the object itself.

Returns:

  • (Hash<String,Shortcut>)

    as a dictionary.



24
25
26
# File 'lib/fast/shortcut.rb', line 24

def shortcuts
  @shortcuts ||= {}
end