Skip to content

Commit

Permalink
Runner and CLI basics
Browse files Browse the repository at this point in the history
  • Loading branch information
baweaver committed May 6, 2024
1 parent 142f357 commit dce251d
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@

# rspec failure tracking
.rspec_status

*.gem
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Metrics/BlockLength:
Enabled: false
Metrics/MethodLength:
Enabled: false
Style/StringLiterals:
Enabled: false
Style/AccessModifierDeclarations:
Enabled: false
Layout/MultilineMethodCallIndentation:
Enabled: false
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
## [Unreleased]

## [0.1.0] - 2024-02-27
- Added `Runner` and `refactor` binary
- Added colored diffing to `Runner` and `OptionParser` to CLI

## [0.1.0] - 2024-05-05

- Gem transferred to @baweaver
- Initial base of Refactor allowing for multiple rule rewrites
- Initial utilities set for dealing with ASTs

## [0.0.1] - 2014-06-12

Expand Down
4 changes: 3 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
PATH
remote: .
specs:
refactor (0.1.0)
refactor (0.1.1)
colorize
rubocop

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
coderay (1.1.3)
colorize (1.1.0)
diff-lcs (1.5.1)
ffi (1.16.3)
formatador (1.1.0)
Expand Down
29 changes: 29 additions & 0 deletions bin/refactor
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative File.join('..', 'lib', 'refactor')
require 'optparse'

@options = {}

OptionParser.new do |opts|
opts.on('-rRULES', '--rules=RULES', 'Directory to load rules from') do |rule_directory|
@options[:rule_directory] = rule_directory
end

opts.on('-tTARGET', '--target=TARGET', 'Target glob to run rules against') do |target_glob|
@options[:target_glob] = target_glob
end

opts.on('-d', '--dry-run', 'Do not change underlying files, output changes') do |dry_run|
@options[:dry_run] = dry_run
end

opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end.parse!

runner = Refactor::Runner.new(**@options)
runner.run!
120 changes: 112 additions & 8 deletions lib/refactor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
# on top of Parser itself
require 'rubocop'

# Niceties for console output
require 'colorized_string'

module Refactor
# Utilities for working with ASTs
module Util
Expand Down Expand Up @@ -43,14 +46,19 @@ def initialize(rewriter)
super()
end

def self.process(string)
Rewriter.new(rules: [self]).process(string)
end
# def process(node)
# super(node).tap do |n_val|

def process_regular_node(node)
return matches(node) if defined?(matches)
# end
# end

super()
# Get all actively loaded rules
def self.descendants
ObjectSpace.each_object(Class).select { |klass| klass < self }
end

def self.process(string)
Rewriter.new(rules: [self]).process(string)
end

protected def replace(node, new_code)
Expand All @@ -64,13 +72,29 @@ def initialize(rules: [])
@rules = rules
end

# Only processes the string
def process(string)
load_rewriter(string).process
end

# Processes the string and returns a set of nested actions that
# were taken against it.
def process_with_context(string)
rewriter = load_rewriter(string)

{
processed_source: rewriter.process,
replacements: rewriter.as_replacements
}
end

