/
config_loader_resolver.rb
222 lines (188 loc) · 7.83 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
# frozen_string_literal: true
require 'yaml'
require 'pathname'
module RuboCop
# A help class for ConfigLoader that handles configuration resolution.
class ConfigLoaderResolver
def resolve_requires(path, hash)
config_dir = File.dirname(path)
Array(hash.delete('require')).each do |r|
if r.start_with?('.')
require(File.join(config_dir, r))
else
require(r)
end
end
end
# rubocop:disable Metrics/MethodLength
def resolve_inheritance(path, hash, file, debug)
inherited_files = Array(hash['inherit_from'])
base_configs(path, inherited_files, file)
.reverse.each_with_index do |base_config, index|
override_department_setting_for_cops(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
end
end
end
# rubocop:enable Metrics/MethodLength
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
if disabled_by_default
config = handle_disabled_by_default(config, default_configuration)
end
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 base_hash[key].is_a?(Hash)
result[key] = merge(base_hash[key], derived_hash[key], **opts)
elsif should_union?(base_hash, key, opts[:inherit_mode])
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.
if derived_hash[key]['Enabled']
derived_hash[key]['Enabled'] = 'override_department'
end
end
end
private
def disabled?(hash, department)
hash[department] && 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.delete('inherit_mode') if cop_cfg.is_a?(Hash)
local_inherit || hash['inherit_mode'] || {}
end
def should_union?(base_hash, key, inherit_mode)
base_hash[key].is_a?(Array) &&
inherit_mode &&
inherit_mode['merge'] &&
inherit_mode['merge'].include?(key)
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)
RemoteConfig.new(inherit_from, File.dirname(path))
elsif file.is_a?(RemoteConfig)
file.inherit_from_remote(inherit_from, path)
else
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])
uri =~ /\A#{regex}\z/
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)
config.transform_values { |params| yield(params) }
end
def gem_config_path(gem_name, relative_config_path)
spec = Gem::Specification.find_by_name(gem_name)
File.join(spec.gem_dir, relative_config_path)
rescue Gem::LoadError => e
raise Gem::LoadError,
"Unable to find gem #{gem_name}; is the gem installed? #{e}"
end
end
end