Module: Fast

Defined in:
lib/fast.rb,
lib/fast/cli.rb,
lib/fast/version.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

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.2'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.debuggingObject

Returns the value of attribute debugging.



176
177
178
# File 'lib/fast.rb', line 176

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



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

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



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

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

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

Return only captures from a search

Returns:

  • (Array<Object>)

    with all captured elements.

  • (Object)

    with single element when single capture.



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/fast.rb', line 160

def capture(node, pattern)
  res =
    if (match = match?(node, pattern))
      match == true ? node : match
    else
      node.each_child_node
        .flat_map { |child| capture(child, pattern) }
        .compact.flatten
    end
  res&.size == 1 ? res[0] : res
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



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

def capture_file(pattern, file)
  node = ast_from_file(file)
  capture node, pattern
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?(s(:int, 1), [:int, 1])
end


188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/fast.rb', line 188

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



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

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:



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

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

.highlight(node, show_sexp: false) ⇒ Object

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



15
16
17
18
19
20
21
22
23
# File 'lib/fast/cli.rb', line 15

def highlight(node, show_sexp: false)
  output =
    if node.respond_to?(:loc) && !show_sexp
      node.loc.expression.source
    else
      node
    end
  CodeRay.scan(output, :ruby).term
end

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

Verify if a given AST matches with a specific pattern

Examples:

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

Returns:

  • (Boolean)

    case matches ast with the current expression



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

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

.replace(ast, pattern, &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:



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/fast.rb', line 107

def replace(ast, pattern, &replacement)
  buffer = Parser::Source::Buffer.new('replacement')
  buffer.source = ast.loc.expression.source
  to_replace = search(ast, pattern)
  types = to_replace.grep(Parser::AST::Node).map(&:type).uniq
  rewriter = Rewriter.new
  rewriter.buffer = buffer
  rewriter.search = pattern
  rewriter.replacement = replacement
  rewriter.replace_on(*types)
  rewriter.rewrite(buffer, ast)
end

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

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



122
123
124
125
# File 'lib/fast.rb', line 122

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

.report(result, show_sexp: nil, file: nil) ⇒ 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: nil)

    Show string expression instead of source

  • file (String) (defaults to: nil)

    Show the file name and result line before content



32
33
34
35
36
37
38
# File 'lib/fast/cli.rb', line 32

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



206
207
208
209
210
211
212
213
214
215
# File 'lib/fast.rb', line 206

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

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

.search(node, pattern) { ... } ⇒ 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



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

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

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

Search with pattern directly on file

Returns:

  • (Array<Astrolabe::Node>)

    that matches the pattern



129
130
131
132
# File 'lib/fast.rb', line 129

def search_file(pattern, file)
  node = ast_from_file(file)
  search node, pattern
end