lib/refactor.rb in refactor-0.0.1 vs lib/refactor.rb in refactor-0.1.0
- old
+ new
@@ -1,68 +1,86 @@
-require 'active_support/core_ext/string/inflections'
-require 'fileutils'
+# frozen_string_literal: true
require_relative 'refactor/version'
+# Eventually we may consider dropping RuboCop and working more directly
+# on top of Parser itself
+require 'rubocop'
+
module Refactor
- def run(from, to)
- from_camelized = from.camelize
- from_dashed = from.dasherize
- from_humanized = from.humanize
- from_underscored = from.underscore
- to_camelized = to.camelize
- to_dashed = to.dasherize
- to_underscored = to.underscore
+ # Utilities for working with ASTs
+ module Util
+ def self.deconstruct_ast(string)
+ deep_deconstruct(ast_from(string))
+ end
- camelized_regex = /(?<=\b|_)#{Regexp.quote(from_camelized)}(?=\b|_)/
- dashed_regex = /(?<=\b|_)#{Regexp.quote(from_dashed)}(?=\b|_)/
- underscored_regex = /(?<=\b|_)#{Regexp.quote(from_underscored)}(?=\b|_)/
+ # Makes it easier to break down an AST into what we'd like to match against
+ # eventually.
+ def self.deep_deconstruct(node)
+ return node unless node.respond_to?(:deconstruct)
- # all files in current directory
- Dir.glob('**/*') do |old_path|
- # ignore certain directories
- unless old_path =~ %r{\A(coverage|pkg|tmp|vendor)(\z|/)}
- # only check the basename so that the directory doesn't get renamed twice
- old_basename = File.basename(old_path)
- new_basename = old_basename.dup
- new_basename.gsub!(dashed_regex, to_dashed)
- new_basename.gsub!(underscored_regex, to_underscored)
+ node.deconstruct.map { deep_deconstruct(_1) }
+ end
- if new_basename == old_basename
- # no change
- new_path = old_path
- else
- # rename file
- new_path = File.join(File.dirname(old_path), new_basename)
- puts "#{old_path} –> #{new_path}"
- FileUtils.mv(old_path, new_path)
- end
+ # Convert a string into its AST representation
+ def self.ast_from(string)
+ processed_source_from(string).ast
+ end
- if File.file?(new_path)
- # replace within file
- old_text = File.read(new_path)
+ def self.processed_source_from(string)
+ RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f)
+ end
+ end
- new_text = old_text.dup
- new_text.gsub!(camelized_regex, to_camelized)
- new_text.gsub!(dashed_regex, to_dashed)
- new_text.gsub!(underscored_regex, to_underscored)
+ # Wrapper for rule processors to simplify the code
+ # needed to run one.
+ class Rule < Parser::AST::Processor
+ include RuboCop::AST::Traversal
- unless new_text == old_text
- # rewrite existing file
- File.write(new_path, new_text)
- end
+ protected attr_reader :rewriter
- # show possible matches in body
- line_num = 0
- new_text.each_line do |old_line|
- new_line = old_line.gsub(/#{Regexp.quote(from_humanized)}/i, "\e[33m\\0\e[0m")
- unless new_line == old_line
- puts "\e[36m#{new_path}:#{line_num}\e[0m #{new_line}"
- end
- line_num += 1
- end
- end
+ def initialize(rewriter)
+ @rewriter = rewriter
+ super()
+ end
+
+ def self.process(string)
+ Rewriter.new(rules: [self]).process(string)
+ end
+
+ def process_regular_node(node)
+ return matches(node) if defined?(matches)
+
+ super()
+ end
+
+ protected def replace(node, new_code)
+ rewriter.replace(node.loc.expression, new_code)
+ end
+ end
+
+ # Full rewriter, typically used for processing multiple rules
+ class Rewriter
+ def initialize(rules: [])
+ @rules = rules
+ end
+
+ def process(string)
+ # No sense in processing anything if there's nothing to apply it to
+ return string if @rules.empty?
+
+ source = Util.processed_source_from(string)
+ ast = source.ast
+
+ source_buffer = source.buffer
+
+ rewriter = Parser::Source::TreeRewriter.new(source_buffer)
+
+ @rules.each do |rule_class|
+ rule = rule_class.new(rewriter)
+ ast.each_node { |node| rule.process(node) }
end
+
+ rewriter.process
end
end
- module_function :run
end