Skip to content

Commit

Permalink
[N/A] Convert gem to be a rubocop extension (#119)
Browse files Browse the repository at this point in the history
Updates the gem to follow the conventions in https://github.com/rubocop/rubocop-extension-generator which is used to generate all of the rubocop gems we use (`rubocop-rspec`, `rubocop-rails`, `rubocop-performance`, etc).

Converting the gem to a rubocop extension allows us to use the `boxt_rubocop` rubocop config by simply requiring it as we do other rubocop gems:

```yml
require:
  - boxt_rubocop
```

It also has the added benefit of having proper support for custom cops. This means that we can now create custom cops without having to worry about manually disabling them in every project we don't want to use them in.

I have left the `rails.yml` and `rspec.yml` configs as separate files that can be imported individually using `inherit_gem`:

```yml
inherit_gem:
  boxt_rubocop:
    - rails.yml
    - rspec.yml
```

If we decide that any of these configs should be enabled by default we can simply move them in to `config/default.yml`.
  • Loading branch information
jivdhaliwal authored Oct 2, 2023
1 parent 7b1992b commit b1a09a4
Show file tree
Hide file tree
Showing 16 changed files with 145 additions and 84 deletions.
4 changes: 1 addition & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
inherit_from:
- default.yml

require:
- boxt_rubocop
- rubocop-performance
- rubocop-rake
- rubocop-rails
Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,37 @@ bundle

## Usage

Add a `.rubocop.yml` file to the root of your project with the following settings:
Put this into your .rubocop.yml.

```yml
require:
- boxt_rubocop
```
To enable additional configuration for `rubocop-rails` and `rubocop-rspec`, add the following to your .rubocop.yml:

```yml
inherit_gem:
boxt_rubocop:
- default.yml # use default cops
- rails.yml # use Rails cops - see Additional Extensions/Cops
- rspec.yml # use rspec cops - see Additional Extensions/Cops
```

## Creating new custom cops

Use the rake task new_cop to generate a cop template:

```sh
$ bundle exec rake 'new_cop[Boxt/Name]'
[create] lib/rubocop/cop/boxt/name.rb
[create] spec/rubocop/cop/boxt/name_spec.rb
[modify] lib/rubocop/cop/boxt_cops.rb - `require_relative 'boxt/name'` was injected.
[modify] A configuration for the cop is added into config/default.yml.

```

Documentation on creating a new cop can be found [here](https://docs.rubocop.org/rubocop/1.56/development.html#create-a-new-cop).

### NewCops

`NewCops` is enabled by default.
Expand Down
28 changes: 23 additions & 5 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rake/testtask"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec) do |t|
t.pattern = Dir.glob("spec/**/*_spec.rb")
end

desc "Map rake test to rake spec"
task test: :spec

RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = FileList["spec/**/*_spec.rb"]
end

desc "Generate a new cop with a template"
task :new_cop, [:cop] => :environment do |_task, args|
require "rubocop"

cop_name = args.fetch(:cop) do
warn "usage: bundle exec rake 'new_cop[Boxt/Name]'"
exit!
end

generator = RuboCop::Cop::Generator.new(cop_name)

generator.write_source
generator.write_spec
generator.inject_require(root_file_path: "lib/rubocop/cop/boxt_cops.rb")
generator.inject_config(config_file_path: "config/default.yml")

puts generator.todo
end
7 changes: 3 additions & 4 deletions boxt_rubocop.gemspec
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# frozen_string_literal: true

lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "boxt_rubocop/version"
require_relative "lib/rubocop/boxt/version"

Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 2.7"
Expand All @@ -16,7 +14,7 @@ Gem::Specification.new do |spec|
}
spec.name = "boxt_rubocop"
spec.summary = "Base Rubocop settings for all Boxt Ruby projects"
spec.version = BoxtRubocop::VERSION
spec.version = RuboCop::Boxt::VERSION

spec.files = Dir[
"*.yml",
Expand All @@ -26,6 +24,7 @@ Gem::Specification.new do |spec|
"VERSION",
"lib/**/*"
]
spec.require_paths = ["lib"]

# Locking rubocop versions so we can control the pending cops
spec.add_dependency "rubocop", "1.56.3"
Expand Down
7 changes: 4 additions & 3 deletions default.yml → config/default.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
require:
- ./lib/rubocop/cop/boxt/api_path_format.rb

AllCops:
Exclude:
- "**/*/schema.rb"
Expand All @@ -10,6 +7,10 @@ AllCops:
- vendor/**/*
NewCops: enable

Boxt/ApiPathFormat:
Description: 'Ensure that the API path uses kebab case'
Enabled: false

Layout/ClassStructure:
Enabled: true

Expand Down
19 changes: 6 additions & 13 deletions lib/boxt_rubocop.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
# frozen_string_literal: true

require "boxt_rubocop/version"
require "rubocop"

# Require all custom cops defined in rubocop/cop/**/*.rb
Dir[File.join(__dir__, "rubocop", "cop", "**", "*.rb")].sort.each { |file| require file }
require_relative "rubocop/boxt"
require_relative "rubocop/boxt/version"
require_relative "rubocop/boxt/inject"

module BoxtRubocop
module_function
RuboCop::Boxt::Inject.defaults!

##
# Provide a root path helper for the gem root dir
#
# Returns Pathname
def root
Pathname.new(File.dirname(__dir__))
end
end
require_relative "rubocop/cop/boxt_cops"
5 changes: 0 additions & 5 deletions lib/boxt_rubocop/version.rb

This file was deleted.

14 changes: 14 additions & 0 deletions lib/rubocop/boxt.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

require_relative "boxt/version"

module RuboCop
module Boxt
class Error < StandardError; end
PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze
CONFIG = YAML.safe_load(CONFIG_DEFAULT.read).freeze

private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
end
end
20 changes: 20 additions & 0 deletions lib/rubocop/boxt/inject.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

# The original code is from https://github.com/rubocop/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
# See https://github.com/rubocop/rubocop-rspec/blob/master/MIT-LICENSE.md
module RuboCop
module Boxt
# Because RuboCop doesn't yet support plugins, we have to monkey patch in a
# bit of our configuration.
module Inject
def self.defaults!
path = CONFIG_DEFAULT.to_s
hash = ConfigLoader.send(:load_yaml_configuration, path)
config = Config.new(hash, path).tap(&:make_excludes_absolute)
Rails.logger.debug { "configuration from #{path}" } if ConfigLoader.debug?
config = ConfigLoader.merge_with_default(config, path)
ConfigLoader.instance_variable_set(:@default_configuration, config)
end
end
end
end
7 changes: 7 additions & 0 deletions lib/rubocop/boxt/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module RuboCop
module Boxt
VERSION = File.read(File.join(File.dirname(__FILE__), "../../../VERSION")).split("\n").first
end
end
4 changes: 1 addition & 3 deletions lib/rubocop/cop/boxt/api_path_format.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# frozen_string_literal: true

require "rubocop"

module RuboCop
module Cop
module Boxt
Expand All @@ -25,7 +23,7 @@ module Boxt
# get "/installation-days"
# namespace "password-resets"
#
class ApiPathFormat < ::RuboCop::Cop::Base
class ApiPathFormat < Base
def_node_matcher :path_defining_method_with_string_path, <<~PATTERN
(send nil? {:post | :get | :namespace} (:str $_))
PATTERN
Expand Down
3 changes: 3 additions & 0 deletions lib/rubocop/cop/boxt_cops.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# frozen_string_literal: true

require_relative "boxt/api_path_format"
11 changes: 0 additions & 11 deletions spec/boxt_rubocop_spec.rb

This file was deleted.

13 changes: 13 additions & 0 deletions spec/rubocop/boxt_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Boxt do
let(:current_version) { File.read("VERSION").split("\n").first }

it "has a version number" do
expect(RuboCop::Boxt::VERSION).not_to be_nil
end

it "is set from the VERSION file" do
expect(RuboCop::Boxt::VERSION).to eq(current_version)
end
end
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
# frozen_string_literal: true

require "rubocop/rspec/support"

RSpec.describe RuboCop::Cop::Boxt::ApiPathFormat do
include RuboCop::RSpec::ExpectOffense

subject(:cop) { described_class.new(config) }

let(:config) { RuboCop::Config.new }

RSpec.describe RuboCop::Cop::Boxt::ApiPathFormat, :config do
it "does not register an offense when using get with a path in kebab-case format" do
expect_no_offenses(<<~RUBY)
class Test < Grape::API
Expand Down Expand Up @@ -46,7 +38,7 @@ class Test < Grape::API
expect_offense(<<~RUBY)
class Test < Grape::API
get "path/snake_case"
^^^^^^^^^^^^^^^^^^^^^ Boxt/ApiPathFormat: Use kebab-case for the API path
^^^^^^^^^^^^^^^^^^^^^ Use kebab-case for the API path
end
RUBY
end
Expand All @@ -55,7 +47,7 @@ class Test < Grape::API
expect_offense(<<~RUBY)
class Test < Grape::API
post "path/snake_case"
^^^^^^^^^^^^^^^^^^^^^^ Boxt/ApiPathFormat: Use kebab-case for the API path
^^^^^^^^^^^^^^^^^^^^^^ Use kebab-case for the API path
end
RUBY
end
Expand All @@ -64,7 +56,7 @@ class Test < Grape::API
expect_offense(<<~RUBY)
class Test < Grape::API
namespace "path/snake_case"
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Boxt/ApiPathFormat: Use kebab-case for the API path
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use kebab-case for the API path
end
RUBY
end
Expand All @@ -73,7 +65,7 @@ class Test < Grape::API
expect_offense(<<~RUBY)
class Test < Grape::API
namespace :snake_case
^^^^^^^^^^^^^^^^^^^^^ Boxt/ApiPathFormat: Use kebab-case for the API path
^^^^^^^^^^^^^^^^^^^^^ Use kebab-case for the API path
end
RUBY
end
Expand Down
44 changes: 22 additions & 22 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# frozen_string_literal: true

$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
require "boxt_rubocop"

require "rspec"
require "rubocop/rspec/support"
require "simplecov"

SimpleCov.start do
Expand All @@ -27,6 +25,8 @@
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
config.include RuboCop::RSpec::ExpectOffense

# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
Expand Down Expand Up @@ -70,13 +70,13 @@
# # the `--only-failures` and `--next-failure` CLI options. We recommend
# # you configure your source control system to ignore this file.
# config.example_status_persistence_file_path = "spec/examples.txt"
#
# # Limits the available syntax to the non-monkey patched syntax that is
# # recommended. For more details, see:
# # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
# config.disable_monkey_patching!

# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
config.disable_monkey_patching!
#
# # Many RSpec users commonly either run the entire suite or an individual
# # file, and it's useful to allow more verbose output when running an
Expand All @@ -92,18 +92,18 @@
# # end of the spec run, to help surface which specs are running
# # particularly slow.
# config.profile_examples = 10
#
# # Run specs in random order to surface order dependencies. If you find an
# # order dependency and want to debug it, you can fix the order by providing
# # the seed, which is printed after each run.
# # --seed 1234
# config.order = :random
#
# # Seed global randomization in this process using the `--seed` CLI option.
# # Setting this allows you to use `--seed` to deterministically reproduce
# # test failures related to randomization by passing the same `--seed` value
# # as the one that triggered the failure.
# Kernel.srand config.seed

# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random

# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed

# In suites with this setting, only RSpec.describe is valid as the first describe block
config.expose_dsl_globally = false
Expand Down

0 comments on commit b1a09a4

Please sign in to comment.