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.

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.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/synvert/core/rewriter/instance.rb', line 23

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



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

attr_reader :file_path, :current_parser

#current_parserObject (readonly)

Returns the value of attribute current_parser.



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

attr_reader :file_path, :current_parser

#file_pathObject (readonly)

Returns file path.

Returns:

  • file path



44
45
46
# File 'lib/synvert/core/rewriter/instance.rb', line 44

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



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

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



437
438
439
# File 'lib/synvert/core/rewriter/instance.rb', line 437

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.



234
235
236
# File 'lib/synvert/core/rewriter/instance.rb', line 234

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.



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

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.



371
372
373
# File 'lib/synvert/core/rewriter/instance.rb', line 371

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.



188
189
190
# File 'lib/synvert/core/rewriter/instance.rb', line 188

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


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

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.



202
203
204
# File 'lib/synvert/core/rewriter/instance.rb', line 202

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.



460
461
462
# File 'lib/synvert/core/rewriter/instance.rb', line 460

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.



268
269
270
# File 'lib/synvert/core/rewriter/instance.rb', line 268

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.



284
285
286
287
# File 'lib/synvert/core/rewriter/instance.rb', line 284

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.



301
302
303
304
# File 'lib/synvert/core/rewriter/instance.rb', line 301

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)


125
126
127
# File 'lib/synvert/core/rewriter/instance.rb', line 125

def mutation_adapter
  @current_mutation.adapter
end

#nodeNode

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

Returns:

  • (Node)


118
119
120
# File 'lib/synvert/core/rewriter/instance.rb', line 118

def node
  @current_node
end

#noopObject

No operation.



395
396
397
# File 'lib/synvert/core/rewriter/instance.rb', line 395

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.



252
253
254
# File 'lib/synvert/core/rewriter/instance.rb', line 252

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.



50
51
52
53
54
55
56
57
58
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
# File 'lib/synvert/core/rewriter/instance.rb', line 50

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



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

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



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

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.



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

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.



347
348
349
# File 'lib/synvert/core/rewriter/instance.rb', line 347

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


316
317
318
319
320
321
# File 'lib/synvert/core/rewriter/instance.rb', line 316

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.



332
333
334
# File 'lib/synvert/core/rewriter/instance.rb', line 332

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.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/synvert/core/rewriter/instance.rb', line 89

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.



216
217
218
# File 'lib/synvert/core/rewriter/instance.rb', line 216

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.



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

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.



169
170
171
172
173
# File 'lib/synvert/core/rewriter/instance.rb', line 169

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



390
391
392
# File 'lib/synvert/core/rewriter/instance.rb', line 390

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



444
445
446
447
448
449
450
451
452
453
# File 'lib/synvert/core/rewriter/instance.rb', line 444

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