diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..67f8702 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + spec: + name: spec (${{ matrix.ruby }}) + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || false }} + strategy: + fail-fast: false + matrix: + ruby: + - "3.1" + - "3.4" + - "jruby-9.4" + - "jruby-10.0" + include: + # Bleeding-edge runtime: exercised, but allowed to fail so a + # not-yet-released or in-flight toolchain never blocks a PR. + - ruby: "4.0" + experimental: true + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run specs + run: bundle exec rake spec diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..5be63fc --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--format documentation diff --git a/.rubocop.yml b/.rubocop.yml index 29fc4b9..74b4db0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,4 @@ -require: +plugins: - rubocop-rails - rubocop-performance diff --git a/.ruby-version b/.ruby-version index 94ff29c..7921bd0 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.1 +3.4.8 diff --git a/README.md b/README.md index 1ba673a..81aa004 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ -[![Dependency Status](https://gemnasium.com/mxenabled/mad_rubocop.svg)](https://gemnasium.com/mxenabled/mad_rubocop) - # MadRubocop -Custom RuboCop yml files to modify the cops enforced by RuboCop. +[![CI](https://github.com/mxenabled/mad_rubocop/actions/workflows/ci.yml/badge.svg)](https://github.com/mxenabled/mad_rubocop/actions/workflows/ci.yml) + +Custom RuboCop yml files to modify the cops enforced by RuboCop, shared across projects. + +Built on RuboCop `~> 1.88`, rubocop-rails `~> 2.35`, and rubocop-performance `~> 1.26`. Requires Ruby `>= 3.1`. + +## What's in the box + +This gem ships configuration only — no Ruby code. Consumers inherit three files: + +- `.rubocop.yml` — the entry point. Loads the `rubocop-rails` and `rubocop-performance` plugins, inherits the two files below, and sets `AllCops` defaults (`NewCops: disable`, so cops added in a RuboCop upgrade stay off until explicitly enabled here). +- `lib/disabled_cops.yml` — cops turned off, either as permanent house style or because the codebase isn't ready for them. +- `lib/modified_cops.yml` — cops kept on but configured to a non-default style (e.g. `hash_rockets`, `double_quotes`, outdented access modifiers). ## Installation @@ -27,15 +37,28 @@ inherit_gem: mad_rubocop: .rubocop.yml ``` -NOTE: The `Exlude` setting on cops that only ignore certain files or directories cannot be set by MadRubocop. +NOTE: The `Exclude` setting on cops that only ignore certain files or directories cannot be set by MadRubocop. ## Development After checking out the repo, run `bin/setup` to install dependencies. +Run the test suite with `bundle exec rake spec`. The specs validate the shipped configuration against the installed RuboCop — they confirm the config loads, the rubocop-rails and rubocop-performance plugins are present, and every referenced cop still exists (catching cops that a RuboCop upgrade renames or removes). + To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +### Upgrading RuboCop + +Keeping the pinned RuboCop (and its plugins) current is the main maintenance task. The workflow: + +1. Bump the version constraints for `rubocop`, `rubocop-rails`, and/or `rubocop-performance` in `mad_rubocop.gemspec`. +2. Run `bundle update rubocop rubocop-rails rubocop-performance`. +3. Run `bundle exec rake spec`. The suite fails if the config references a cop that the new RuboCop **renamed or removed** — the failure message names the offending cop. +4. For each failure, update the cop name in `lib/disabled_cops.yml` / `lib/modified_cops.yml` (or drop it if the cop no longer exists). +5. Because `NewCops` is disabled, cops **added** in the upgrade are off by default. Review `rubocop --show-cops` (or the release notes) and add any you want to disable/configure. +6. Bump `MadRubocop::VERSION` in `version.rb`, note the change, and release. + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/mxenabled/mad_rubocop. diff --git a/Rakefile b/Rakefile index 2995527..b7e9ed5 100644 --- a/Rakefile +++ b/Rakefile @@ -1 +1,6 @@ require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/lib/disabled_cops.yml b/lib/disabled_cops.yml index 78710c9..903517a 100644 --- a/lib/disabled_cops.yml +++ b/lib/disabled_cops.yml @@ -113,7 +113,7 @@ Naming/HeredocDelimiterCase: Enabled: false Naming/MemoizedInstanceVariableName: Enabled: false -Naming/PredicateName: +Naming/PredicatePrefix: Enabled: false Naming/MethodParameterName: Enabled: false diff --git a/lib/mad_rubocop/version.rb b/lib/mad_rubocop/version.rb index 67e0fbb..0338c8a 100644 --- a/lib/mad_rubocop/version.rb +++ b/lib/mad_rubocop/version.rb @@ -1,3 +1,3 @@ module MadRubocop - VERSION = "4.0.0" + VERSION = "4.2.0.pre1" end diff --git a/mad_rubocop.gemspec b/mad_rubocop.gemspec index baea7d5..e9176ac 100644 --- a/mad_rubocop.gemspec +++ b/mad_rubocop.gemspec @@ -20,9 +20,12 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "rubocop", "~> 1.32.0" - spec.add_dependency "rubocop-performance", "~> 1.14.3" - spec.add_dependency "rubocop-rails", "~> 2.15.2" + spec.required_ruby_version = ">= 3.1" + + spec.add_dependency "rubocop", "~> 1.88.0" + spec.add_dependency "rubocop-performance", "~> 1.26.1" + spec.add_dependency "rubocop-rails", "~> 2.35.5" spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency "rake" + spec.add_development_dependency "rspec", "~> 3.0" end diff --git a/spec/fixtures/sample.rb b/spec/fixtures/sample.rb new file mode 100644 index 0000000..8a0160c --- /dev/null +++ b/spec/fixtures/sample.rb @@ -0,0 +1,7 @@ +# A tiny fixture used to exercise the gem's RuboCop configuration end-to-end. +# It exists only so the CLI has something to inspect; offenses are irrelevant. +module Sample + def self.greeting + "hello" + end +end diff --git a/spec/mad_rubocop/config_spec.rb b/spec/mad_rubocop/config_spec.rb new file mode 100644 index 0000000..81fb46e --- /dev/null +++ b/spec/mad_rubocop/config_spec.rb @@ -0,0 +1,76 @@ +require "spec_helper" +require "yaml" +require "stringio" +require "rubocop" + +# These specs treat the gem's YAML as the unit under test. Because mad_rubocop +# ships nothing but configuration, the most valuable guarantees are that the +# config loads under the pinned RuboCop, that every cop it references still +# exists, and that RuboCop raises no obsolete/removed-cop warnings when it runs. +# A green suite here is what catches upgrades that rename a cop (e.g. the +# Naming/PredicateName -> Naming/PredicatePrefix rename in RuboCop 1.88). +RSpec.describe "MadRubocop configuration" do + ROOT = File.expand_path("../..", __dir__) + CONFIG_FILE = File.join(ROOT, ".rubocop.yml") + COP_CONFIG_FILES = { + "disabled_cops.yml" => File.join(ROOT, "lib", "disabled_cops.yml"), + "modified_cops.yml" => File.join(ROOT, "lib", "modified_cops.yml"), + }.freeze + + # Loading the config also loads the rubocop-rails / rubocop-performance + # plugins declared under `plugins:`, which registers their cops globally. + before(:all) do + RuboCop::ConfigLoader.load_file(CONFIG_FILE) + end + + def self.cop_names_in(file) + YAML.safe_load(File.read(file)).keys.select { |key| key.include?("/") } + end + + it "loads the top-level config without raising" do + expect { RuboCop::ConfigLoader.load_file(CONFIG_FILE) }.not_to raise_error + end + + it "loads the rubocop-rails and rubocop-performance plugin cops" do + departments = RuboCop::Cop::Registry.global.departments.map(&:to_s) + + expect(departments).to include("Rails", "Performance") + end + + describe "every configured cop is recognized by the installed RuboCop" do + COP_CONFIG_FILES.each do |label, file| + context label do + cop_names_in(file).each do |cop_name| + it "recognizes #{cop_name}" do + cop = RuboCop::Cop::Registry.global.find_by_cop_name(cop_name) + + expect(cop).not_to( + be_nil, + "#{cop_name} is not a known cop — it may have been renamed, " \ + "removed, or moved to a plugin that is no longer loaded", + ) + end + end + end + end + end + + it "runs against a fixture without emitting obsolete/removed-cop warnings" do + fixture = File.join(ROOT, "spec", "fixtures", "sample.rb") + + captured = StringIO.new + original_stderr = $stderr + $stderr = captured + begin + RuboCop::CLI.new.run( + ["--config", CONFIG_FILE, "--force-exclusion", "--no-color", fixture], + ) + ensure + $stderr = original_stderr + end + + expect(captured.string).not_to match( + /obsolete|has been (removed|renamed|extracted)|unrecognized cop/i, + ) + end +end diff --git a/spec/mad_rubocop/version_spec.rb b/spec/mad_rubocop/version_spec.rb new file mode 100644 index 0000000..271a4af --- /dev/null +++ b/spec/mad_rubocop/version_spec.rb @@ -0,0 +1,14 @@ +require "spec_helper" + +RSpec.describe MadRubocop do + it "has a semantic version number" do + expect(MadRubocop::VERSION).to match(/\A\d+\.\d+\.\d+\z/) + end + + it "matches the version declared in the gemspec" do + gemspec_path = File.expand_path("../../mad_rubocop.gemspec", __dir__) + gemspec = Gem::Specification.load(gemspec_path) + + expect(gemspec.version.to_s).to eq(MadRubocop::VERSION) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..0a2fb2c --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,15 @@ +require "mad_rubocop" + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.disable_monkey_patching! + config.order = :random + Kernel.srand config.seed +end