/
config_loader_resolver.rb
277 lines (228 loc) · 10.6 KB
/
config_loader_resolver.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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# frozen_string_literal: true
require 'yaml'
require 'pathname'
module RuboCop
# A help class for ConfigLoader that handles configuration resolution.
# @api private
class ConfigLoaderResolver
def resolve_requires(path, hash)
config_dir = File.dirname(path)
hash.delete('require').tap do |loaded_features|
Array(loaded_features).each do |feature|
FeatureLoader.load(config_directory_path: config_dir, feature: feature)
end
end
end
def resolve_inheritance(path, hash, file, debug) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
inherited_files = Array(hash['inherit_from'])
base_configs(path, inherited_files, file)
.each_with_index.reverse_each do |base_config, index|
override_department_setting_for_cops(base_config, hash)
override_enabled_for_disabled_departments(base_config, hash)
base_config.each do |k, v|
next unless v.is_a?(Hash)
if hash.key?(k)
v = merge(v, hash[k],
cop_name: k, file: file, debug: debug,
inherited_file: inherited_files[index],
inherit_mode: determine_inherit_mode(hash, k))
end
hash[k] = v
fix_include_paths(base_config.loaded_path, hash, path, k, v) if v.key?('Include')
end
end
end
# When one .rubocop.yml file inherits from another .rubocop.yml file, the Include paths in the
# base configuration are relative to the directory where the base configuration file is. For the
# derived configuration, we need to make those paths relative to where the derived configuration
# file is.
def fix_include_paths(base_config_path, hash, path, key, value)
return unless File.basename(base_config_path).start_with?('.rubocop')
base_dir = File.dirname(base_config_path)
derived_dir = File.dirname(path)
hash[key]['Include'] = value['Include'].map do |include_path|
PathUtil.relative_path(File.join(base_dir, include_path), derived_dir)
end
end
def resolve_inheritance_from_gems(hash)
gems = hash.delete('inherit_gem')
(gems || {}).each_pair do |gem_name, config_path|
if gem_name == 'rubocop'
raise ArgumentError, "can't inherit configuration from the rubocop gem"
end
hash['inherit_from'] = Array(hash['inherit_from'])
Array(config_path).reverse_each do |path|
# Put gem configuration first so local configuration overrides it.
hash['inherit_from'].unshift gem_config_path(gem_name, path)
end
end
end
# Merges the given configuration with the default one. If
# AllCops:DisabledByDefault is true, it changes the Enabled params so that
# only cops from user configuration are enabled. If
# AllCops:EnabledByDefault is true, it changes the Enabled params so that
# only cops explicitly disabled in user configuration are disabled.
def merge_with_default(config, config_file, unset_nil:)
default_configuration = ConfigLoader.default_configuration
disabled_by_default = config.for_all_cops['DisabledByDefault']
enabled_by_default = config.for_all_cops['EnabledByDefault']
if disabled_by_default || enabled_by_default
default_configuration = transform(default_configuration) do |params|
params.merge('Enabled' => !disabled_by_default)
end
end
config = handle_disabled_by_default(config, default_configuration) if disabled_by_default
override_enabled_for_disabled_departments(default_configuration, config)
opts = { inherit_mode: config['inherit_mode'] || {}, unset_nil: unset_nil }
Config.new(merge(default_configuration, config, **opts), config_file)
end
# Return a recursive merge of two hashes. That is, a normal hash merge,
# with the addition that any value that is a hash, and occurs in both
# arguments, will also be merged. And so on.
#
# rubocop:disable Metrics/AbcSize
def merge(base_hash, derived_hash, **opts)
result = base_hash.merge(derived_hash)
keys_appearing_in_both = base_hash.keys & derived_hash.keys
keys_appearing_in_both.each do |key|
if opts[:unset_nil] && derived_hash[key].nil?
result.delete(key)
elsif merge_hashes?(base_hash, derived_hash, key)
result[key] = merge(base_hash[key], derived_hash[key], **opts)
elsif should_union?(derived_hash, base_hash, opts[:inherit_mode], key)
result[key] = base_hash[key] | derived_hash[key]
elsif opts[:debug]
warn_on_duplicate_setting(base_hash, derived_hash, key, **opts)
end
end
result
end
# rubocop:enable Metrics/AbcSize
# An `Enabled: true` setting in user configuration for a cop overrides an
# `Enabled: false` setting for its department.
def override_department_setting_for_cops(base_hash, derived_hash)
derived_hash.each_key do |key|
next unless key =~ %r{(.*)/.*}
department = Regexp.last_match(1)
next unless disabled?(derived_hash, department) || disabled?(base_hash, department)
# The `override_department` setting for the `Enabled` parameter is an
# internal setting that's not documented in the manual. It will cause a
# cop to be enabled later, when logic surrounding enabled/disabled it
# run, even though its department is disabled.
derived_hash[key]['Enabled'] = 'override_department' if derived_hash[key]['Enabled']
end
end
# If a cop was previously explicitly enabled, but then superseded by the
# department being disabled, disable it.
def override_enabled_for_disabled_departments(base_hash, derived_hash)
cops_to_disable = derived_hash.each_key.with_object([]) do |key, cops|
next unless disabled?(derived_hash, key)
cops.concat(base_hash.keys.grep(Regexp.new("^#{key}/")))
end
cops_to_disable.each do |cop_name|
next unless base_hash.dig(cop_name, 'Enabled') == true
derived_hash.replace(merge({ cop_name => { 'Enabled' => false } }, derived_hash))
end
end
private
def disabled?(hash, department)
hash[department].is_a?(Hash) && hash[department]['Enabled'] == false
end
def duplicate_setting?(base_hash, derived_hash, key, inherited_file)
return false if inherited_file.nil? # Not inheritance resolving merge
return false if inherited_file.start_with?('..') # Legitimate override
return false if base_hash[key] == derived_hash[key] # Same value
return false if remote_file?(inherited_file) # Can't change
Gem.path.none? { |dir| inherited_file.start_with?(dir) } # Can change?
end
def warn_on_duplicate_setting(base_hash, derived_hash, key, **opts)
return unless duplicate_setting?(base_hash, derived_hash, key, opts[:inherited_file])
inherit_mode = opts[:inherit_mode]['merge'] || opts[:inherit_mode]['override']
return if base_hash[key].is_a?(Array) && inherit_mode && inherit_mode.include?(key)
puts "#{PathUtil.smart_path(opts[:file])}: " \
"#{opts[:cop_name]}:#{key} overrides " \
"the same parameter in #{opts[:inherited_file]}"
end
def determine_inherit_mode(hash, key)
cop_cfg = hash[key]
local_inherit = cop_cfg['inherit_mode'] if cop_cfg.is_a?(Hash)
local_inherit || hash['inherit_mode'] || {}
end
def should_union?(derived_hash, base_hash, root_mode, key)
return false unless base_hash[key].is_a?(Array)
derived_mode = derived_hash['inherit_mode']
return false if should_override?(derived_mode, key)
return true if should_merge?(derived_mode, key)
base_mode = base_hash['inherit_mode']
return false if should_override?(base_mode, key)
return true if should_merge?(base_mode, key)
should_merge?(root_mode, key)
end
def should_merge?(mode, key)
mode && mode['merge'] && mode['merge'].include?(key)
end
def should_override?(mode, key)
mode && mode['override'] && mode['override'].include?(key)
end
def merge_hashes?(base_hash, derived_hash, key)
base_hash[key].is_a?(Hash) && derived_hash[key].is_a?(Hash)
end
def base_configs(path, inherit_from, file)
configs = Array(inherit_from).compact.map do |f|
ConfigLoader.load_file(inherited_file(path, f, file))
end
configs.compact
end
def inherited_file(path, inherit_from, file)
if remote_file?(inherit_from)
# A remote configuration, e.g. `inherit_from: http://example.com/rubocop.yml`.
RemoteConfig.new(inherit_from, File.dirname(path))
elsif Pathname.new(inherit_from).absolute?
# An absolute path to a config, e.g. `inherit_from: /Users/me/rubocop.yml`.
# The path may come from `inherit_gem` option, where a gem name is expanded
# to an absolute path to that gem.
print 'Inheriting ' if ConfigLoader.debug?
inherit_from
elsif file.is_a?(RemoteConfig)
# A path relative to a URL, e.g. `inherit_from: configs/default.yml`
# in a config included with `inherit_from: http://example.com/rubocop.yml`
file.inherit_from_remote(inherit_from, path)
else
# A local relative path, e.g. `inherit_from: default.yml`
print 'Inheriting ' if ConfigLoader.debug?
File.expand_path(inherit_from, File.dirname(path))
end
end
def remote_file?(uri)
regex = URI::DEFAULT_PARSER.make_regexp(%w[http https])
/\A#{regex}\z/.match?(uri)
end
def handle_disabled_by_default(config, new_default_configuration)
department_config = config.to_hash.reject { |cop| cop.include?('/') }
department_config.each do |dept, dept_params|
next unless dept_params['Enabled']
new_default_configuration.each do |cop, params|
next unless cop.start_with?("#{dept}/")
# Retain original default configuration for cops in the department.
params['Enabled'] = ConfigLoader.default_configuration[cop]['Enabled']
end
end
transform(config) do |params|
{ 'Enabled' => true }.merge(params) # Set true if not set.
end
end
def transform(config, &block)
config.transform_values(&block)
end
def gem_config_path(gem_name, relative_config_path)
if defined?(Bundler)
gem = Bundler.load.specs[gem_name].first
gem_path = gem.full_gem_path if gem
end
gem_path ||= Gem::Specification.find_by_name(gem_name).gem_dir
File.join(gem_path, relative_config_path)
rescue Gem::LoadError => e
raise Gem::LoadError, "Unable to find gem #{gem_name}; is the gem installed? #{e}"
end
end
end