module Fast

Fast is a tool to help you search in the code through the Abstract Syntax Tree

Fast is a powerful tool to search through the command line for specific Ruby code. It defines report and highlight functions that can be used to pretty print code and results from the search.

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

Git plugin for Fast::Node. It allows to easily access metadata from current file.

Rewriter loads a set of methods related to automated replacement using expressions and custom blocks of code.

Allow user to define shortcuts and reuse them in the command line.

Constants

LITERAL

Literals are shortcuts allowed inside {ExpressionParser}

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

TOKENIZER

Allowed tokens in the node pattern domain

VERSION

Attributes

debugging[RW]
experiments[R]

Public Class Methods

ast(content, buffer_name: '(string)') click to toggle source

@return [Fast::Node] from the parsed content @example

Fast.ast("1") # => s(:int, 1)
Fast.ast("a.b") # => s(:send, s(:send, nil, :a), :b)
# 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) click to toggle source

@return [Fast::Node] parsed from file content caches the content based on the filename. @example

Fast.ast_from_file("example.rb") # => s(...)
# File lib/fast.rb, line 153
def ast_from_file(file)
  @cache ||= {}
  @cache[file] ||= ast(IO.read(file), buffer_name: file)
end
builder_for(buffer_name) click to toggle source
# File lib/fast.rb, line 143
def builder_for(buffer_name)
  builder = Builder.new
  builder.buffer_name = buffer_name
  builder
end
capture(pattern, node) click to toggle source

Only captures from a search @return [Array<Object>] with all captured elements.

# 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) click to toggle source

Capture with pattern on a directory or multiple files @param [String] pattern @param [Array<String>] locations where to search. Default is '.' @return [Hash<String,Object>] with files and captures

# 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) click to toggle source

Capture elements from searches in files. Keep in mind you need to use `$` in the pattern to make it work. @return [Array<Object>] captured from the pattern matched in the file

# File lib/fast.rb, line 228
def capture_file(pattern, file)
  node = ast_from_file(file)
  return [] unless node

  capture pattern, node
end
debug() { || ... } click to toggle source

Utility function to inspect search details using debug block.

It prints output of all matching cases.

@example

 Fast.debug do
    Fast.match?([:int, 1], s(:int, 1))
 end
int == (int 1) # => true
1 == 1 # => true
# 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) click to toggle source

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.

@example 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
# File lib/fast/experiment.rb, line 25
def experiment(name, &block)
  @experiments ||= {}
  @experiments[name] = Experiment.new(name, &block)
end
expression(string) click to toggle source
# File lib/fast.rb, line 262
def expression(string)
  ExpressionParser.new(string).parse
end
expression_from(node) click to toggle source

Extracts a node pattern expression from a given node supressing identifiers and primitive types. Useful to index abstract patterns or similar code structure. @see jonatas.github.io/fast/similarity_tutorial/ @return [String] with an pattern to search from it. @param node [Fast::Node] @example

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 _) _))'
# 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_files() click to toggle source

@return [Array<String>] with existent Fastfiles from {LOOKUP_FAST_FILES_DIRECTORIES}.

# 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) click to toggle source

Compact grouped results by file allowing parallel processing. @param [Proc] group_files allows to define a search that can be executed parallel or not. @param [Proc] on_result allows to define a callback for fast feedback while it process several locations in parallel. @param [Boolean] parallel runs the `group_files` in parallel @return [Hash[String, Array]] with files and results

# 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
load_fast_files!() click to toggle source

Loads `Fastfiles` from {.fast_files} list

# File lib/fast/shortcut.rb, line 36
def load_fast_files!
  fast_files.each(&method(:load))
end
match?(pattern, ast, *args) click to toggle source

Verify if a given AST matches with a specific pattern @return [Boolean] case matches ast with the current expression @example

Fast.match?("int", Fast.ast("1")) # => true
# File lib/fast.rb, line 162
def match?(pattern, ast, *args)
  Matcher.new(pattern, ast, *args).match?
end
replace(pattern, ast, source = nil, &replacement) click to toggle source

Replaces content based on a pattern. @param [Astrolabe::Node] ast with the current AST to search. @param [String] pattern with the expression to be targeting nodes. @param [Proc] replacement gives the [Rewriter] context in the block. @example

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

@return [String] with the new source code after apply the replacement @see Fast::Rewriter

# 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) click to toggle source

Replaces the source of an {Fast#ast_from_file} with and the same source if the pattern does not match.

# 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
rewrite_file(pattern, file, &replacement) click to toggle source

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

# 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) click to toggle source

@return [Fast::Rewriter]

# 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) click to toggle source

@return [Array<String>] with all ruby files from arguments. @param files can be file paths or directories. When the argument is a folder, it recursively fetches all `.rb` files from it.

# 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_all(pattern, locations = ['.'], parallel: true, on_result: nil) click to toggle source

Search with pattern on a directory or multiple files @param [String] pattern @param [Array<String>] *locations where to search. Default is '.' @return [Hash<String,Array<Fast::Node>>] with files and results

# 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) click to toggle source

Search with pattern directly on file @return [Array<Fast::Node>] that matches the pattern

# 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) click to toggle source

Store predefined searches with default paths through shortcuts. define your Fastfile in you root folder or @example Shortcut for finding validations in rails models

Fast.shortcut(:validations, "(send nil {validate validates})", "app/models")
# 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
shortcuts() click to toggle source

Stores shortcuts in a simple hash where the key is the identifier and the value is the object itself. @return [Hash<String,Shortcut>] as a dictionary.

# File lib/fast/shortcut.rb, line 24
def shortcuts
  @shortcuts ||= {}
end

Public Instance Methods

highlight(node, show_sexp: false, colorize: true) click to toggle source

Highligh some source code based on the node. Useful for printing code with syntax highlight. @param show_sexp [Boolean] prints node expression instead of code @param colorize [Boolean] skips `CodeRay` processing when false.

# 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
report(result, show_link: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true) click to toggle source

Combines {.highlight} with files printing file name in the head with the source line. @param result [Astrolabe::Node] @param show_sexp [Boolean] Show string expression instead of source @param file [String] Show the file name and result line before content @param headless [Boolean] Skip printing the file name and line before content @example

Fast.highlight(Fast.search(...))
# 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