forked from rubocop/rubocop
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rubocop#9987 from dvandersluis/internal/undefined-…
…config Add `InternalAffairs/UndefinedConfig` cop.
- Loading branch information
Showing
5 changed files
with
315 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = '`%<name>s` is not defined in the configuration for `%<cop>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 |
234 changes: 234 additions & 0 deletions
234
spec/rubocop/cop/internal_affairs/undefined_config_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |