/
config_validator.rb
247 lines (192 loc) · 8.45 KB
/
config_validator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# frozen_string_literal: true
require 'pathname'
module RuboCop
# Handles validation of configuration, for example cop names, parameter
# names, and Ruby versions.
class ConfigValidator
extend Forwardable
# @api private
COMMON_PARAMS = %w[Exclude Include Severity inherit_mode AutoCorrect StyleGuide Details].freeze
# @api private
INTERNAL_PARAMS = %w[Description StyleGuide
VersionAdded VersionChanged VersionRemoved
Reference Safe SafeAutoCorrect].freeze
# @api private
NEW_COPS_VALUES = %w[pending disable enable].freeze
# @api private
CONFIG_CHECK_KEYS = %w[Enabled Safe SafeAutoCorrect AutoCorrect].to_set.freeze
CONFIG_CHECK_DEPARTMENTS = %w[pending override_department].freeze
private_constant :CONFIG_CHECK_KEYS, :CONFIG_CHECK_DEPARTMENTS
def_delegators :@config, :smart_loaded_path, :for_all_cops
def initialize(config)
@config = config
@config_obsoletion = ConfigObsoletion.new(config)
@target_ruby = TargetRuby.new(config)
end
def validate
check_cop_config_value(@config)
reject_conflicting_safe_settings
# Don't validate RuboCop's own files further. Avoids infinite recursion.
return if @config.internal?
valid_cop_names, invalid_cop_names = @config.keys.partition do |key|
ConfigLoader.default_configuration.key?(key)
end
check_obsoletions
alert_about_unrecognized_cops(invalid_cop_names)
check_target_ruby
validate_new_cops_parameter
validate_parameter_names(valid_cop_names)
validate_enforced_styles(valid_cop_names)
validate_syntax_cop
reject_mutually_exclusive_defaults
end
def target_ruby_version
target_ruby.version
end
def validate_section_presence(name)
return unless @config.key?(name) && @config[name].nil?
raise ValidationError, "empty section #{name} found in #{smart_loaded_path}"
end
private
attr_reader :target_ruby
def check_obsoletions
@config_obsoletion.reject_obsolete!
return unless @config_obsoletion.warnings.any?
warn Rainbow("Warning: #{@config_obsoletion.warnings.join("\n")}").yellow
end
def check_target_ruby
return if target_ruby.supported?
source = target_ruby.source
last_version = target_ruby.rubocop_version_with_support
msg = if last_version
"RuboCop found unsupported Ruby version #{target_ruby_version} " \
"in #{source}. #{target_ruby_version}-compatible " \
"analysis was dropped after version #{last_version}."
else
'RuboCop found unknown Ruby version ' \
"#{target_ruby_version.inspect} in #{source}."
end
msg += "\nSupported versions: #{TargetRuby.supported_versions.join(', ')}"
raise ValidationError, msg
end
def alert_about_unrecognized_cops(invalid_cop_names)
unknown_cops = []
invalid_cop_names.each do |name|
# There could be a custom cop with this name. If so, don't warn
next if Cop::Registry.global.contains_cop_matching?([name])
# Special case for inherit_mode, which is a directive that we keep in
# the configuration (even though it's not a cop), because it's easier
# to do so than to pass the value around to various methods.
next if name == 'inherit_mode'
message = <<~MESSAGE.rstrip
unrecognized cop or department #{name} found in #{smart_loaded_path}
#{suggestion(name)}
MESSAGE
unknown_cops << message
end
raise ValidationError, unknown_cops.join("\n") if unknown_cops.any?
end
def suggestion(name)
registry = Cop::Registry.global
departments = registry.departments.map(&:to_s)
suggestions = NameSimilarity.find_similar_names(name, departments + registry.map(&:cop_name))
if suggestions.any?
"Did you mean `#{suggestions.join('`, `')}`?"
else
# Department names can contain slashes, e.g. Chef/Correctness, but there's no support for
# the concept of higher level departments in RuboCop. It's a flat structure. So if the user
# tries to configure a "top level department", we hint that it's the bottom level
# departments that should be configured.
suggestions = departments.select { |department| department.start_with?("#{name}/") }
"#{name} is not a department. Use `#{suggestions.join('`, `')}`." if suggestions.any?
end
end
def validate_syntax_cop
syntax_config = @config['Lint/Syntax']
default_config = ConfigLoader.default_configuration['Lint/Syntax']
return unless syntax_config && default_config.merge(syntax_config) != default_config
raise ValidationError,
"configuration for Syntax cop found in #{smart_loaded_path}\n" \
'It\'s not possible to disable this cop.'
end
def validate_new_cops_parameter
new_cop_parameter = @config.for_all_cops['NewCops']
return if new_cop_parameter.nil? || NEW_COPS_VALUES.include?(new_cop_parameter)
message = "invalid #{new_cop_parameter} for `NewCops` found in" \
"#{smart_loaded_path}\n" \
"Valid choices are: #{NEW_COPS_VALUES.join(', ')}"
raise ValidationError, message
end
def validate_parameter_names(valid_cop_names)
valid_cop_names.each do |name|
validate_section_presence(name)
each_invalid_parameter(name) do |param, supported_params|
warn Rainbow(<<~MESSAGE).yellow
Warning: #{name} does not support #{param} parameter.
Supported parameters are:
- #{supported_params.join("\n - ")}
MESSAGE
end
end
end
def each_invalid_parameter(cop_name)
default_config = ConfigLoader.default_configuration[cop_name]
@config[cop_name].each_key do |param|
next if COMMON_PARAMS.include?(param) || default_config.key?(param)
supported_params = default_config.keys - INTERNAL_PARAMS
yield param, supported_params
end
end
def validate_enforced_styles(valid_cop_names) # rubocop:todo Metrics/AbcSize
valid_cop_names.each do |name|
styles = @config[name].select { |key, _| key.start_with?('Enforced') }
styles.each do |style_name, style|
supported_key = RuboCop::Cop::Util.to_supported_styles(style_name)
valid = ConfigLoader.default_configuration[name][supported_key]
next unless valid
next if valid.include?(style)
next if validate_support_and_has_list(name, style, valid)
msg = "invalid #{style_name} '#{style}' for #{name} found in " \
"#{smart_loaded_path}\n" \
"Valid choices are: #{valid.join(', ')}"
raise ValidationError, msg
end
end
end
def validate_support_and_has_list(name, formats, valid)
ConfigLoader.default_configuration[name]['AllowMultipleStyles'] &&
formats.is_a?(Array) &&
formats.all? { |format| valid.include?(format) }
end
def reject_mutually_exclusive_defaults
disabled_by_default = for_all_cops['DisabledByDefault']
enabled_by_default = for_all_cops['EnabledByDefault']
return unless disabled_by_default && enabled_by_default
msg = 'Cops cannot be both enabled by default and disabled by default'
raise ValidationError, msg
end
def reject_conflicting_safe_settings
@config.each do |name, cop_config|
next unless cop_config.is_a?(Hash)
next unless cop_config['Safe'] == false && cop_config['SafeAutoCorrect'] == true
msg = 'Unsafe cops cannot have a safe auto-correction ' \
"(section #{name} in #{smart_loaded_path})"
raise ValidationError, msg
end
end
def check_cop_config_value(hash, parent = nil)
hash.each do |key, value|
check_cop_config_value(value, key) if value.is_a?(Hash)
next unless CONFIG_CHECK_KEYS.include?(key) && value.is_a?(String)
next if key == 'Enabled' && CONFIG_CHECK_DEPARTMENTS.include?(value)
raise ValidationError, msg_not_boolean(parent, key, value)
end
end
# FIXME: Handling colors in exception messages like this is ugly.
def msg_not_boolean(parent, key, value)
"#{Rainbow('').reset}" \
"Property #{Rainbow(key).yellow} of cop #{Rainbow(parent).yellow}" \
" is supposed to be a boolean and #{Rainbow(value).yellow} is not."
end
end
end