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?
-
Current directory that the command is being runned
-
Home folder
-
Using the `FAST_FILE_DIR` variable to set an extra folder
-
- TOKENIZER
Allowed tokens in the node pattern domain
- VERSION
Attributes
Public Class Methods
@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
@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
@return [Proc] binding `pattern` argument from a given `method_name`. @param [Symbol] method_name with `:capture_file` or `:search_file` @param [String] pattern to match in a search to any file @param [Proc] on_result is a callback that can be notified soon it matches
# 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
# File lib/fast.rb, line 143 def builder_for(buffer_name) builder = Builder.new builder.buffer_name = buffer_name builder end
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
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
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
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
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
# File lib/fast.rb, line 262 def expression(string) ExpressionParser.new(string).parse end
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
@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
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
Loads `Fastfiles` from {.fast_files} list
# File lib/fast/shortcut.rb, line 36 def load_fast_files! fast_files.each(&method(:load)) end
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
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
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
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
@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
@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 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 @yield node and capture if block given
# 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 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 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
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
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
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
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