Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix #9580] Add New Cop to Enforce Bundler Gem filename #9903

Merged
merged 4 commits into from Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/fix_enforce_gem_file_name.md
@@ -0,0 +1 @@
* [#9580](https://github.com/rubocop/rubocop/issues/9580): Add a new cop that enforces which bundler gem file to use. ([@gregfletch][])
14 changes: 14 additions & 0 deletions config/default.yml
Expand Up @@ -174,6 +174,20 @@ Bundler/GemComment:
IgnoredGems: []
OnlyFor: []

Bundler/GemFilename:
Description: 'Enforces the filename for managing gems.'
Enabled: true
VersionAdded: '<<next>>'
EnforcedStyle: 'Gemfile'
SupportedStyles:
- 'Gemfile'
- 'gems.rb'
dvandersluis marked this conversation as resolved.
Show resolved Hide resolved
Include:
- '**/Gemfile'
- '**/gems.rb'
- '**/Gemfile.lock'
- '**/gems.locked'

Bundler/GemVersion:
Description: 'Requires or forbids specifying gem versions.'
Enabled: false
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -152,6 +152,7 @@

require_relative 'rubocop/cop/bundler/duplicated_gem'
require_relative 'rubocop/cop/bundler/gem_comment'
require_relative 'rubocop/cop/bundler/gem_filename'
require_relative 'rubocop/cop/bundler/gem_version'
require_relative 'rubocop/cop/bundler/insecure_protocol_source'
require_relative 'rubocop/cop/bundler/ordered_gems'
Expand Down
103 changes: 103 additions & 0 deletions lib/rubocop/cop/bundler/gem_filename.rb
@@ -0,0 +1,103 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Bundler
# This cop verifies that a project contains Gemfile or gems.rb file and correct
# associated lock file based on the configuration.
#
# @example EnforcedStyle: Gemfile (default)
# # bad
# Project contains gems.rb and gems.locked files
#
# # bad
# Project contains Gemfile and gems.locked file
#
# # good
# Project contains Gemfile and Gemfile.lock
#
# @example EnforcedStyle: gems.rb
# # bad
# Project contains Gemfile and Gemfile.lock files
#
# # bad
# Project contains gems.rb and Gemfile.lock file
#
# # good
# Project contains gems.rb and gems.locked files
class GemFilename < Base
include ConfigurableEnforcedStyle
include RangeHelp

MSG_GEMFILE_REQUIRED = '`gems.rb` file was found but `Gemfile` is required '\
'(file path: %<file_path>s).'
MSG_GEMS_RB_REQUIRED = '`Gemfile` was found but `gems.rb` file is required '\
'(file path: %<file_path>s).'
MSG_GEMFILE_MISMATCHED = 'Expected a `Gemfile.lock` with `Gemfile` but found '\
'`gems.locked` file (file path: %<file_path>s).'
MSG_GEMS_RB_MISMATCHED = 'Expected a `gems.locked` file with `gems.rb` but found '\
'`Gemfile.lock` (file path: %<file_path>s).'
GEMFILE_FILES = %w[Gemfile Gemfile.lock].freeze
GEMS_RB_FILES = %w[gems.rb gems.locked].freeze

def on_new_investigation
file_path = processed_source.file_path
basename = File.basename(file_path)
return if expected_gemfile?(basename)

register_offense(file_path, basename)
end

private

def register_offense(file_path, basename)
register_gemfile_offense(file_path, basename) if gemfile_offense?(basename)
register_gems_rb_offense(file_path, basename) if gems_rb_offense?(basename)
end

def register_gemfile_offense(file_path, basename)
message = case basename
when 'gems.rb'
MSG_GEMFILE_REQUIRED
when 'gems.locked'
MSG_GEMFILE_MISMATCHED
end

add_global_offense(format(message, file_path: file_path))
end

def register_gems_rb_offense(file_path, basename)
message = case basename
when 'Gemfile'
MSG_GEMS_RB_REQUIRED
when 'Gemfile.lock'
MSG_GEMS_RB_MISMATCHED
end

add_global_offense(format(message, file_path: file_path))
end

def gemfile_offense?(basename)
gemfile_required? && GEMS_RB_FILES.include?(basename)
end

def gems_rb_offense?(basename)
gems_rb_required? && GEMFILE_FILES.include?(basename)
end

def expected_gemfile?(basename)
(gemfile_required? && GEMFILE_FILES.include?(basename)) ||
(gems_rb_required? && GEMS_RB_FILES.include?(basename))
end

def gemfile_required?
style == :Gemfile
end

def gems_rb_required?
style == :'gems.rb'
end
end
end
end
end
139 changes: 139 additions & 0 deletions spec/rubocop/cop/bundler/gem_filename_spec.rb
@@ -0,0 +1,139 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Bundler::GemFilename, :config do
shared_examples_for 'invalid gem file' do |message|
it 'registers an offense' do
offenses = _investigate(cop, processed_source)

expect(offenses.size).to eq(1)
expect(offenses.first.message).to eq(message)
end
end

shared_examples_for 'valid gem file' do
it 'does not register an offense' do
offenses = _investigate(cop, processed_source)

expect(offenses.size).to eq(0)
end
end

context 'with default configuration (EnforcedStyle => `Gemfile`)' do
let(:source) { 'print 1' }
let(:processed_source) { parse_source(source) }

before { allow(processed_source.buffer).to receive(:name).and_return(filename) }

context 'with gems.rb file path' do
let(:filename) { 'gems.rb' }

include_examples 'invalid gem file',
'`gems.rb` file was found but `Gemfile` is required '\
'(file path: gems.rb).'
end

context 'with non-root gems.rb file path' do
let(:filename) { 'spec/gems.rb' }

include_examples 'invalid gem file',
'`gems.rb` file was found but `Gemfile` is required '\
'(file path: spec/gems.rb).'
end

context 'with gems.locked file path' do
let(:filename) { 'gems.locked' }

include_examples 'invalid gem file',
'Expected a `Gemfile.lock` with `Gemfile` but found `gems.locked` file '\
'(file path: gems.locked).'
end

context 'with non-root gems.locked file path' do
let(:filename) { 'spec/gems.locked' }

include_examples 'invalid gem file',
'Expected a `Gemfile.lock` with `Gemfile` but found `gems.locked` file '\
'(file path: spec/gems.locked).'
end

context 'with Gemfile file path' do
let(:filename) { 'Gemfile' }

include_examples 'valid gem file'
end

context 'with non-root Gemfile file path' do
let(:filename) { 'spec/Gemfile' }

include_examples 'valid gem file'
end

context 'with Gemfile.lock file path' do
let(:filename) { 'Gemfile.lock' }

include_examples 'valid gem file'
end

context 'with non-root Gemfile.lock file path' do
let(:filename) { 'spec/Gemfile.lock' }

include_examples 'valid gem file'
end
end

context 'with EnforcedStyle set to `gems.rb`' do
let(:source) { 'print 1' }
let(:processed_source) { parse_source(source) }
let(:cop_config) { { 'EnforcedStyle' => 'gems.rb' } }

before { allow(processed_source.buffer).to receive(:name).and_return(filename) }

context 'with Gemfile file path' do
let(:filename) { 'Gemfile' }

include_examples 'invalid gem file', '`Gemfile` was found but `gems.rb` file is required '\
'(file path: Gemfile).'
end

context 'with non-root Gemfile file path' do
let(:filename) { 'spec/Gemfile' }

include_examples 'invalid gem file', '`Gemfile` was found but `gems.rb` file is required '\
'(file path: spec/Gemfile).'
end

context 'with Gemfile.lock file path' do
let(:filename) { 'Gemfile.lock' }

include_examples 'invalid gem file',
'Expected a `gems.locked` file with `gems.rb` but found `Gemfile.lock` '\
'(file path: Gemfile.lock).'
end

context 'with non-root Gemfile.lock file path' do
let(:filename) { 'spec/Gemfile.lock' }

include_examples 'invalid gem file',
'Expected a `gems.locked` file with `gems.rb` but found `Gemfile.lock` '\
'(file path: spec/Gemfile.lock).'
end

context 'with gems.rb file path' do
let(:filename) { 'gems.rb' }

include_examples 'valid gem file'
end

context 'with non-root gems.rb file path' do
let(:filename) { 'spec/gems.rb' }

include_examples 'valid gem file'
end

context 'with non-root gems.locked file path' do
let(:filename) { 'spec/gems.locked' }

include_examples 'valid gem file'
end
end
end