Skip to content

Instantly share code, notes, and snippets.

@MaxLap
Forked from skanev/rubocop.rb
Last active March 12, 2024 17:15
Show Gist options
  • Save MaxLap/ea4b6d1df81de3024562798b5501b9c8 to your computer and use it in GitHub Desktop.
Save MaxLap/ea4b6d1df81de3024562798b5501b9c8 to your computer and use it in GitHub Desktop.
A Rubocop wrapper that checks only added/modified code
#!/usr/bin/env ruby
# A sneaky wrapper around Rubocop that allows you to run it only against
# the recent changes, as opposed to the whole project. It lets you
# enforce the style guide for new/modified code only, as opposed to
# having to restyle everything or adding cops incrementally. It relies
# on git to figure out which files to check.
#
# Here are some options you can pass in addition to the ones in rubocop:
#
# --local Check only the changes you are about to push
# to the remote repository.
#
# --staged Check only changes that are currently staged.
#
# --uncommitted Check only changes in files that have not been
# --index committed (i.e. either in working directory or
# staged).
#
# --against REFSPEC Check changes since REFSPEC. This can be
# anything that git will recognize.
#
# --branch Check only changes in the current branch.
#
# --courage Without this option, only the modified lines
# are inspected. When supplied, it will check
# the full contents of the file. You should have
# the courage to fix your style violations as
# you see them, you know.
#
# Caveat emptor:
#
# * Monkey patching ahead. This script relies on Rubocop internals and
# has been tested against 1.62.1. Newer (or older) versions might
# break it.
#
# * While it does try to check modified lines only, there might be some
# quirks. It might not show offenses in modified code if they are
# reported at unmodified lines. It might also show offenses in
# unmodified code if they are reported in modified lines.
# Latest source: https://gist.github.com/MaxLap/ea4b6d1df81de3024562798b5501b9c8
require 'rubocop'
require 'pathname'
module DirtyCop
extend self # In your face, style guide!
def bury_evidence?(file, line)
!report_offense_at?(file, line)
end
def bury_evidences(file, offenses)
offenses.reject { |o| bury_evidence?(file, o.line) }
end
def staged_changes_only?
!!@staged_changes_only
end
def uncovered_targets
@files
end
def cover_up_unmodified(ref, only_changed_lines = true)
@files = files_modified_since(ref)
@line_filter = build_line_filter(@files, ref) if only_changed_lines
end
def process_bribe
eat_a_donut if ARGV.empty?
ref = nil
only_changed_lines = true
loop do
arg = ARGV.shift
case arg
when '--local'
ref = `git rev-parse --abbrev-ref --symbolic-full-name @{u}`.chomp
exit 1 unless $?.success?
when '--staged'
ref = '--cached'
@staged_changes_only = true
ARGV << "--cache=false"
when '--against'
ref = ARGV.shift
when '--uncommitted', '--index'
ref = 'HEAD'
when '--branch'
ref = `git merge-base HEAD master`.chomp
when '--courage'
only_changed_lines = false
else
ARGV.unshift arg
break
end
end
return unless ref
cover_up_unmodified ref, only_changed_lines
end
private
def report_offense_at?(file, line)
!@line_filter || @line_filter.fetch(file)[line]
end
def files_modified_since(ref)
`git diff --diff-filter=AM --name-only #{ref}`.
lines.
map(&:chomp).
map { |file| File.absolute_path(file) }
end
def build_line_filter(files, ref)
result = {}
suspects = files_modified_since(ref)
suspects.each do |file|
result[file] = lines_modified_since(file, ref)
end
result
end
def lines_modified_since(file, ref)
ranges =
`git diff -p -U0 #{ref} #{file}`.
lines.
grep(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/) { $1.to_i...($1.to_i + ($2 || 1).to_i) }.
reverse
return [] if ranges.empty?
mask = Array.new(ranges.first.end)
ranges.each do |range|
range.each do |line|
mask[line] = true
end
end
mask
end
def eat_a_donut
puts "#$PROGRAM_NAME: The dirty cop Alex Murphy could have been"
puts
puts File.read(__FILE__)[/(?:^#(?:[^!].*)?\n)+/s].gsub(/^#/, ' ')
exit
end
end
module RuboCop
class TargetFinder
alias_method :find_unpatched, :find
def find(args, mode)
every_files = find_unpatched(args, mode)
modified_files = DirtyCop.uncovered_targets
every_files & modified_files if modified_files
end
end
class Runner
alias_method :inspect_file_unpatched, :inspect_file
def inspect_file(processed_source, *args)
offenses, updated = inspect_file_unpatched(processed_source, *args)
offenses = DirtyCop.bury_evidences(processed_source.path, offenses)
[offenses, updated]
end
alias_method :add_redundant_disables_unpatched, :add_redundant_disables
def add_redundant_disables(file, offenses, source)
offenses = add_redundant_disables_unpatched(file, offenses, source)
DirtyCop.bury_evidences(file, offenses)
end
end
class ProcessedSource
class << self
alias_method :from_file_unpatched, :from_file
end
def self.from_file(path, ruby_version, **kwargs)
if DirtyCop.staged_changes_only?
pathname = Pathname.new(path)
git_root = Pathname.new(`git rev-parse --show-toplevel`.strip)
git_relative_path = pathname.relative_path_from(git_root).to_s
source = `git show :#{git_relative_path}`
new(source, ruby_version, path, **kwargs)
else
from_file_unpatched(path, ruby_version, **kwargs)
end
end
end
end
DirtyCop.process_bribe
exit RuboCop::CLI.new.run
@juanfer0002
Copy link

juanfer0002 commented Aug 20, 2021

At some point this will stop working, so you probably want to remove the alias in line :177 and the method in line :178. These:

    # Remove these lines

    alias add_unneeded_disables_unpatched add_unneeded_disables 
    def add_unneeded_disables(file, offenses, source)
      offenses = add_unneeded_disables_unpatched(file, offenses, source)
      DirtyCop.bury_evidences(file, offenses)
    end

Note: This happened in my localhost, and it just stopped workign out of nowhere.

@MaxLap
Copy link
Author

MaxLap commented Mar 12, 2024

Updated script for Rubocop 1.62.1

What @juanfer0002 mentions was caused by rubocop/rubocop@e71d77f, so just removing that code means that redundant disable might be wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment