Skip to content

Commit

Permalink
[Fix rubocop#7669] Add Bundler/GemVersionDeclaration Cop
Browse files Browse the repository at this point in the history
  • Loading branch information
timlkelly committed Apr 22, 2021
1 parent 28e11ec commit 74b5bfb
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/new_gem_version_declaration.md
@@ -0,0 +1 @@
* [#7669](https://github.com/rubocop/rubocop/issues/7669): New cop `Bundler/GemVersionDeclaration` requires or prohibits specifying gem versions. ([@timlkelly][])
12 changes: 12 additions & 0 deletions config/default.yml
Expand Up @@ -174,6 +174,18 @@ Bundler/GemComment:
IgnoredGems: []
OnlyFor: []

Bundler/GemVersionDeclaration:
Description: 'Requires or prohibits gem version declarations.'
Enabled: false
VersionAdded: '<<next>>'
EnforcedStyle: 'required'
SupportedStyles:
- 'required'
- 'prohibited'
Include:
- '**/Gemfile'
IgnoredGems: []

Bundler/InsecureProtocolSource:
Description: >-
The source `:gemcutter`, `:rubygems` and `:rubyforge` are deprecated
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -148,6 +148,7 @@

require_relative 'rubocop/cop/bundler/duplicated_gem'
require_relative 'rubocop/cop/bundler/gem_comment'
require_relative 'rubocop/cop/bundler/gem_version_declaration'
require_relative 'rubocop/cop/bundler/insecure_protocol_source'
require_relative 'rubocop/cop/bundler/ordered_gems'

Expand Down
101 changes: 101 additions & 0 deletions lib/rubocop/cop/bundler/gem_version_declaration.rb
@@ -0,0 +1,101 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Bundler
# Enforce that Gem version declarations are either required
# or prohibited.
#
# @example EnforcedStyle: required (default)
# # bad
# gem 'rubocop'
#
# # good
# gem 'rubocop', '~> 1.12'
#
# # good
# gem 'rubocop', '>= 1.10.0'
#
# # good
# gem 'rubocop', '>= 1.5.0', '< 1.10.0'
#
# @example EnforcedStyle: prohibited
# # good
# gem 'rubocop'
#
# # bad
# gem 'rubocop', '~> 1.12'
#
# # bad
# gem 'rubocop', '>= 1.10.0'
#
# # bad
# gem 'rubocop', '>= 1.5.0', '< 1.10.0'
#
class GemVersionDeclaration < Base
include ConfigurableEnforcedStyle

REQUIRED_MSG = 'Gem version declaration is required.'
PROHIBITED_MSG = 'Gem version declaration is prohibited.'
VERSION_DECLARATION_REGEX = /^[~<>=]*\s?[0-9.]+/.freeze

# @!method gem_declaration?(node)
def_node_matcher :gem_declaration?, '(send nil? :gem str ...)'

# @!method includes_version_declaration?(node)
def_node_matcher :includes_version_declaration?, <<~PATTERN
(send nil? :gem <(str #version_declaration?) ...>)
PATTERN

def on_send(node)
return unless gem_declaration?(node)
return if ignored_gem?(node)

if offense?(node)
add_offense(node)
opposite_style_detected
else
correct_style_detected
end
end

private

def ignored_gem?(node)
ignored_gems.include?(node.first_argument.value)
end

def ignored_gems
Array(cop_config['IgnoredGems'])
end

def message(range)
gem_declaration = range.source

if required_style?
format(REQUIRED_MSG, gem_declaration: gem_declaration)
elsif prohibited_style?
format(PROHIBITED_MSG, gem_declaration: gem_declaration)
end
end

def offense?(node)
(required_style? && !includes_version_declaration?(node)) ||
(prohibited_style? && includes_version_declaration?(node))
end

def prohibited_style?
style == :prohibited
end

def required_style?
style == :required
end

def version_declaration?(expression)
expression.match?(VERSION_DECLARATION_REGEX)
end
end
end
end
end
71 changes: 71 additions & 0 deletions spec/rubocop/cop/bundler/gem_version_declaration_spec.rb
@@ -0,0 +1,71 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Bundler::GemVersionDeclaration, :config do
context 'when EnforcedStyle is set to required (default)' do
let(:cop_config) do
{
'EnforcedStyle' => 'required',
'IgnoredGems' => ['rspec']
}
end

it 'flags gems without a version declaration' do
expect_offense(<<~RUBY)
gem 'rubocop'
^^^^^^^^^^^^^ Gem version declaration is required.
gem 'rubocop', require: false
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Gem version declaration is required.
RUBY
end

it 'ignores gems with a declared version' do
expect_no_offenses(<<~RUBY)
gem 'rubocop', '>=1.10.0'
gem 'rubocop', '~> 1'
gem 'rubocop', '~> 1.12', require: false
gem 'rubocop', '>= 1.5.0', '< 1.10.0', git: 'https://github.com/rubocop/rubocop'
RUBY
end

it 'ignores gems included in IgnoredGems metadata' do
expect_no_offenses(<<~RUBY)
gem 'rspec'
RUBY
end
end

context 'when EnforcedStyle is set to prohibited' do
let(:cop_config) do
{
'EnforcedStyle' => 'prohibited',
'IgnoredGems' => ['rspec']
}
end

it 'flags gems with a version declaration' do
expect_offense(<<~RUBY)
gem 'rubocop', '~> 1'
^^^^^^^^^^^^^^^^^^^^^ Gem version declaration is prohibited.
gem 'rubocop', '>=1.10.0'
^^^^^^^^^^^^^^^^^^^^^^^^^ Gem version declaration is prohibited.
gem 'rubocop', '~> 1.12', require: false
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Gem version declaration is prohibited.
gem 'rubocop', '>= 1.5.0', '< 1.10.0', git: 'https://github.com/rubocop/rubocop'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Gem version declaration is prohibited.
RUBY
end

it 'ignores gems without a declared version' do
expect_no_offenses(<<~RUBY)
gem 'rubocop'
gem 'rubocop', require: false
RUBY
end

it 'ignores gems included in IgnoredGems metadata' do
expect_no_offenses(<<~RUBY)
gem 'rspec', '~> 3.10'
RUBY
end
end
end

0 comments on commit 74b5bfb

Please sign in to comment.