diff --git a/changelog/new_bundler_gem_comment_limiting_version_specifications.md b/changelog/new_bundler_gem_comment_limiting_version_specifications.md new file mode 100644 index 00000000000..6e2a4afbda5 --- /dev/null +++ b/changelog/new_bundler_gem_comment_limiting_version_specifications.md @@ -0,0 +1 @@ +* [#9358](https://github.com/rubocop/rubocop/pull/9358): Support `restrictive_version_specificiers` option in `Bundler/GemComment` cop. ([@RobinDaugherty][]) diff --git a/lib/rubocop/cop/bundler/gem_comment.rb b/lib/rubocop/cop/bundler/gem_comment.rb index d29c6bd0076..277a599933f 100644 --- a/lib/rubocop/cop/bundler/gem_comment.rb +++ b/lib/rubocop/cop/bundler/gem_comment.rb @@ -3,15 +3,24 @@ module RuboCop module Cop module Bundler - # Add a comment describing each gem in your Gemfile. + # Each gem in the Gemfile should have a comment explaining + # its purpose in the project, or the reason for its version + # or source. # - # Optionally, the "OnlyFor" configuration + # The optional "OnlyFor" configuration array # can be used to only register offenses when the gems # use certain options or have version specifiers. - # Add "version_specifiers" and/or the gem option names - # you want to check. # - # A useful use-case is to enforce a comment when using + # When "version_specifiers" is included, a comment + # will be enforced if the gem has any version specifier. + # + # When "restrictive_version_specifiers" is included, a comment + # will be enforced if the gem has a version specifier that + # holds back the version of the gem. + # + # For any other value in the array, a comment will be enforced for + # a gem if an option by the same name is present. + # A useful use case is to enforce a comment when using # options that change the source of a gem: # # - `bitbucket` @@ -21,7 +30,8 @@ module Bundler # - `source` # # For a full list of options supported by bundler, - # you can check the https://bundler.io/man/gemfile.5.html[official documentation]. + # see https://bundler.io/man/gemfile.5.html + # . # # @example OnlyFor: [] (default) # # bad @@ -43,6 +53,18 @@ module Bundler # # Version 2.1 introduces breaking change baz # gem 'foo', '< 2.1' # + # @example OnlyFor: ['restrictive_version_specifiers'] + # # bad + # + # gem 'foo', '< 2.1' + # + # # good + # + # gem 'foo', '>= 1.0' + # + # # Version 2.1 introduces breaking change baz + # gem 'foo', '< 2.1' + # # @example OnlyFor: ['version_specifiers', 'github'] # # bad # @@ -64,6 +86,8 @@ class GemComment < Base MSG = 'Missing gem description comment.' CHECKED_OPTIONS_CONFIG = 'OnlyFor' VERSION_SPECIFIERS_OPTION = 'version_specifiers' + RESTRICTIVE_VERSION_SPECIFIERS_OPTION = 'restrictive_version_specifiers' + RESTRICTIVE_VERSION_PATTERN = /<|~>/.freeze RESTRICT_ON_SEND = %i[gem].freeze # @!method gem_declaration?(node) @@ -113,6 +137,8 @@ def ignored_gem?(node) def checked_options_present?(node) (cop_config[CHECKED_OPTIONS_CONFIG].include?(VERSION_SPECIFIERS_OPTION) && version_specified_gem?(node)) || + (cop_config[CHECKED_OPTIONS_CONFIG].include?(RESTRICTIVE_VERSION_SPECIFIERS_OPTION) && + restrictive_version_specified_gem?(node)) || contains_checked_options?(node) end @@ -123,6 +149,15 @@ def version_specified_gem?(node) node.arguments[1]&.str_type? end + # Version specifications that restrict all updates going forward. This excludes versions + # like ">= 1.0" or "!= 2.0.3". + def restrictive_version_specified_gem?(node) + return unless version_specified_gem?(node) + + node.arguments + .any? { |arg| arg&.str_type? && RESTRICTIVE_VERSION_PATTERN.match?(arg.to_s) } + end + def contains_checked_options?(node) (Array(cop_config[CHECKED_OPTIONS_CONFIG]) & gem_options(node).map(&:to_s)).any? end diff --git a/spec/rubocop/cop/bundler/gem_comment_spec.rb b/spec/rubocop/cop/bundler/gem_comment_spec.rb index 25fc928e021..b6524d15041 100644 --- a/spec/rubocop/cop/bundler/gem_comment_spec.rb +++ b/spec/rubocop/cop/bundler/gem_comment_spec.rb @@ -74,7 +74,7 @@ context 'when the "OnlyFor" option is set' do before { cop_config['OnlyFor'] = checked_options } - context 'when the version specifiers are checked' do + context 'including "version_specifiers"' do let(:checked_options) { ['version_specifiers'] } context 'when a gem is commented' do @@ -86,7 +86,7 @@ end end - context 'when a gem is uncommented and has no extra options' do + context 'when a gem is uncommented and has no version specified' do it 'does not register an offense' do expect_no_offenses(<<-GEM, 'Gemfile') gem 'rubocop' @@ -120,7 +120,7 @@ end end - context 'when a gem is uncommented and has a version specifier along with unrelated options' do + context 'when a gem is uncommented and has a version specifier along with other options' do it 'registers an offense' do expect_offense(<<-GEM, 'Gemfile') gem 'rubocop', '~> 12.0', required: true @@ -130,10 +130,74 @@ end end - context 'and some other options are checked' do + context 'including "restrictive_version_specifiers"' do + let(:checked_options) { ['restrictive_version_specifiers'] } + + context 'when a gem is commented' do + it 'does not register an offense' do + expect_no_offenses(<<~RUBY, 'Gemfile') + # Style-guide enforcer. + gem 'rubocop' + RUBY + end + end + + context 'when a gem is uncommented and has no version specified' do + it 'does not register an offense' do + expect_no_offenses(<<-GEM, 'Gemfile') + gem 'rubocop' + GEM + end + end + + context 'when a gem is uncommented and has options but no version specifiers' do + it 'does not register an offense' do + expect_no_offenses(<<-GEM, 'Gemfile') + gem 'rubocop', group: development + GEM + end + end + + context 'when a gem is uncommented and has only a minimum version specifier' do + it 'does not register an offense' do + expect_no_offenses(<<-GEM, 'Gemfile') + gem 'rubocop', '>= 12.0' + GEM + end + end + + context 'when a gem is uncommented and has a version specifier' do + it 'registers an offense' do + expect_offense(<<-GEM, 'Gemfile') + gem 'rubocop', '~> 12.0' + ^^^^^^^^^^^^^^^^^^^^^^^^ Missing gem description comment. + GEM + end + end + + context 'when a gem is uncommented and has both minimum and non-minimum version specifier' do + it 'registers an offense' do + expect_offense(<<-GEM, 'Gemfile') + gem 'rubocop', '~> 12.0', '>= 11.0' + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Missing gem description comment. + GEM + end + end + + context 'when a gem is uncommented and has a version specifier along with other options' do + it 'registers an offense' do + expect_offense(<<-GEM, 'Gemfile') + gem 'rubocop', '~> 12.0', required: true + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Missing gem description comment. + GEM + end + end + end + + context 'including one or more option names but not "version_specifiers"' do let(:checked_options) { %w[github required] } - context 'when a gem is uncommented and has one of the checked options' do + context 'when a gem is uncommented and has one of the specified options' do it 'registers an offense' do expect_offense(<<-GEM, 'Gemfile') gem 'rubocop', github: 'some_user/some_fork' @@ -142,7 +206,7 @@ end end - context 'when a gem is uncommented and has a version specifier but no other options' do + context 'when a gem is uncommented and has a version specifier but none of the specified options' do it 'does not register an offense' do expect_no_offenses(<<-GEM, 'Gemfile') gem 'rubocop', '~> 12.0' @@ -150,7 +214,7 @@ end end - context 'when a gem is uncommented and only unchecked options' do + context 'when a gem is uncommented and containts only options not specified' do it 'does not register an offense' do expect_no_offenses(<<-GEM, 'Gemfile') gem 'rubocop', group: development