Skip to content

Instantly share code, notes, and snippets.

@nikhgupta
Last active April 6, 2021 10:09
Show Gist options
  • Save nikhgupta/2af7dab86a68c052315f652f174017bc to your computer and use it in GitHub Desktop.
Save nikhgupta/2af7dab86a68c052315f652f174017bc to your computer and use it in GitHub Desktop.
rubocop v0.71.0 - sane config and automation
require:
- ./spec/linters/migrations/migration_linting.rb
- rubocop-rails
- rubocop-rspec
AllCops:
TargetRubyVersion: 2.4
UseCache: true
CacheRootDirectory: ./.rubocop-cache
Exclude:
- node_modules/**/*
- vendor/**/*
- lib/*/node_modules/**/*
- tmp/*
Lint/Debugger:
Enabled: true
Exclude: []
# Commonly used screens these days easily fit more than 80 characters.
Metrics/LineLength:
Max: 120
Metrics/MethodLength:
Max: 20
ExcludedMethods:
- it
- describe
- context
- feature
- freeze
- specify
- define
- renum
Metrics/ClassLength:
Max: 160
# allow developer's preference on this
Style/StringLiterals:
Enabled: false
# We do not need to support Ruby 1.9, so this is good to use.
Style/SymbolArray:
Enabled: true
# prefer inject/detect over reduce/find (more common in the community)
Style/CollectionMethods:
Enabled: true
PreferredMethods:
find: detect
reduce: inject
detect: detect
inject: inject
# Fail is an alias of raise. Avoid aliases, it's more cognitive load for no gain.
# The argument that fail should be used to abort the program is wrong too,
# there's Kernel#abort for that.
Style/SignalException:
EnforcedStyle: only_raise
# No space makes the method definition shorter and differentiates
# from a regular assignment.
Layout/SpaceAroundEqualsInParameterDefault:
EnforcedStyle: no_space
Style/BlockDelimiters:
IgnoredMethods:
- let
- let!
- subject
- lambda
- proc
- it
- expect
# do / end blocks should be used for side effects,
# methods that run a block for side effects and have
# a useful return value are rare, assign the return
# value to a local variable for those cases.
Style/MethodCalledOnDoEndBlock:
Enabled: true
# Do not enforce single-character naming of parameters for single-line blocks.
Style/SingleLineBlockParams:
Enabled: false
# better readability
Layout/IndentFirstHashElement:
EnforcedStyle: consistent
# better readability - do not mix hash rockets with `:`
Style/HashSyntax:
EnforcedStyle: ruby19_no_mixed_keys
RSpec/Focus:
Enabled: true
Exclude: []
Rails/Output:
Enabled: true
Exclude: []
# use find_each whenever possible for performance reasons
Rails/FindEach:
Enabled: true
Exclude: []
# use uniq.pluck instead of pluck.uniq where possible
Rails/UniqBeforePluck:
Enabled: true
Exclude: []
Migration/AddIndexMustUseConcurrently:
Enabled: true
Exclude: []
Migration/MustDisableDdlTransaction:
Enabled: true
Exclude: []
Migration/AddReferenceCantUseIndex:
Enabled: true
Exclude: []
## Cops disabled or autocorrect disabled as safe-auto-correct is unsafe
# https://github.com/rubocop/rubocop/issues/7287
Style/FrozenStringLiteralComment:
Enabled: false
# https://github.com/rubocop/rubocop/issues/7766
Naming/RescuedExceptionsVariableName:
AutoCorrect: false
# String#% is by far the least verbose and only object oriented variant.
# https://github.com/rubocop/rubocop/issues/7130
Style/FormatString:
EnforcedStyle: percent
AutoCorrect: false
# encode: UTF-8
# frozen_string_literal: true
require 'pry'
require 'yaml'
require 'rubocop'
require 'rubocop-rspec'
require 'rubocop-rails'
module RuboCop
class AutocorrectAutomation
SEARCHABLE = [RuboCop::Cop, RuboCop::Cop::Rails, RuboCop::Cop::RSpec].freeze
RUBOCOP = 'bundle exec rubocop --require rubocop-rspec --require rubocop-rails'
def initialize(opts={})
@klasses = {}
@allowed = {}
@unsafe = opts.fetch(:unsafe, false)
@run_tests = opts.fetch(:run_tests, true)
@main = File.expand_path(opts.fetch(:main, '../.rubocop.yml'), __dir__)
@todo = File.expand_path(opts.fetch(:todo, '../.rubocop_todo.yml'), __dir__)
@default_config = RuboCop::ConfigLoader.default_configuration
@config = { @main => YAML.load_file(@main), @todo => YAML.load_file(@todo) }
end
def run
puts "====> Run tests: #{@run_tests ? 'Yes' : 'No'}"
puts "====> RuboCop mode: #{@unsafe ? :unsafe : :safe}"
puts "====> RuboCop version: #{RuboCop::Version.version}"
puts '====> Generating auto config from rubocop..'
# regenerate_todo_config unless File.exist?(@todo)
remove_inheritance_from_rubocop_todo
puts '====> Reading TODO configuration from rubocop for a list of issues..'
find_classes_for_cops
filter_for_allowed_cops
@allowed.each_with_index do |(cop, _), idx|
print "==> COP #{'%03d' % (idx + 1)}/#{@allowed.count}: #{'%-60s' % cop}"
run_autocorrect_for(cop)
end
puts '====> Re-generate auto config from rubocop with now uncorrectable issues..'
# regenerate_todo_config
puts '=> Done.'
end
def run_autocorrect_for(cop)
modified = `#{RUBOCOP} --auto-correct --only #{cop} --format files`.split("\n")
print ' ✅️' # running autocorrect
if modified.empty? # no auto correction was required
on_no_modification(cop)
elsif @run_tests
`RAILS_ENV=test bundle exec rspec 2>&1 >/dev/null`
$CHILD_STATUS.exitstatus.zero? ? on_test_success(cop) : on_test_failure(cop)
else
on_test_skipped(cop)
end
end
def list_unsafe_autocorrecting_cops
@klasses.select { |cop, _| unsafe_autocorrect?(cop) }
end
protected
def on_no_modification(cop)
puts ' 💚'
@config[@todo].delete(cop)
save_config
add_rubocop_commit :skipped, cop
end
def on_test_success(cop)
puts ' ✅'
@config[@todo].delete(cop)
save_config
add_rubocop_commit :ran, cop
end
def on_test_failure(cop)
puts ' ❌'
`git add . && git reset --hard`
@config[@main][cop] ||= {}
@config[@main][cop].merge!('Enabled' => true, 'AutoCorrect' => false)
save_config
add_rubocop_commit :failed, cop
end
def on_test_skipped(cop)
puts
@config[@todo].delete(cop)
save_config
add_rubocop_commit :ran, cop
end
def add_rubocop_commit(action, cop)
safety = safe_autocorrect?(cop) ? '' : ' (unsafe)'
tests = @run_tests ? '' : ' (tests skipped)'
message = "rubocop: #{action} autocorrect for cop: #{cop}#{safety}#{tests}"
`git add . && git commit -m "#{message}" 2>&1 >/dev/null`
end
def regenerate_todo_config
`#{RUBOCOP} --auto-gen-config --exclude-limit=10000`
end
private
def save_config
@config.each do |path, conf|
File.open(path, 'w') { |f| f.puts conf.to_yaml }
end
end
def remove_inheritance_from_rubocop_todo(key='inherit_from')
data = @config[@main][key]
data = [data].flatten.compact
# dirty-check
data = data.reject { |f| !f || File.basename(f) == File.basename(@todo) }
@config[@main].delete(key) if data.empty?
@config[@main][key] = (data.length > 1 ? data : data.first) if data.any?
save_config
end
def filter_for_allowed_cops
@allowed = @klasses.select do |cop, _|
@unsafe ? support_autocorrect?(cop) : safe_autocorrect?(cop)
end
end
def find_classes_for_cops
@klasses = @config[@todo].map do |cop, _|
klass = SEARCHABLE.map do |mod|
begin
mod.const_get(cop.gsub('/', '::'))
rescue NameError
nil
end
end.compact.first
[cop, klass]
end.to_h
end
def support_autocorrect?(cop)
inst = @klasses[cop].new
valid = inst.respond_to?(:support_autocorrect?, true) && inst.support_autocorrect?
valid && (@config[@main][cop] || {}).fetch('AutoCorrect', true)
end
def safe_autocorrect?(cop)
return false unless support_autocorrect?(cop)
conf = @default_config[cop]
conf.fetch('Safe', true) && conf.fetch('SafeAutoCorrect', true)
end
def unsafe_autocorrect?(cop)
support_autocorrect?(cop) && !safe_autocorrect?(cop)
end
end
end
# run this automation
require 'optparse'
options = {}
OptionParser.new do |opts|
opts.banner = 'Usage: bundle exec ruby rubocop_automate.rb [options]'
opts.on('-U', '--[no-]unsafe', 'Run unsafe autocorrections') do |n|
options[:unsafe] = n
end
opts.on('-T', '--[no-]skip-tests', 'Skip running tests after each autocorrection') do |n|
options[:run_tests] = !n
end
end.parse!
RuboCop::AutocorrectAutomation.new(options).run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment