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, #reject_keys_from_hash, #strip_brackets

Constructor Details

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

Initialize an Instance.

Parameters:

Yields:

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



16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/synvert/core/rewriter/instance.rb', line 16

def initialize(rewriter, file_path, &block)
  @rewriter = rewriter
  @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 })
  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



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

attr_reader :file_path, :current_node, :query_adapter, :mutation_adapter

#file_pathObject (readonly)

Returns file path.

Returns:

  • file path



37
38
39
# File 'lib/synvert/core/rewriter/instance.rb', line 37

def file_path
  @file_path
end

#mutation_adapterObject (readonly)

Returns the value of attribute mutation_adapter.



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

attr_reader :file_path, :current_node, :query_adapter, :mutation_adapter

#query_adapterObject (readonly)

Returns NodeQuery Adapter.

Returns:

  • NodeQuery Adapter



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

attr_reader :file_path, :current_node, :query_adapter, :mutation_adapter

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



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

def add_action(action)
  @current_mutation.actions << action.process
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.



241
242
243
# File 'lib/synvert/core/rewriter/instance.rb', line 241

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

#delete(*selectors, **options) ⇒ 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.

  • options (Hash)
  • and_comma (Hash)

    a customizable set of options



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

def delete(*selectors, **options)
  @current_mutation.delete(@current_node, *selectors, **options)
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.



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

def goto_node(child_node_name, &block)
  Rewriter::GotoScope.new(self, child_node_name, &block).process
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.



194
195
196
# File 'lib/synvert/core/rewriter/instance.rb', line 194

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

#if_only_exist_node(nql_or_rules, &block) ⇒ Object

It creates a Synvert::Core::Rewriter::IfOnlyExistCondition to check if current node has only one child node and the child node matches, if so, then continue operating on each matching ast node.

Examples:

# it { should matcher }
with_node type: 'block', caller: { message: 'it' } do
  if_only_exist_node type: 'send', receiver: nil, message: 'should' 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.



223
224
225
# File 'lib/synvert/core/rewriter/instance.rb', line 223

def if_only_exist_node(nql_or_rules, &block)
  Rewriter::IfOnlyExistCondition.new(self, nql_or_rules, &block).process
end

#insert(code, at: 'end', to: nil) ⇒ 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.



274
275
276
# File 'lib/synvert/core/rewriter/instance.rb', line 274

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

#insert_after(code, to: nil) ⇒ 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.



288
289
290
291
# File 'lib/synvert/core/rewriter/instance.rb', line 288

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

#insert_before(code, to: nil) ⇒ 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.



303
304
305
306
# File 'lib/synvert/core/rewriter/instance.rb', line 303

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

#nodeParser::AST::Node

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

Returns:



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

def node
  @current_node
end

#noopObject

No operation.



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

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.



259
260
261
# File 'lib/synvert/core/rewriter/instance.rb', line 259

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

#processObject

Process the instance. It finds specified files, for each file, it executes the block code, rewrites the original code, then writes the code back to the original file.



43
44
45
46
47
48
49
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
# File 'lib/synvert/core/rewriter/instance.rb', line 43

def process
  puts @file_path if Configuration.show_run_process

  absolute_file_path = File.join(Configuration.root_path, @file_path)
  while true
    source = read_source(absolute_file_path)
    @current_mutation = NodeMutation.new(source)
    @mutation_adapter = NodeMutation.adapter
    @query_adapter = NodeQuery.adapter
    begin
      node = parse_code(@file_path, source)

      process_with_node(node) do
        instance_eval(&@block)
      rescue NoMethodError => e
        puts [
          "error: #{e.message}",
          "file: #{file_path}",
          "source: #{source}",
          "line: #{current_node.line}"
        ].join("\n")
        raise
      end

      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
      puts "[Warn] file #{file_path} was not parsed correctly."
      # do nothing, iterate next file
    end
  end
end

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

Set current_node to node and process.

Parameters:

Yields:

  • process



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

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:

Yields:

  • process



135
136
137
138
139
140
# File 'lib/synvert/core/rewriter/instance.rb', line 135

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

#remove(**options) ⇒ Object

It removes current node.

Examples:

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

Parameters:

  • options (Hash)

    options.

  • and_comma (Hash)

    a customizable set of options



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

def remove(**options)
  @current_mutation.remove(@current_node, **options)
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.



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

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


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

def replace_erb_stmt_with_expr
  @current_mutation.actions << Rewriter::ReplaceErbStmtWithExprAction.new(@current_node).process
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.



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

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

#testObject

Test the instance. It finds specified files, for each file, it executes the block code, tests the original code, then returns the actions.



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

def test
  absolute_file_path = File.join(Configuration.root_path, file_path)
  source = read_source(absolute_file_path)
  @current_mutation = NodeMutation.new(source)
  @mutation_adapter = NodeMutation.adapter
  @query_adapter = NodeQuery.adapter
  begin
    node = parse_code(file_path, source)

    process_with_node(node) do
      instance_eval(&@block)
    rescue NoMethodError => e
      puts [
        "error: #{e.message}",
        "file: #{file_path}",
        "source: #{source}",
        "line: #{current_node.line}"
      ].join("\n")
      raise
    end

    result = @current_mutation.test
    result.file_path = file_path
    result
  rescue Parser::SyntaxError
    puts "[Warn] file #{file_path} was not parsed correctly."
    # do nothing, iterate next file
  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.



208
209
210
# File 'lib/synvert/core/rewriter/instance.rb', line 208

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.



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

def warn(message)
  @rewriter.add_warning Rewriter::Warning.new(self, 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.



161
162
163
164
165
# File 'lib/synvert/core/rewriter/instance.rb', line 161

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

#wrap(with:) ⇒ Object

It wraps current node with code.

Examples:

# class Foobar
# end
# =>
# module Synvert
#   class Foobar
#   end
# end
within_node type: 'class' do
  wrap with: 'module Synvert'
end

Parameters:

  • with (String)

    code need to be wrapped with.



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

def wrap(with:)
  @current_mutation.wrap(@current_node, with: with)
end