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