Skip to content

Commit

Permalink
[Fix #7669] Add Bundler/GemVersion cop (#9727)
Browse files Browse the repository at this point in the history
  • Loading branch information
timlkelly committed May 5, 2021
1 parent 8384d34 commit 762655e
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 3 deletions.
1 change: 1 addition & 0 deletions changelog/new_gem_version.md
@@ -0,0 +1 @@
* [#7669](https://github.com/rubocop/rubocop/issues/7669): New cop `Bundler/GemVersion` requires or forbids specifying gem versions. ([@timlkelly][])
14 changes: 14 additions & 0 deletions config/default.yml
Expand Up @@ -174,6 +174,20 @@ Bundler/GemComment:
IgnoredGems: []
OnlyFor: []

Bundler/GemVersion:
Description: 'Requires or forbids specifying gem versions.'
Enabled: false
VersionAdded: '<<next>>'
EnforcedStyle: 'required'
SupportedStyles:
- 'required'
- 'forbidden'
Include:
- '**/*.gemfile'
- '**/Gemfile'
- '**/gems.rb'
AllowedGems: []

Bundler/InsecureProtocolSource:
Description: >-
The source `:gemcutter`, `:rubygems` and `:rubyforge` are deprecated
Expand Down
2 changes: 2 additions & 0 deletions lib/rubocop.rb
Expand Up @@ -82,6 +82,7 @@
require_relative 'rubocop/cop/mixin/enforce_superclass'
require_relative 'rubocop/cop/mixin/first_element_line_break'
require_relative 'rubocop/cop/mixin/frozen_string_literal'
require_relative 'rubocop/cop/mixin/gem_declaration'
require_relative 'rubocop/cop/mixin/hash_alignment_styles'
require_relative 'rubocop/cop/mixin/hash_transform_method'
require_relative 'rubocop/cop/mixin/ignored_pattern'
Expand Down Expand Up @@ -148,6 +149,7 @@

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

Expand Down
4 changes: 1 addition & 3 deletions lib/rubocop/cop/bundler/gem_comment.rb
Expand Up @@ -82,6 +82,7 @@ module Bundler
#
class GemComment < Base
include DefNode
include GemDeclaration

MSG = 'Missing gem description comment.'
CHECKED_OPTIONS_CONFIG = 'OnlyFor'
Expand All @@ -90,9 +91,6 @@ class GemComment < Base
RESTRICTIVE_VERSION_PATTERN = /<|~>/.freeze
RESTRICT_ON_SEND = %i[gem].freeze

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

def on_send(node)
return unless gem_declaration?(node)
return if ignored_gem?(node)
Expand Down
99 changes: 99 additions & 0 deletions lib/rubocop/cop/bundler/gem_version.rb
@@ -0,0 +1,99 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Bundler
# Enforce that Gem version specifications are either required
# or forbidden.
#
# @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: forbidden
# # good
# gem 'rubocop'
#
# # bad
# gem 'rubocop', '~> 1.12'
#
# # bad
# gem 'rubocop', '>= 1.10.0'
#
# # bad
# gem 'rubocop', '>= 1.5.0', '< 1.10.0'
#
class GemVersion < Base
include ConfigurableEnforcedStyle
include GemDeclaration

REQUIRED_MSG = 'Gem version specification is required.'
FORBIDDEN_MSG = 'Gem version specification is forbidden.'
VERSION_SPECIFICATION_REGEX = /^\s*[~<>=]*\s*[0-9.]+/.freeze

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

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

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

private

def allowed_gem?(node)
allowed_gems.include?(node.first_argument.value)
end

def allowed_gems
Array(cop_config['AllowedGems'])
end

def message(range)
gem_specification = range.source

if required_style?
format(REQUIRED_MSG, gem_specification: gem_specification)
elsif forbidden_style?
format(FORBIDDEN_MSG, gem_specification: gem_specification)
end
end

def offense?(node)
(required_style? && !includes_version_specification?(node)) ||
(forbidden_style? && includes_version_specification?(node))
end

def forbidden_style?
style == :forbidden
end

def required_style?
style == :required
end

def version_specification?(expression)
expression.match?(VERSION_SPECIFICATION_REGEX)
end
end
end
end
end
13 changes: 13 additions & 0 deletions lib/rubocop/cop/mixin/gem_declaration.rb
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module RuboCop
module Cop
# Common functionality for checking gem declarations.
module GemDeclaration
extend NodePattern::Macros

# @!method gem_declaration?(node)
def_node_matcher :gem_declaration?, '(send nil? :gem str ...)'
end
end
end
71 changes: 71 additions & 0 deletions spec/rubocop/cop/bundler/gem_version_spec.rb
@@ -0,0 +1,71 @@
# frozen_string_literal: true

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

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

it 'does not flag gems with a specified 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 'does not flag gems included in AllowedGems metadata' do
expect_no_offenses(<<~RUBY)
gem 'rspec'
RUBY
end
end

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

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

it 'does not flag gems without a specified version' do
expect_no_offenses(<<~RUBY)
gem 'rubocop'
gem 'rubocop', require: false
RUBY
end

it 'does not flag gems included in AllowedGems metadata' do
expect_no_offenses(<<~RUBY)
gem 'rspec', '~> 3.10'
RUBY
end
end
end

0 comments on commit 762655e

Please sign in to comment.