private def load_rewriter(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

ast = source.ast
source_buffer = source.buffer

rewriter = Parser::Source::TreeRewriter.new(source_buffer)
Expand All @@ -80,7 +104,87 @@ def process(string)
ast.each_node { |node| rule.process(node) }
end

rewriter.process
rewriter
end
end

# Runner for applying refactoring rules
class Runner
DEFAULT_RULE_DIRECTORY = 'refactor_rules'
DEFAULT_TARGET_BLOB = '**/*.rb'

def initialize(
rules: [],
rule_directory: DEFAULT_RULE_DIRECTORY,
target_glob: DEFAULT_TARGET_BLOB,
dry_run: false
)
@rule_directory = rule_directory

if Dir.exist?(rule_directory)
Dir["#{rule_directory}/**/*.rb"].each do |rule|
load(rule)
end
else
warn "Rules directory '#{rule_directory}' does not exist. Skipping load."
end

@rules = (rules + Rule.descendants).uniq

@target_glob = target_glob
@target_files = Dir[@target_glob]

@rewriter = Rewriter.new(rules: @rules)
@dry_run = dry_run
end

def run!(dry_run: @dry_run)
@target_files.each do |file|
content = File.read(file)
@rewriter.process_with_context(content) => processed_source:, replacements:

next if replacements.empty?

puts "Changes made to #{file}:"

replacements.each do |range, replacement|
puts diff_output(range:, replacement:, file:), ''
end

File.write(file, processed_source) unless dry_run
end
end

private def diff_output(range:, replacement:, file:, indent: 2)
line_no = formatted_line_no(range)
space = ' ' * indent
large_space = ' ' * (indent + 2)

removed_source = range
.source
.lines
.map { ColorizedString["#{large_space}- #{_1}"].colorize(:red) }
.join("\n")

added_source = replacement
.lines
.map { ColorizedString["#{large_space}+ #{_1}"].colorize(:green) }
.join("\n")

<<~OUTPUT
#{space}[#{file}:#{line_no}]
#{removed_source}
#{added_source}
OUTPUT
end

private def formatted_line_no(range)
if range.single_line?
"L#{range.first_line}"
else
"L#{range.first_line}-#{range.last_line}"
end
end
end
end
2 changes: 1 addition & 1 deletion lib/refactor/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Refactor
VERSION = "0.1.0"
VERSION = "0.1.1"
end
1 change: 1 addition & 0 deletions refactor.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Gem::Specification.new do |spec|

# Consider removing this later and having a more minimal subset
spec.add_dependency "rubocop"
spec.add_dependency "colorize"

spec.add_development_dependency "guard-rspec"
end
10 changes: 10 additions & 0 deletions refactor_rules/big_decimal_rule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

class BigDecimalRule < Refactor::Rule
def on_send(node)
return unless node in [:send, _, :BigDecimal,
[:float | :int, value]
]

replace(node, "BigDecimal('#{value}')")
end
end
9 changes: 9 additions & 0 deletions refactor_rules/hash_ref_default_rule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class HashRefDefaultRule < Refactor::Rule
def on_send(node)
return unless node in [:send, [:const, nil, :Hash], :new,
[:array | :hash] => reference_value
]

replace(node, "Hash.new { |h, k| h[k] = #{reference_value.source} }")
end
end
19 changes: 19 additions & 0 deletions refactor_rules/shorthand_rule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Inherits from base rule, which provides a lot of utilities
# to match and replace with.
class ShorthandRule < Refactor::Rule
# The code we're trying to work with here is:
#
# [1, 2, 3].select { |v| v.even? }
#
# ...and we want to make it into:
#
# [1, 2, 3].select(&:even?)
#
def on_block(node)
return unless node in [:block, receiver,
[[:arg, arg_name]], [:send, [:lvar, ^arg_name], method_name]
]

replace(node, "#{receiver.source}(&:#{method_name})")
end
end
50 changes: 25 additions & 25 deletions sandbox/experimental.rb
Original file line number Diff line number Diff line change
Expand Up @@ -261,28 +261,28 @@ def match(#{node_arg_name})
end

# Tests for RuleMacros for later
context 'When using RuleMacros' do
let(:macro_rule) do
Class.new(Refactor::Rule) do
matches do |macro_node|
macro_node in [:block, receiver,
[[:arg, arg_name]], [:send, [:lvar, ^arg_name], method_name]
]
end

replace do |_macro_node, match_data|
"#{match_data[:receiver].source}(&:#{match_data[:method_name]})"
end
end
end

it 'creates a valid rule' do
expect(macro_rule.superclass).to eq(Refactor::Rule)
end

describe ".process" do
it "processes a rule inline for convenience" do
expect(macro_rule.process(target_source)).to eq(corrected_source)
end
end
end
# context 'When using RuleMacros' do
# let(:macro_rule) do
# Class.new(Refactor::Rule) do
# matches do |macro_node|
# macro_node in [:block, receiver,
# [[:arg, arg_name]], [:send, [:lvar, ^arg_name], method_name]
# ]
# end

# replace do |_macro_node, match_data|
# "#{match_data[:receiver].source}(&:#{match_data[:method_name]})"
# end
# end
# end

# it 'creates a valid rule' do
# expect(macro_rule.superclass).to eq(Refactor::Rule)
# end

# describe ".process" do
# it "processes a rule inline for convenience" do
# expect(macro_rule.process(target_source)).to eq(corrected_source)
# end
# end
# end

0 comments on commit dce251d

Please sign in to comment.