# frozen_string_literal: true module Synvert::Core # 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 contains one or many [Synvert::Core::Rewriter::Scope] and [Synvert::Rewriter::Condition]. class Rewriter::Instance include Rewriter::Helper class < 0 @actions.sort_by! { |action| action.send(@options[:sort_by]) } conflict_actions = get_conflict_actions @actions.reverse_each do |action| source[action.begin_pos...action.end_pos] = action.rewritten_code source = remove_code_or_whole_line(source, action.line) end @actions = [] self.class.write_file(file_path, source) end rescue Parser::SyntaxError puts "[Warn] file #{file_path} was not parsed correctly." # do nothing, iterate next file end while !conflict_actions.empty? end end end # Gets current node, it allows to get current node in block code. # # @return [Parser::AST::Node] def node @current_node end # Set current_node to node and process. # # @param node [Parser::AST::Node] node set to current_node # @yield process def process_with_node(node) self.current_node = node yield self.current_node = node end # Set current_node properly, process and set current_node back to original current_node. # # @param node [Parser::AST::Node] node set to current_node # @yield process def process_with_other_node(node) original_node = self.current_node self.current_node = node yield self.current_node = original_node end ####### # DSL # ####### # Parse within_node dsl, it creates a [Synvert::Core::Rewriter::WithinScope] to find recursive matching ast nodes, # then continue operating on each matching ast node. # # @param rules [Hash] rules to find mathing ast nodes. # @param block [Block] block code to continue operating on the matching nodes. def within_node(rules, &block) Rewriter::WithinScope.new(self, rules, { recursive: true }, &block).process end alias_method :with_node, :within_node # Parse within_direct_node dsl, it creates a [Synvert::Core::Rewriter::WithinScope] to find direct matching ast nodes, # then continue operating on each matching ast node. # # @param rules [Hash] rules to find mathing ast nodes. # @param block [Block] block code to continue operating on the matching nodes. def within_direct_node(rules, &block) Rewriter::WithinScope.new(self, rules, { recursive: false }, &block).process end alias_method :with_direct_node, :within_direct_node # Parse goto_node dsl, it creates a [Synvert::Core::Rewriter::GotoScope] to go to a child node, # then continue operating on the child node. # # @param child_node_name [String] the name of the child node. # @param block [Block] block code to continue operating on the matching nodes. def goto_node(child_node_name, &block) Rewriter::GotoScope.new(self, child_node_name, &block).process end # Parse if_exist_node dsl, 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. # # @param rules [Hash] rules to check mathing ast nodes. # @param block [Block] block code to continue operating on the matching nodes. def if_exist_node(rules, &block) Rewriter::IfExistCondition.new(self, rules, &block).process end # Parse unless_exist_node dsl, it creates a [Synvert::Core::Rewriter::UnlessExistCondition] to check # if matching nodes doesn't exist in the child nodes, if so, then continue operating on each matching ast node. # # @param rules [Hash] rules to check mathing ast nodes. # @param block [Block] block code to continue operating on the matching nodes. def unless_exist_node(rules, &block) Rewriter::UnlessExistCondition.new(self, rules, &block).process end # Parse if_only_exist_node dsl, it creates a [Synvert::Core::Rewriter::IfOnlyExistCondition] to check # if current node has only one child node and the child node matches rules, # if so, then continue operating on each matching ast node. # # @param rules [Hash] rules to check mathing ast nodes. # @param block [Block] block code to continue operating on the matching nodes. def if_only_exist_node(rules, &block) Rewriter::IfOnlyExistCondition.new(self, rules, &block).process end # Parse append dsl, it creates a [Synvert::Core::Rewriter::AppendAction] to # append the code to the bottom of current node body. # # @param code [String] code need to be appended. # @param options [Hash] action options. def append(code, options={}) @actions << Rewriter::AppendAction.new(self, code, options) end # Parse insert dsl, it creates a [Synvert::Core::Rewriter::InsertAction] to # insert the code to the top of current node body. # # @param code [String] code need to be inserted. # @param options [Hash] action options. def insert(code, options={}) @actions << Rewriter::InsertAction.new(self, code, options) end # Parse insert_after dsl, it creates a [Synvert::Core::Rewriter::InsertAfterAction] to # insert the code next to the current node. # # @param code [String] code need to be inserted. # @param options [Hash] action options. def insert_after(node, options={}) @actions << Rewriter::InsertAfterAction.new(self, node, options) end # Parse replace_with dsl, it creates a [Synvert::Core::Rewriter::ReplaceWithAction] to # replace current node with code. # # @param code [String] code need to be replaced with. # @param options [Hash] action options. def replace_with(code, options={}) @actions << Rewriter::ReplaceWithAction.new(self, code, options) end # Parse replace_erb_stmt_with_expr dsl, it creates a [Synvert::Core::Rewriter::ReplaceErbStmtWithExprAction] to # replace erb stmt code to expr code. def replace_erb_stmt_with_expr @actions << Rewriter::ReplaceErbStmtWithExprAction.new(self) end # Parse remove dsl, it creates a [Synvert::Core::Rewriter::RemoveAction] to current node. def remove @actions << Rewriter::RemoveAction.new(self) end # Parse warn dsl, it creates a [Synvert::Core::Rewriter::Warning] to save warning message. # # @param message [String] warning message. def warn(message) @rewriter.add_warning Rewriter::Warning.new(self, message) end private # It changes source code from bottom to top, and it can change source code twice at the same time, # So if there is an overlap between two actions, it removes the conflict actions and operate them in the next loop. def get_conflict_actions i = @actions.length - 1 j = i - 1 conflict_actions = [] return if i < 0 begin_pos = @actions[i].begin_pos while j > -1 if begin_pos <= @actions[j].end_pos conflict_actions << @actions.delete_at(j) else i = j begin_pos = @actions[i].begin_pos end j -= 1 end conflict_actions end # It checks if code is removed and that line is empty. # # @param source [String] source code of file # @param line [String] the line number def remove_code_or_whole_line(source, line) newline_at_end_of_line = source[-1] == "\n" source_arr = source.split("\n") if source_arr[line - 1] && source_arr[line - 1].strip.empty? source_arr.delete_at(line - 1) if source_arr[line - 2] && source_arr[line - 2].strip.empty? && source_arr[line - 1] && source_arr[line - 1].strip.empty? source_arr.delete_at(line - 1) end source_arr.join("\n") + (newline_at_end_of_line ? "\n" : '') else source end end end end