Class: Synvert::Core::Rewriter::Instance

Inherits:
Object
  • Object
show all
Includes:
Helper
Defined in:
lib/synvert/core/rewriter/instance.rb

Overview

Instance is an execution unit, it finds specified ast nodes, checks if the nodes match some conditions, then add, replace or remove code.

One instance can contain one or many Scope and Condition.

Constant Summary collapse

DSL_METHODS =
%i[
  within_node with_node find_node goto_node
  if_exist_node unless_exist_node
  append prepend insert insert_after insert_before replace delete remove wrap replace_with warn replace_erb_stmt_with_expr noop group add_action
  add_callback
  wrap_with_quotes add_leading_spaces
  file_path node mutation_adapter
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Helper

#add_arguments_with_parenthesis_if_necessary, #add_curly_brackets_if_necessary, #add_receiver_if_necessary, #strip_brackets

Constructor Details

#initialize(rewriter, file_path) { ... } ⇒ Instance

Initialize an Instance.

Parameters:

Yields:

  • block code to find nodes, match conditions and rewrite code.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/synvert/core/rewriter/instance.rb', line 32

def initialize(rewriter, file_path, &block)
  @rewriter = rewriter
  @current_parser = @rewriter.parser
  @current_visitor = NodeVisitor.new(adapter: @current_parser)
  @actions = []
  @file_path = file_path
  @block = block
  strategy = NodeMutation::Strategy::KEEP_RUNNING
  if rewriter.options[:strategy] == Strategy::ALLOW_INSERT_AT_SAME_POSITION
    strategy |= NodeMutation::Strategy::ALLOW_INSERT_AT_SAME_POSITION
  end
  NodeMutation.configure({ strategy: strategy, tab_width: Configuration.tab_width })
  rewriter.helpers.each { |helper| singleton_class.send(:define_method, helper[:name], &helper[:block]) }
end

Instance Attribute Details

#current_nodeObject

Returns current ast node.

Returns:

  • current ast node



53
# File 'lib/synvert/core/rewriter/instance.rb', line 53

attr_reader :file_path, :current_parser

#current_parserObject (readonly)

Returns the value of attribute current_parser.



53
# File 'lib/synvert/core/rewriter/instance.rb', line 53

attr_reader :file_path, :current_parser

#file_pathObject (readonly)

Returns file path.

Returns:

  • file path



53
54
55
# File 'lib/synvert/core/rewriter/instance.rb', line 53

def file_path
  @file_path
end

Instance Method Details

#add_action(action) ⇒ Object

Add a custom action.

Examples:

remover_action = NodeMutation::RemoveAction.new(node)
add_action(remover_action)

Parameters:

  • action (Synvert::Core::Rewriter::Action)

    action



423
424
425
# File 'lib/synvert/core/rewriter/instance.rb', line 423

def add_action(action)
  @current_mutation.actions << action.process
end

#add_callback(node_type, at: 'start') { ... } ⇒ Object

It adds a callback when visiting an ast node.

Examples:

add_callback :class, at: 'start' do |node|
  # do something when visiting class node
end

Parameters:

  • node_type (Symbol)

    node type

  • at (String) (defaults to: 'start')

    at start or end

Yields:

  • block code to run when visiting the node



446
447
448
# File 'lib/synvert/core/rewriter/instance.rb', line 446

def add_callback(node_type, at: 'start', &block)
  @current_visitor.add_callback(node_type, at: at, &block)
end

#append(code) ⇒ Object

It appends the code to the bottom of current node body.

Examples:

# def teardown
#   clean_something
# end
# =>
# def teardown
#   clean_something
#   super
# end
with_node type: 'def', name: 'steardown' do
  append 'super'
end

Parameters:

  • code (String)

    code need to be appended.



243
244
245
# File 'lib/synvert/core/rewriter/instance.rb', line 243

def append(code)
  @current_mutation.append(@current_node, code)
end

#dedent(source, tab_size: 1) ⇒ String

Dedents the given source code by removing leading spaces or tabs.

Parameters:

  • source (String)

    The source code to dedent.

  • tab_size (Integer) (defaults to: 1)

    The number of spaces per tab (default is 1).

Returns:

  • (String)

    The dedented source code.



478
479
480
# File 'lib/synvert/core/rewriter/instance.rb', line 478

def dedent(source, tab_size: 1)
  source.each_line.map { |line| line.sub(/^ {#{NodeMutation.tab_width * tab_size}}/, '') }.join
end

#delete(*selectors, and_comma: false) ⇒ Object

It deletes child nodes.

Examples:

# FactoryBot.create(...)
# =>
# create(...)
with_node type: 'send', receiver: 'FactoryBot', message: 'create' do
  delete :receiver, :dot
end

Parameters:

  • selectors (Array<Symbol>)

    selector names of child node.

  • and_comma (Hash) (defaults to: false)

    a customizable set of options

Options Hash (and_comma:):

  • delete (Boolean)

    extra comma.



380
381
382
# File 'lib/synvert/core/rewriter/instance.rb', line 380

def delete(*selectors, and_comma: false)
  @current_mutation.delete(@current_node, *selectors, and_comma: and_comma)
end

#goto_node(child_node_name, &block) ⇒ Object

It creates a GotoScope to go to a child node, then continue operating on the child node.

Examples:

# head status: 406
with_node type: 'send', receiver: nil, message: 'head', arguments: { size: 1, first: { type: 'hash' } } do
  goto_node 'arguments.first' do
  end
end

Parameters:

  • child_node_name (Symbol|String)

    the name of the child nodes.

  • block (Block)

    block code to continue operating on the matching nodes.



197
198
199
# File 'lib/synvert/core/rewriter/instance.rb', line 197

def goto_node(child_node_name, &block)
  Rewriter::GotoScope.new(self, child_node_name, &block).process
end

#group(&block) ⇒ Object

Group actions.

Examples:

group do
  delete :message, :dot
  replace 'receiver.caller.message', with: 'flat_map'
end


414
415
416
# File 'lib/synvert/core/rewriter/instance.rb', line 414

def group(&block)
  @current_mutation.group(&block)
end

#if_exist_node(nql_or_rules, &block) ⇒ Object

It creates a Synvert::Core::Rewriter::IfExistCondition to check if matching nodes exist in the child nodes, if so, then continue operating on each matching ast node.

Examples:

# Klass.any_instance.stub(:message)
with_node type: 'send', message: 'stub', arguments: { first: { type: { not: 'hash' } } } do
  if_exist_node type: 'send', message: 'any_instance' do
  end
end

Parameters:

  • nql_or_rules (String|Hash)

    nql or rules to check mathing ast nodes.

  • block (Block)

    block code to continue operating on the matching nodes.



211
212
213
# File 'lib/synvert/core/rewriter/instance.rb', line 211

def if_exist_node(nql_or_rules, &block)
  Rewriter::IfExistCondition.new(self, nql_or_rules, &block).process
end

#indent(source, tab_size: 1) ⇒ String

Indents the given source code by the specified tab size.

Parameters:

  • source (String)

    The source code to be indented.

  • tab_size (Integer) (defaults to: 1)

    The number of spaces per tab.

Returns:

  • (String)

    The indented source code.



469
470
471
# File 'lib/synvert/core/rewriter/instance.rb', line 469

def indent(source, tab_size: 1)
  source.each_line.map { |line| (' ' * NodeMutation.tab_width * tab_size) + line }.join
end

#insert(code, at: 'end', to: nil, and_comma: false) ⇒ Object

It inserts code.

Examples:

# open('http://test.com')
# =>
# URI.open('http://test.com')
with_node type: 'send', receiver: nil, message: 'open' do
  insert 'URI.', at: 'beginning'
end

Parameters:

  • code (String)

    code need to be inserted.

  • at (String) (defaults to: 'end')

    insert position, beginning or end

  • to (String) (defaults to: nil)

    where to insert, if it is nil, will insert to current node.

  • and_comma (Boolean) (defaults to: false)

    insert extra comma.



277
278
279
# File 'lib/synvert/core/rewriter/instance.rb', line 277

def insert(code, at: 'end', to: nil, and_comma: false)
  @current_mutation.insert(@current_node, code, at: at, to: to, and_comma: and_comma)
end

#insert_after(code, to: nil, and_comma: false) ⇒ Object

It inserts the code next to the current node.

Examples:

# Synvert::Application.config.secret_token = "0447aa931d42918bfb934750bb78257088fb671186b5d1b6f9fddf126fc8a14d34f1d045cefab3900751c3da121a8dd929aec9bafe975f1cabb48232b4002e4e"
# =>
# Synvert::Application.config.secret_token = "0447aa931d42918bfb934750bb78257088fb671186b5d1b6f9fddf126fc8a14d34f1d045cefab3900751c3da121a8dd929aec9bafe975f1cabb48232b4002e4e"
# Synvert::Application.config.secret_key_base = "bf4f3f46924ecd9adcb6515681c78144545bba454420973a274d7021ff946b8ef043a95ca1a15a9d1b75f9fbdf85d1a3afaf22f4e3c2f3f78e24a0a188b581df"
with_node type: 'send', message: 'secret_token=' do
  insert_after "{{receiver}}.secret_key_base = \"#{SecureRandom.hex(64)}\""
end

Parameters:

  • code (String)

    code need to be inserted.

  • to (String) (defaults to: nil)

    where to insert, if it is nil, will insert to current node.

  • and_comma (Boolean) (defaults to: false)

    insert extra comma.



293
294
295
296
# File 'lib/synvert/core/rewriter/instance.rb', line 293

def insert_after(code, to: nil, and_comma: false)
  column = ' ' * @current_mutation.adapter.get_start_loc(@current_node, to).column
  @current_mutation.insert(@current_node, "\n#{column}#{code}", at: 'end', to: to, and_comma: and_comma)
end

#insert_before(code, to: nil, and_comma: false) ⇒ Object

It inserts the code previous to the current node.

Examples:

# Synvert::Application.config.secret_token = "0447aa931d42918bfb934750bb78257088fb671186b5d1b6f9fddf126fc8a14d34f1d045cefab3900751c3da121a8dd929aec9bafe975f1cabb48232b4002e4e"
# =>
# Synvert::Application.config.secret_key_base = "bf4f3f46924ecd9adcb6515681c78144545bba454420973a274d7021ff946b8ef043a95ca1a15a9d1b75f9fbdf85d1a3afaf22f4e3c2f3f78e24a0a188b581df"
# Synvert::Application.config.secret_token = "0447aa931d42918bfb934750bb78257088fb671186b5d1b6f9fddf126fc8a14d34f1d045cefab3900751c3da121a8dd929aec9bafe975f1cabb48232b4002e4e"
with_node type: 'send', message: 'secret_token=' do
  insert_before "{{receiver}}.secret_key_base = \"#{SecureRandom.hex(64)}\""
end

Parameters:

  • code (String)

    code need to be inserted.

  • to (String) (defaults to: nil)

    where to insert, if it is nil, will insert to current node.

  • and_comma (Boolean) (defaults to: false)

    insert extra comma.



310
311
312
313
# File 'lib/synvert/core/rewriter/instance.rb', line 310

def insert_before(code, to: nil, and_comma: false)
  column = ' ' * @current_mutation.adapter.get_start_loc(@current_node, to).column
  @current_mutation.insert(@current_node, "#{code}\n#{column}", at: 'beginning', to: to, and_comma: and_comma)
end

#mutation_adapterNodeMutation::Adapter

Get current_mutation's adapter.

Returns:

  • (NodeMutation::Adapter)


134
135
136
# File 'lib/synvert/core/rewriter/instance.rb', line 134

def mutation_adapter
  @current_mutation.adapter
end

#nodeNode

Gets current node, it allows to get current node in block code.

Returns:

  • (Node)


127
128
129
# File 'lib/synvert/core/rewriter/instance.rb', line 127

def node
  @current_node
end

#noopObject

No operation.



404
405
406
# File 'lib/synvert/core/rewriter/instance.rb', line 404

def noop
  @current_mutation.noop(@current_node)
end

#prepend(code) ⇒ Object

It prepends the code to the top of current node body.

Examples:

# def setup
#   do_something
# end
# =>
# def setup
#   super
#   do_something
# end
with_node type: 'def', name: 'setup' do
  prepend 'super'
end

Parameters:

  • code (String)

    code need to be prepended.



261
262
263
# File 'lib/synvert/core/rewriter/instance.rb', line 261

def prepend(code)
  @current_mutation.prepend(@current_node, code)
end

#processObject

Process the instance. It executes the block code, rewrites the original code, then writes the code back to the original file.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/synvert/core/rewriter/instance.rb', line 59

def process
  puts @file_path if Configuration.show_run_process

  absolute_file_path = File.join(Configuration.root_path, @file_path)
  # It keeps running until no conflict,
  # it will try 5 times at maximum.
  5.times do
    source = read_source(absolute_file_path)
    encoded_source = Engine.encode(File.extname(file_path), source)
    @current_mutation = NodeMutation.new(source, adapter: @current_parser)
    @current_mutation.transform_proc = Engine.generate_transform_proc(File.extname(file_path), encoded_source)
    begin
      node = parse_code(@file_path, encoded_source)

      process_with_node(node) do
        instance_eval(&@block)
      end

      @current_visitor.visit(node, self)

      result = @current_mutation.process
      if result.affected?
        @rewriter.add_affected_file(file_path)
        write_source(absolute_file_path, result.new_source)
      end
      break unless result.conflicted?
    rescue Parser::SyntaxError, Prism::ParseError, SyntaxTree::Parser::ParseError => e
      if ENV['DEBUG'] == 'true'
        puts "[Warn] file #{file_path} was not parsed correctly."
        puts e.message
      end
      break
    end
  end
end

#process_with_node(node) { ... } ⇒ Object

Set current_node to node and process.

Parameters:

  • node (Node)

    node set to current_node

Yields:

  • process



142
143
144
145
146
# File 'lib/synvert/core/rewriter/instance.rb', line 142

def process_with_node(node)
  self.current_node = node
  yield
  self.current_node = node
end

#process_with_other_node(node) { ... } ⇒ Object

Set current_node properly, process and set current_node back to original current_node.

Parameters:

  • node (Node)

    node set to other_node

Yields:

  • process



152
153
154
155
156
157
# File 'lib/synvert/core/rewriter/instance.rb', line 152

def process_with_other_node(node)
  original_node = current_node
  self.current_node = node
  yield
  self.current_node = original_node
end

#remove(and_comma: false) ⇒ Object

It removes current node.

Examples:

with_node type: 'send', message: { in: %w[puts p] } do
  remove
end

Parameters:

  • and_comma (Hash) (defaults to: false)

    a customizable set of options

Options Hash (and_comma:):

  • delete (Boolean)

    extra comma.



366
367
368
# File 'lib/synvert/core/rewriter/instance.rb', line 366

def remove(and_comma: false)
  @current_mutation.remove(@current_node, and_comma: and_comma)
end

#replace(*selectors, with:) ⇒ Object

It replaces the code of specified child nodes.

Examples:

# assert(object.empty?)
# =>
# assert_empty(object)
with_node type: 'send', receiver: nil, message: 'assert', arguments: { size: 1, first: { type: 'send', message: 'empty?', arguments: { size: 0 } } } do
  replace :message, with: 'assert_empty'
  replace :arguments, with: '{{arguments.first.receiver}}'
end

Parameters:

  • selectors (Array<Symbol>)

    selector names of child node.

  • with (String)

    code need to be replaced with.



356
357
358
# File 'lib/synvert/core/rewriter/instance.rb', line 356

def replace(*selectors, with:)
  @current_mutation.replace(@current_node, *selectors, with: with)
end

#replace_erb_stmt_with_exprObject

It replaces erb stmt code to expr code.

Examples:

# <% form_for post do |f| %>
# <% end %>
# =>
# <%= form_for post do |f| %>
# <% end %>
with_node type: 'block', caller: { type: 'send', receiver: nil, message: 'form_for' } do
  replace_erb_stmt_with_expr
end


325
326
327
328
329
330
# File 'lib/synvert/core/rewriter/instance.rb', line 325

def replace_erb_stmt_with_expr
  absolute_file_path = File.join(Configuration.root_path, @file_path)
  erb_source = read_source(absolute_file_path)
  action = Rewriter::ReplaceErbStmtWithExprAction.new(@current_node, erb_source, adapter: @current_mutation.adapter)
  add_action(action)
end

#replace_with(code) ⇒ Object

It replaces the whole code of current node.

Examples:

# obj.stub(:foo => 1, :bar => 2)
# =>
# allow(obj).to receive_messages(:foo => 1, :bar => 2)
with_node type: 'send', message: 'stub', arguments: { first: { type: 'hash' } } do
  replace_with 'allow({{receiver}}).to receive_messages({{arguments}})'
end

Parameters:

  • code (String)

    code need to be replaced with.



341
342
343
# File 'lib/synvert/core/rewriter/instance.rb', line 341

def replace_with(code)
  @current_mutation.replace_with(@current_node, code)
end

#testObject

Test the instance. It executes the block code, tests the original code, then returns the actions.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/synvert/core/rewriter/instance.rb', line 98

def test
  absolute_file_path = File.join(Configuration.root_path, file_path)
  source = read_source(absolute_file_path)
  @current_mutation = NodeMutation.new(source, adapter: @current_parser)
  encoded_source = Engine.encode(File.extname(file_path), source)
  @current_mutation.transform_proc = Engine.generate_transform_proc(File.extname(file_path), encoded_source)
  begin
    node = parse_code(file_path, encoded_source)

    process_with_node(node) do
      instance_eval(&@block)
    end

    @current_visitor.visit(node, self)

    result = Configuration.test_result == 'new_source' ? @current_mutation.process : @current_mutation.test
    result.file_path = file_path
    result
  rescue Parser::SyntaxError, Prism::ParseError, SyntaxTree::Parser::ParseError => e
    if ENV['DEBUG'] == 'true'
      puts "[Warn] file #{file_path} was not parsed correctly."
      puts e.message
    end
  end
end

#unless_exist_node(nql_or_rules, &block) ⇒ Object

It creates a UnlessExistCondition to check if matching nodes doesn't exist in the child nodes, if so, then continue operating on each matching ast node.

Examples:

# obj.stub(:message)
with_node type: 'send', message: 'stub', arguments: { first: { type: { not: 'hash' } } } do
  unless_exist_node type: 'send', message: 'any_instance' do
  end
end

Parameters:

  • nql_or_rules (String|Hash)

    nql or rules to check mathing ast nodes.

  • block (Block)

    block code to continue operating on the matching nodes.



225
226
227
# File 'lib/synvert/core/rewriter/instance.rb', line 225

def unless_exist_node(nql_or_rules, &block)
  Rewriter::UnlessExistCondition.new(self, nql_or_rules, &block).process
end

#warn(message) ⇒ Object

It creates a Warning to save warning message.

Examples:

within_files 'vendor/plugins' do
  warn 'Rails::Plugin is deprecated and will be removed in Rails 4.0. Instead of adding plugins to vendor/plugins use gems or bundler with path or git dependencies.'
end

Parameters:

  • message (String)

    warning message.



433
434
435
436
# File 'lib/synvert/core/rewriter/instance.rb', line 433

def warn(message)
  line = @current_mutation.adapter.get_start_loc(@current_node).line
  @rewriter.add_warning Rewriter::Warning.new(@file_path, line, message)
end

#within_node(nql_or_rules, options = {}) { ... } ⇒ Object Also known as: with_node, find_node

It creates a WithinScope to recursively find matching ast nodes, then continue operating on each matching ast node.

Examples:

# matches User.find_by_login('test')
with_node type: 'send', message: /^find_by_/ do
end
# matches FactoryBot.create(:user)
with_node '.send[receiver=FactoryBot][message=create][arguments.size=1]' do
end

Parameters:

  • nql_or_rules (String|Hash)

    nql or rules to find mathing ast nodes.

  • options (Hash) (defaults to: {})

    optional

  • including_self (Hash)

    a customizable set of options

  • stop_at_first_match (Hash)

    a customizable set of options

  • recursive (Hash)

    a customizable set of options

Yields:

  • run on the matching nodes.



178
179
180
181
182
# File 'lib/synvert/core/rewriter/instance.rb', line 178

def within_node(nql_or_rules, options = {}, &block)
  Rewriter::WithinScope.new(self, nql_or_rules, options, &block).process
rescue NodeQueryLexer::ScanError, Racc::ParseError
  raise NodeQuery::Compiler::ParseError, "Invalid query string: #{nql_or_rules}"
end

#wrap(prefix:, suffix:, newline: false) ⇒ Object

It wraps current node with prefix and suffix code.

Examples:

# class Foobar
# end
# =>
# module Synvert
#   class Foobar
#   end
# end
within_node type: 'class' do
  wrap prefix: 'module Synvert', suffix: 'end', newline: true
end

Parameters:

  • prefix (String)

    prefix code need to be wrapped with.

  • suffix (String)

    suffix code need to be wrapped with.

  • newline (Boolean) (defaults to: false)

    if wrap code in newline, default is false



399
400
401
# File 'lib/synvert/core/rewriter/instance.rb', line 399

def wrap(prefix:, suffix:, newline: false)
  @current_mutation.wrap(@current_node, prefix: prefix, suffix: suffix, newline: newline)
end

#wrap_with_quotes(str) ⇒ String

Wrap str string with single or doulbe quotes based on Configuration.single_quote.

Parameters:

  • str (String)

Returns:

  • (String)

    quoted string



453
454
455
456
457
458
459
460
461
462
# File 'lib/synvert/core/rewriter/instance.rb', line 453

def wrap_with_quotes(str)
  quote = Configuration.single_quote ? "'" : '"';
  another_quote = Configuration.single_quote ? '"' : "'";
  if str.include?(quote) && !str.include?(another_quote)
    return "#{another_quote}#{str}#{another_quote}"
  end

  escaped_str = str.gsub(quote) { |_char| '\\' + quote }
  quote + escaped_str + quote
end