diff --git a/changelog/fix_enforce_gem_file_name.md b/changelog/fix_enforce_gem_file_name.md new file mode 100644 index 00000000000..0466e6e1aa9 --- /dev/null +++ b/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][]) diff --git a/config/default.yml b/config/default.yml index 53e201581c4..780410b1ae2 100644 --- a/config/default.yml +++ b/config/default.yml @@ -174,6 +174,12 @@ Bundler/GemComment: IgnoredGems: [] OnlyFor: [] +Bundler/GemFilename: + Description: 'Enforces the filename for managing gems.' + Enabled: true + VersionAdded: '<>' + RequiresGemfile: true + Bundler/GemVersion: Description: 'Requires or forbids specifying gem versions.' Enabled: false diff --git a/lib/rubocop.rb b/lib/rubocop.rb index 7dc7b4dcc06..ad2799d3ed3 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -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' diff --git a/lib/rubocop/cop/bundler/gem_filename.rb b/lib/rubocop/cop/bundler/gem_filename.rb new file mode 100644 index 00000000000..68e187c1256 --- /dev/null +++ b/lib/rubocop/cop/bundler/gem_filename.rb @@ -0,0 +1,90 @@ +# 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 RequiresGemfile: true (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 RequiresGemfile: false + # # 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 RangeHelp + + MSG_GEMFILE_REQUIRED = 'gems.rb file was found but Gemfile is required.' + MSG_GEMS_RB_REQUIRED = 'Gemfile was found but gems.rb file is required.' + MSG_GEMFILE_MISMATCHED = 'Expected a Gemfile.lock with Gemfile but found gems.locked file.' + MSG_GEMS_RB_MISMATCHED = 'Expected a gems.locked file with gems.rb but found Gemfile.lock.' + GEMFILE_REQUIRED_CONFIG = 'RequiresGemfile' + 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 + return if expected_gemfile?(file_path) + + register_offense(processed_source, file_path) + end + + private + + def register_offense(processed_source, file_path) + register_gemfile_offense(processed_source, file_path) if gemfile_required? + register_gems_rb_offense(processed_source, file_path) unless gemfile_required? + end + + def register_gemfile_offense(processed_source, file_path) + message = case file_path + when 'gems.rb' + MSG_GEMFILE_REQUIRED + when 'gems.locked' + MSG_GEMFILE_MISMATCHED + end + + return if message.nil? + + add_offense(source_range(processed_source.buffer, 1, 0), message: message) + end + + def register_gems_rb_offense(processed_source, file_path) + message = case file_path + when 'Gemfile' + MSG_GEMS_RB_REQUIRED + when 'Gemfile.lock' + MSG_GEMS_RB_MISMATCHED + end + + return if message.nil? + + add_offense(source_range(processed_source.buffer, 1, 0), message: message) + end + + def expected_gemfile?(file_path) + (gemfile_required? && GEMFILE_FILES.include?(file_path)) || + (!gemfile_required? && GEMS_RB_FILES.include?(file_path)) + end + + def gemfile_required? + cop_config[GEMFILE_REQUIRED_CONFIG] + end + end + end + end +end diff --git a/spec/rubocop/cop/bundler/gem_filename_spec.rb b/spec/rubocop/cop/bundler/gem_filename_spec.rb new file mode 100644 index 00000000000..5508f7c297b --- /dev/null +++ b/spec/rubocop/cop/bundler/gem_filename_spec.rb @@ -0,0 +1,85 @@ +# 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' 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.' + 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.' + end + + context 'with Gemfile file path' do + let(:filename) { 'Gemfile' } + + include_examples 'valid gem file' + end + + context 'with Gemfile.lock file path' do + let(:filename) { 'Gemfile.lock' } + + include_examples 'valid gem file' + end + end + + context 'with RequiresGemfile set to false' do + let(:source) { 'print 1' } + let(:processed_source) { parse_source(source) } + let(:cop_config) { { 'RequiresGemfile' => false } } + + 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.' + 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.' + end + + context 'with gems.rb file path' do + let(:filename) { 'gems.rb' } + + include_examples 'valid gem file' + end + + context 'with gems.locked file path' do + let(:filename) { 'gems.locked' } + + include_examples 'valid gem file' + end + end +end