As a Ruby developer, I’ve spent my fair share of time wrestling with code style tools like RuboCop and Pronto. They’re fantastic for keeping code clean, but let’s be honest—manually fixing linting errors across a codebase can feel like a slog. Worse, when bad style slips into your develop
branch because CI only catches it post-merge, you’re stuck playing cleanup. I wanted a better way: a tool that automates style corrections with precision and safety, whether I’m linting a pull request’s changes or the whole project. So, I wrote (with the help of AI) ProntoAutocorrect
.
Here’s the scoop on this Ruby script—what it does, how it works, and why it might save you (and your team) some headaches.
What Is ProntoAutocorrect?
ProntoAutocorrect
is a command-line tool that combines Pronto’s diff-based linting with RuboCop’s auto-correction magic. It scans your Ruby files for style issues and fixes them in one go, offering both a “safe” mode for low-risk tweaks and an “aggressive” mode for deeper cleanups. You can run it against a base branch (like origin/develop
) to catch changes in a PR, or sweep through all Ruby files in your current branch with the --all
flag.
Here’s the script in action:
./pronto_autocorrect.rb origin/main -A
That’ll compare your current branch to origin/main
and apply aggressive auto-corrections. Want to see it for yourself? Check out the full code at the end of this post.
Features
Here’s what it brings to the table:
- Flexible Targeting: Compare against any branch (default:
origin/develop
) or lint all Ruby files—both Git-tracked and untracked—with--all
, automatically ignoring files in.gitignore
. - Safe vs. Aggressive Modes: Use
-a
(default) for safe fixes that won’t break logic, or-A
for RuboCop’s full auto-correct power (watch out—semantics might shift!). - Batch Processing: Corrects files in chunks of 50 to avoid command-line length limits.
- Real-Time Output: See Pronto’s linting and RuboCop’s fixes as they happen.
- Smart Summaries: Get a rundown of files processed, time taken, and any lingering issues with next-step tips.
Run it with -h
for the full usage guide:
./pronto_autocorrect.rb --help
A Sample Run
Here’s what it looks like fixing a few files:
🔍 Running Pronto against origin/develop...
app/models/user.rb:5: Missing comma
app/controllers/api_controller.rb:12: Trailing whitespace detected
📝 Found 2 files to auto-correct:
• app/models/user.rb
• app/controllers/api_controller.rb
🔧 Running safe auto-corrections...
Processing files 1-2 of 2...
Inspecting 2 files
..
2 files inspected, 2 offenses detected, 2 offenses corrected
📊 Summary:
• Time taken: 1.34 seconds
• Files processed: 2
• Mode: Comparing against origin/develop
• Auto-correct: Safe
• Status: All safe corrections applied
✅ Done! Your code is now more beautiful 🎨
The Code
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'set'
require 'open3'
class ProntoAutocorrect
def self.print_help
puts <<~HELP
Usage: #{$PROGRAM_NAME} [base_branch] [options]
A tool to run Pronto and auto-correct Ruby code style issues.
Arguments:
base_branch The branch to compare against (default: origin/develop)
Not used when --all flag is provided
Options:
-h, --help Show this help message
-a Use safe auto-corrections (default)
Only makes changes that won't break your code
-A, --aggressive Use aggressive auto-corrections
Makes all possible style corrections, may alter code behavior
--all Run on all Ruby files in current branch
Does not compare against any other branch
Examples:
#{$PROGRAM_NAME} # Compare against origin/develop, safe corrections
#{$PROGRAM_NAME} origin/main # Compare against origin/main, safe corrections
#{$PROGRAM_NAME} -A # Compare against origin/develop, aggressive corrections
#{$PROGRAM_NAME} origin/main -A # Compare against origin/main, aggressive corrections
#{$PROGRAM_NAME} --all # Run on all files, safe corrections
#{$PROGRAM_NAME} --all -A # Run on all files, aggressive corrections
Note:
Safe mode (-a) is the default and only makes low-risk corrections.
Aggressive mode (-A) may change code behavior, use with caution.
Consider committing changes between runs to easily revert if needed.
HELP
exit 0
end
def self.run(base_branch = 'origin/develop', aggressive = false, all_files = false)
new(base_branch, aggressive, all_files).run
end
def initialize(base_branch, aggressive = false, all_files = false)
@base_branch = base_branch
@files_to_correct = Set.new
@start_time = Time.now
@remaining_issues = false
@aggressive = aggressive
@all_files = all_files
end
def run
if @all_files
puts "\n🔍 Running on all Ruby files..."
find_all_ruby_files
else
puts "\n🔍 Running Pronto against #{@base_branch}..."
run_pronto
end
if @files_to_correct.any?
puts "\n📝 Found #{@files_to_correct.size} files to auto-correct:"
@files_to_correct.each { |file| puts " • #{file}" }
autocorrect_files_safely
else
puts "\n✨ No files need auto-correction!"
end
print_summary
end
private
def run_pronto
cmd = "bin/pronto run -c #{@base_branch}"
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
stdout.each_line do |line|
puts line # Show the Pronto output in real-time
# Extract Ruby file paths from Pronto output
if (file_path = extract_file_path(line))
@files_to_correct << file_path
end
end
# Show any errors
stderr_output = stderr.read
unless stderr_output.empty?
puts "\n⚠️ Pronto warnings/errors:"
puts stderr_output
end
unless wait_thr.value.success?
puts "\n❌ Pronto failed to run properly"
exit 1
end
end
end
def extract_file_path(line)
# Match Ruby file paths that Pronto reports on
match = line.match(/^([^:]+\.rb):\d+/)
match[1] if match
end
def autocorrect_files_safely
mode = @aggressive ? 'aggressive' : 'safe'
flag = @aggressive ? '-A' : '-a'
puts "\n🔧 Running #{mode} auto-corrections..."
# Process files in batches to avoid command line length limits
batch_size = 50
total_files = @files_to_correct.size
files_array = @files_to_correct.to_a
success = true
has_remaining_issues = false
files_array.each_slice(batch_size).with_index do |batch, index|
start_file = (index * batch_size) + 1
end_file = [start_file + batch.size - 1, total_files].min
puts "\n Processing files #{start_file}-#{end_file} of #{total_files}..."
# Run auto-corrections for this batch
cmd = "bundle exec rubocop #{flag} #{batch.join(' ')}"
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
stdout.each_line { |line| puts " #{line}" }
stderr_output = stderr.read
unless stderr_output.empty?
puts "\n⚠️ Rubocop warnings/errors:"
puts stderr_output
success = false
end
# Check if this batch has remaining issues
has_remaining_issues ||= check_remaining_issues(batch) if success
end
end
print_remaining_issues_message if has_remaining_issues
end
def check_remaining_issues(files)
# Check if there are still issues after corrections
cmd = "bundle exec rubocop #{files.join(' ')}"
has_issues = false
Open3.popen3(cmd) do |_stdin, stdout, _stderr, wait_thr|
has_issues = !wait_thr.value.success?
end
has_issues
end
def print_remaining_issues_message
puts "\n⚠️ Some issues remain after #{@aggressive ? 'aggressive' : 'safe'} corrections."
if @aggressive
puts "Consider manually reviewing and fixing the remaining issues."
else
puts "To apply potentially breaking corrections, you have two options:"
puts "\nOption 1: Rerun with aggressive mode (-A)"
puts " #{$PROGRAM_NAME} #{@base_branch} #{@all_files ? '--all' : ''} -A".strip
puts "\nOption 2: Commit current changes and run rubocop directly:"
puts "1. First commit your current changes:"
puts " git add . && git commit -m 'WIP: Safe autocorrections'"
puts "2. Then run rubocop directly on specific files"
puts "\nOption 1 is more convenient, but Option 2 gives you more control"
puts "by letting you easily revert if the aggressive auto-corrections cause issues."
end
end
def find_all_ruby_files
# Use git ls-files to list all Ruby files, respecting .gitignore
# --cached: include tracked files
# --others: include untracked files
# --exclude-standard: respect .gitignore
# -z: null-terminate file names (handles special characters in names)
cmd = "git ls-files --cached --others --exclude-standard -z '*.rb'"
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
# Split on null byte to handle filenames with newlines
stdout.read.split("\0").each do |file_path|
next if file_path.empty?
next if file_path.start_with?('vendor/', 'tmp/') # Still exclude vendor and tmp
@files_to_correct << file_path
end
stderr_output = stderr.read
unless stderr_output.empty?
puts "\n⚠️ File search warnings/errors:"
puts stderr_output
end
unless wait_thr.value.success?
puts "\n❌ Git file search failed"
exit 1
end
end
end
def print_summary
duration = Time.now - @start_time
puts "\n📊 Summary:"
puts " • Time taken: #{'%.2f' % duration} seconds"
puts " • Files processed: #{@files_to_correct.size}"
puts " • Mode: #{@all_files ? 'All files' : "Comparing against #{@base_branch}"}"
puts " • Auto-correct: #{@aggressive ? 'Aggressive' : 'Safe'}"
if @remaining_issues
puts " • Status: Some issues remain (see instructions above)"
else
puts " • Status: All #{@aggressive ? 'aggressive' : 'safe'} corrections applied"
end
puts "\n✅ Done! Your code is #{@remaining_issues ? 'partially' : 'now'} more beautiful 🎨\n\n"
end
end
if $PROGRAM_NAME == __FILE__
if ARGV.include?('-h') || ARGV.include?('--help')
ProntoAutocorrect.print_help
else
# Check for --all flag
all_files = ARGV.include?('--all')
# Filter out option flags to get the base branch
args = ARGV.reject { |arg| arg.start_with?('-') }
base_branch = args.first || 'origin/develop'
aggressive = ARGV.include?('--aggressive') || ARGV.include?('-A')
ProntoAutocorrect.run(base_branch, aggressive, all_files)
end
end