Module: Fast

Defined in:
lib/fast.rb,
lib/fast/cli.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, Capture, Cli, Experiment, ExperimentCombinations, ExperimentFile, ExpressionParser, Find, FindFromArgument, FindString, FindWithCapture, InstanceMethodCall, Matcher, Maybe, MethodCall, 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.1.9'
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.



198
199
200
# File 'lib/fast.rb', line 198

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)') ⇒ Astrolabe::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:

  • (Astrolabe::Node)

    from the parsed content



75
76
77
78
79
# File 'lib/fast.rb', line 75

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

.ast_from_file(file) ⇒ Astrolabe::Node

caches the content based on the filename.

Examples:

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

Returns:

  • (Astrolabe::Node)

    parsed from file content



85
86
87
88
# File 'lib/fast.rb', line 85

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`.



129
130
131
132
133
134
135
136
137
138
# File 'lib/fast.rb', line 129

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

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

Only captures from a search

Returns:

  • (Array<Object>)

    with all captured elements.



184
185
186
187
188
189
190
191
192
# File 'lib/fast.rb', line 184

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



120
121
122
123
# File 'lib/fast.rb', line 120

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



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

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


210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/fast.rb', line 210

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



194
195
196
# File 'lib/fast.rb', line 194

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:

  • node (Astrolabe::Node)

Returns:

  • (String)

    with an pattern to search from it.

See Also:



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

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



147
148
149
150
151
152
153
154
155
# File 'lib/fast.rb', line 147

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



94
95
96
# File 'lib/fast.rb', line 94

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_sexp: false, file: nil, headless: 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
# File 'lib/fast/cli.rb', line 39

def report(result, show_sexp: false, file: nil, headless: false, colorize: true)
  if file
    line = result.loc.expression.line if result.is_a?(Parser::AST::Node)
    puts(highlight("# #{file}:#{line}", colorize: colorize)) unless headless
  end
  puts highlight(result, show_sexp: show_sexp, colorize: colorize)
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.



228
229
230
231
232
233
234
235
236
237
238
# File 'lib/fast.rb', line 228

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



171
172
173
174
175
176
177
178
179
180
# File 'lib/fast.rb', line 171

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<Astrolabe::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<Astrolabe::Node>>)

    with files and results



111
112
113
114
# File 'lib/fast.rb', line 111

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<Astrolabe::Node>

Search with pattern directly on file

Returns:

  • (Array<Astrolabe::Node>)

    that matches the pattern



100
101
102
103
104
105
# File 'lib/fast.rb', line 100

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