Automating Ruby Style Fixes with ProntoAutocorrect

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Up Next:

Automate SSH logins when a password is required

Automate SSH logins when a password is required