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 #7669] Add Bundler/GemVersion cop #9727

Merged
merged 8 commits into from May 5, 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/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'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw that other Bundler cops also include *.gemfile and gems.rb. I've personally never worked with this format of gem file, so I was unsure what exactly to test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format is exactly the same, it's just a different filename.

- '**/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