diff --git a/.rubocop.yml b/.rubocop.yml index 026dd222722..f531112f3ee 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -140,3 +140,10 @@ RSpec/StubbedMock: InternalAffairs/ExampleDescription: Include: - 'spec/rubocop/cop/**/*.rb' + +InternalAffairs/UndefinedConfig: + Include: + - 'lib/rubocop/cop/**/*.rb' + Exclude: + - 'lib/rubocop/cop/correctors/**/*.rb' + - 'lib/rubocop/cop/mixin/**/*.rb' diff --git a/config/default.yml b/config/default.yml index f797bb86da6..80e7bad773f 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1824,7 +1824,6 @@ Lint/MultipleComparison: Enabled: true VersionAdded: '0.47' VersionChanged: '1.1' - AllowMethodComparison: true Lint/NestedMethodDefinition: Description: 'Do not use nested method definitions.' @@ -3931,6 +3930,7 @@ Style/MultipleComparison: Enabled: true VersionAdded: '0.49' VersionChanged: '1.1' + AllowMethodComparison: true Style/MutableConstant: Description: 'Do not assign mutable objects to constants.' @@ -4154,6 +4154,7 @@ Style/OptionHash: - args - params - parameters + Allowlist: [] Style/OptionalArguments: Description: >- diff --git a/lib/rubocop/cop/internal_affairs.rb b/lib/rubocop/cop/internal_affairs.rb index b32ceca3673..6e3dae35771 100644 --- a/lib/rubocop/cop/internal_affairs.rb +++ b/lib/rubocop/cop/internal_affairs.rb @@ -13,4 +13,5 @@ require_relative 'internal_affairs/redundant_location_argument' require_relative 'internal_affairs/redundant_message_argument' require_relative 'internal_affairs/style_detected_api_use' +require_relative 'internal_affairs/undefined_config' require_relative 'internal_affairs/useless_message_assertion' diff --git a/lib/rubocop/cop/internal_affairs/undefined_config.rb b/lib/rubocop/cop/internal_affairs/undefined_config.rb new file mode 100644 index 00000000000..b097f30fd11 --- /dev/null +++ b/lib/rubocop/cop/internal_affairs/undefined_config.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module InternalAffairs + # Looks for references to a cop configuration key that isn't defined in config/default.yml. + class UndefinedConfig < Base + ALLOWED_CONFIGURATIONS = %w[ + Safe SafeAutoCorrect AutoCorrect Severity StyleGuide Details Reference Include Exclude + ].freeze + RESTRICT_ON_SEND = %i[[] fetch].freeze + MSG = '`%s` is not defined in the configuration for `%s` ' \ + 'in `config/default.yml`.' + + # @!method cop_class_def(node) + def_node_search :cop_class_def, <<~PATTERN + (class _ (const _ {:Base :Cop}) ...) + PATTERN + + # @!method cop_config_accessor?(node) + def_node_matcher :cop_config_accessor?, <<~PATTERN + (send (send nil? :cop_config) {:[] :fetch} ${str sym}...) + PATTERN + + def on_new_investigation + super + return unless processed_source.ast + + cop_class = cop_class_def(processed_source.ast).first + return unless (@cop_class_name = extract_cop_name(cop_class)) + + @config_for_cop = RuboCop::ConfigLoader.default_configuration.for_cop(@cop_class_name) + end + + def on_send(node) + return unless cop_class_name + return unless (config_name_node = cop_config_accessor?(node)) + return if always_allowed?(config_name_node) + return if configuration_key_defined?(config_name_node) + + message = format(MSG, name: config_name_node.value, cop: cop_class_name) + add_offense(config_name_node, message: message) + end + + private + + attr_reader :config_for_cop, :cop_class_name + + def extract_cop_name(class_node) + return unless class_node + + segments = [class_node].concat( + class_node.each_ancestor(:class, :module).take_while do |n| + n.identifier.short_name != :Cop + end + ) + + segments.reverse_each.map { |s| s.identifier.short_name }.join('/') + end + + def always_allowed?(node) + ALLOWED_CONFIGURATIONS.include?(node.value) + end + + def configuration_key_defined?(node) + config_for_cop.key?(node.value) + end + end + end + end +end diff --git a/spec/rubocop/cop/internal_affairs/undefined_config_spec.rb b/spec/rubocop/cop/internal_affairs/undefined_config_spec.rb new file mode 100644 index 00000000000..c26eb2fa941 --- /dev/null +++ b/spec/rubocop/cop/internal_affairs/undefined_config_spec.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::InternalAffairs::UndefinedConfig, :config, :isolated_environment do + include FileHelper + + before do + create_file('config/default.yml', <<~YAML) + Test/Foo: + Defined: true + + X/Y/Z: + Defined: true + YAML + + allow(RuboCop::ConfigLoader).to receive(:default_configuration).and_return( + RuboCop::ConfigLoader.load_file('config/default.yml', check: false) + ) + end + + it 'does not register an offense for implicit configuration keys' do + expect_no_offenses(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Base + def configured? + cop_config['Safe'] + cop_config['SafeAutoCorrect'] + cop_config['AutoCorrect'] + cop_config['Severity'] + end + end + end + end + end + RUBY + end + + it 'registers an offense when the cop has no configuration at all' do + expect_offense(<<~RUBY) + module RuboCop + module Cop + module Test + class Bar < Base + def configured? + cop_config['Missing'] + ^^^^^^^^^ `Missing` is not defined in the configuration for `Test/Bar` in `config/default.yml`. + end + end + end + end + end + RUBY + end + + it 'registers an offense when the cop is not within the `RuboCop::Cop` namespace' do + expect_offense(<<~RUBY) + module Test + class Foo < Base + def configured? + cop_config['Defined'] + cop_config['Missing'] + ^^^^^^^^^ `Missing` is not defined in the configuration for `Test/Foo` in `config/default.yml`. + end + end + end + RUBY + end + + context 'element lookup' do + it 'does not register an offense for defined configuration keys' do + expect_no_offenses(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Base + def configured? + cop_config['Defined'] + end + end + end + end + end + RUBY + end + + it 'registers an offense for missing configuration keys' do + expect_offense(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Base + def configured? + cop_config['Missing'] + ^^^^^^^^^ `Missing` is not defined in the configuration for `Test/Foo` in `config/default.yml`. + end + end + end + end + end + RUBY + end + end + + context 'fetch' do + it 'does not register an offense for defined configuration keys' do + expect_no_offenses(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Base + def configured? + cop_config.fetch('Defined') + end + end + end + end + end + RUBY + end + + it 'registers an offense for missing configuration keys' do + expect_offense(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Base + def configured? + cop_config.fetch('Missing') + ^^^^^^^^^ `Missing` is not defined in the configuration for `Test/Foo` in `config/default.yml`. + end + end + end + end + end + RUBY + end + + context 'with a default value' do + it 'does not register an offense for defined configuration keys' do + expect_no_offenses(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Base + def configured? + cop_config.fetch('Defined', default) + end + end + end + end + end + RUBY + end + + it 'registers an offense for missing configuration keys' do + expect_offense(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Base + def configured? + cop_config.fetch('Missing', default) + ^^^^^^^^^ `Missing` is not defined in the configuration for `Test/Foo` in `config/default.yml`. + end + end + end + end + end + RUBY + end + end + end + + it 'works with deeper nested cop names' do + expect_offense(<<~RUBY) + module RuboCop + module Cop + module X + module Y + class Z < Base + def configured? + cop_config['Defined'] + cop_config['Missing'] + ^^^^^^^^^ `Missing` is not defined in the configuration for `X/Y/Z` in `config/default.yml`. + end + end + end + end + end + end + RUBY + end + + it 'works when the base class is `Cop` instead of `Base`' do + expect_offense(<<~RUBY) + module RuboCop + module Cop + module Test + class Foo < Cop + def configured? + cop_config['Defined'] + cop_config['Missing'] + ^^^^^^^^^ `Missing` is not defined in the configuration for `Test/Foo` in `config/default.yml`. + end + end + end + end + end + RUBY + end + + it 'ignores `cop_config` in non-cop classes' do + expect_no_offenses(<<~RUBY) + class Test + def configured? + cop_config['Missing'] + end + end + RUBY + end + + it 'does not register an offense if using `cop_config` outside of a cop class' do + expect_no_offenses(<<~RUBY) + def configured? + cop_config['Missing'] + end + RUBY + end + + it 'can handle an empty file' do + expect_no_offenses('') + end +end