Module: Fast

Defined in:
lib/fast.rb,
lib/fast/cli.rb,
lib/fast/git.rb,
lib/fast/sql.rb,
lib/fast/version.rb,
lib/fast/rewriter.rb,
lib/fast/shortcut.rb,
lib/fast/experiment.rb,
lib/fast/sql/rewriter.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

Modules: SQL 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.2'
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'],
  File.join(File.dirname(__FILE__), '..', '..')
].compact.map(&File.method(:expand_path)).uniq.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.debuggingObject

Returns the value of attribute debugging.



291
292
293
# File 'lib/fast.rb', line 291

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. Also, it can parse SQL files.

Examples:

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

Returns:



154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/fast.rb', line 154

def ast_from_file(file)
  @cache ||= {}
  @cache[file] ||=
    begin
      method =
        if file.end_with?('.sql')
          require_relative 'fast/sql' unless respond_to?(:parse_sql)
          :parse_sql
        else
          :ast
        end
      Fast.public_send(method, IO.read(file), buffer_name: file)
    end
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`.



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

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.



277
278
279
280
281
282
283
284
285
# File 'lib/fast.rb', line 277

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



204
205
206
207
# File 'lib/fast.rb', line 204

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



244
245
246
247
248
249
250
251
252
253
# File 'lib/fast.rb', line 244

def capture_file(pattern, file)
  node = ast_from_file(file)
  return [] unless node
  case node
  when Array
    node.map { |n| capture(pattern, n) }.flatten.compact
  else
    capture pattern, node
  end
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


303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/fast.rb', line 303

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



287
288
289
# File 'lib/fast.rb', line 287

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:



342
343
344
345
346
347
348
349
350
351
352
# File 'lib/fast.rb', line 342

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:



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

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

.first_position_from_expression(node) ⇒ Object

If a node is the first on it’s line, print since the beginning of the line to show the proper whitespaces for identing the next lines of the code.



51
52
53
54
55
56
57
58
# File 'lib/fast/cli.rb', line 51

def first_position_from_expression(node)
  expression = node.loc.expression
  if node.parent && node.parent.loc.expression.line != expression.line
    expression.begin_pos - expression.column
  else
    expression.begin_pos
  end
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



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

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, sql: false) ⇒ 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.



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

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

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

.last_position_from_expression(node) ⇒ Object

If a method call contains a heredoc, it should print the STR around it too.



44
45
46
47
# File 'lib/fast/cli.rb', line 44

def last_position_from_expression(node)
  internal_heredoc = node.each_descendant(:str).select { |n| n.loc.respond_to?(:heredoc_end) }
  internal_heredoc.map { |n| n.loc.heredoc_end.end_pos }.max if internal_heredoc.any?
end

.load_fast_files!Object

Loads ‘Fastfiles` from fast_files list



41
42
43
# File 'lib/fast/shortcut.rb', line 41

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



173
174
175
# File 'lib/fast.rb', line 173

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

.parse_sql(statement, buffer_name: "(sql)") ⇒ Fast::Node

ast = Fast.parse_sql(“select ‘hello AST’”)

=> s(:select_stmt,
     s(:target_list,
       s(:res_target,
         s(:val,
           s(:a_const,
             s(:val,
               s(:string,
                 s(:str, "hello AST"))))))))

‘s` represents a Fast::Node which is a subclass of Parser::AST::Node and has additional methods to access the tokens and location of the node. ast.search(:string).first.location.expression

=> #<Parser::Source::Range (sql) 7...18>

Returns:

  • (Fast::Node)

    the AST representation of the sql statement



49
50
51
# File 'lib/fast/sql.rb', line 49

def parse_sql(statement, buffer_name: "(sql)")
  SQL.parse(statement, buffer_name: buffer_name)
end

.parse_sql_file(file) ⇒ Fast::Node

Shortcut to parse a sql file

Examples:

Fast.parse_sql_file(‘spec/fixtures/sql/select.sql’)

Returns:

  • (Fast::Node)

    the AST representation of the sql statements from a file



11
12
13
# File 'lib/fast/sql.rb', line 11

def parse_sql_file(file)
  SQL.parse_file(file)
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

.replace_sql(pattern, ast, &replacement) ⇒ Object

Fast.replace_sql(‘ival’, Fast.parse_sql(‘select 1’), &->(node){ replace(node.location.expression, ‘2’) }) # => “select 2”

Returns:

  • string with the sql content updated in case the pattern matches.

See Also:

  • SQLRewriter


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

def replace_sql(pattern, ast, &replacement)
  SQL.replace(pattern, ast, &replacement)
end

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

Returns string with the sql content updated in case the pattern matches.

Returns:

  • string with the sql content updated in case the pattern matches.



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

def replace_sql_file(pattern, file, &replacement)
  SQL.replace_file(pattern, file, &replacement)
end

.report(result, show_link: false, show_permalink: 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.report(result, file: 'file.rb')

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



68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/fast/cli.rb', line 68

def report(result, show_link: false, show_permalink: 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 show_permalink
      puts(result.permalink)
    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.



321
322
323
324
325
326
327
328
329
330
331
# File 'lib/fast.rb', line 321

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



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/fast.rb', line 259

def search(pattern, node, *args)
  if (match = match?(pattern, node, *args))
    yield node, match if block_given?
    match != true ? [node, match] : [node]
  else
    case node
    when Array
      node.flat_map { |child| search(pattern, child, *args) }
    else
      node.each_child_node
        .flat_map { |child| search(pattern, child, *args) }
        .compact.flatten
    end
  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



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

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



179
180
181
182
183
184
185
186
187
188
189
# File 'lib/fast.rb', line 179

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

  case node
  when Array
    node.map { |n| search(pattern, n) }.flatten.compact
  else
    search pattern, node
  end
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")


21
22
23
24
# File 'lib/fast/shortcut.rb', line 21

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.



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

def shortcuts
  @shortcuts ||= {}
end

.sql_rewriter_for(pattern, ast, &replacement) ⇒ Fast::SQLRewriter

Returns which can be used to rewrite the SQL.

Returns:

  • (Fast::SQLRewriter)

    which can be used to rewrite the SQL

See Also:

  • SQLRewriter


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

def sql_rewriter_for(pattern, ast, &replacement)
  SQL.rewriter_for(pattern, ast, &replacement)
end

.wrap_source_range(node) ⇒ Object

Fixes initial spaces to print the line since the beginning and fixes end of the expression including heredoc strings.



34
35
36
37
38
39
40
41
# File 'lib/fast/cli.rb', line 34

def wrap_source_range(node)
  expression = node.loc.expression
  Parser::Source::Range.new(
    expression.source_buffer,
    first_position_from_expression(node),
    last_position_from_expression(node) || expression.end_pos
  )
end