From baba941f1b755b9823cd0874ff4d79e5af3d4df0 Mon Sep 17 00:00:00 2001 From: Greg Fletcher Date: Thu, 26 Aug 2021 03:22:56 -0400 Subject: [PATCH] [Fix #9580] Add New Cop to Enforce Bundler Gem filename (#9903) Add a new cop which enforces which bundler gem filename to use. By default, enforces the presence of Gemfile and its related Gemfile.lock file. Alternatively, setting EnforcedStyle to gems.rb enforces gems.rb and its related gems.locked file. --- changelog/fix_enforce_gem_file_name.md | 1 + config/default.yml | 14 ++ lib/rubocop.rb | 1 + lib/rubocop/cop/bundler/gem_filename.rb | 103 +++++++++++++ spec/rubocop/cop/bundler/gem_filename_spec.rb | 139 ++++++++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 changelog/fix_enforce_gem_file_name.md create mode 100644 lib/rubocop/cop/bundler/gem_filename.rb create mode 100644 spec/rubocop/cop/bundler/gem_filename_spec.rb 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 f58940e7994..d614ae5c95a 100644 --- a/config/default.yml +++ b/config/default.yml @@ -174,6 +174,20 @@ Bundler/GemComment: IgnoredGems: [] OnlyFor: [] +Bundler/GemFilename: + Description: 'Enforces the filename for managing gems.' + Enabled: true + VersionAdded: '<>' + EnforcedStyle: 'Gemfile' + SupportedStyles: + - 'Gemfile' + - 'gems.rb' + Include: + - '**/Gemfile' + - '**/gems.rb' + - '**/Gemfile.lock' + - '**/gems.locked' + Bundler/GemVersion: Description: 'Requires or forbids specifying gem versions.' Enabled: false diff --git a/lib/rubocop.rb b/lib/rubocop.rb index a96137c1b6c..ab6171ba700 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..2c0536ded0e --- /dev/null +++ b/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: %s).' + MSG_GEMS_RB_REQUIRED = '`Gemfile` was found but `gems.rb` file is required '\ + '(file path: %s).' + MSG_GEMFILE_MISMATCHED = 'Expected a `Gemfile.lock` with `Gemfile` but found '\ + '`gems.locked` file (file path: %s).' + MSG_GEMS_RB_MISMATCHED = 'Expected a `gems.locked` file with `gems.rb` but found '\ + '`Gemfile.lock` (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 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..f212cb95001 --- /dev/null +++ b/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