-
Notifications
You must be signed in to change notification settings - Fork 38
/
config.cr
350 lines (311 loc) · 10 KB
/
config.cr
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
require "yaml"
require "./glob_utils"
# A configuration entry for `Ameba::Runner`.
#
# Config can be loaded from configuration YAML file and adjusted.
#
# ```
# config = Config.load
# config.formatter = my_formatter
# ```
#
# By default config loads `.ameba.yml` file located in a current
# working directory.
#
# If it cannot be found until reaching the root directory, then it will be
# searched for in the user’s global config locations, which consists of a
# dotfile or a config file inside the XDG Base Directory specification.
#
# - `~/.ameba.yml`
# - `$XDG_CONFIG_HOME/ameba/config.yml` (expands to `~/.config/ameba/config.yml`
# if `$XDG_CONFIG_HOME` is not set)
#
# If both files exist, the dotfile will be selected.
#
# As an example, if Ameba is invoked from inside `/path/to/project/lib/utils`,
# then it will use the config as specified inside the first of the following files:
#
# - `/path/to/project/lib/utils/.ameba.yml`
# - `/path/to/project/lib/.ameba.yml`
# - `/path/to/project/.ameba.yml`
# - `/path/to/.ameba.yml`
# - `/path/.ameba.yml`
# - `/.ameba.yml`
# - `~/.ameba.yml`
# - `~/.config/ameba/config.yml`
class Ameba::Config
include GlobUtils
AVAILABLE_FORMATTERS = {
progress: Formatter::DotFormatter,
todo: Formatter::TODOFormatter,
flycheck: Formatter::FlycheckFormatter,
silent: Formatter::BaseFormatter,
disabled: Formatter::DisabledFormatter,
json: Formatter::JSONFormatter,
}
XDG_CONFIG_HOME = ENV.fetch("XDG_CONFIG_HOME", "~/.config")
FILENAME = ".ameba.yml"
DEFAULT_PATH = Path[Dir.current] / FILENAME
DEFAULT_PATHS = {
Path["~"] / FILENAME,
Path[XDG_CONFIG_HOME] / "ameba/config.yml",
}
DEFAULT_GLOBS = %w(
**/*.cr
!lib
)
getter rules : Array(Rule::Base)
property severity = Severity::Convention
# Returns a list of paths (with wildcards) to files.
# Represents a list of sources to be inspected.
# If globs are not set, it will return default list of files.
#
# ```
# config = Ameba::Config.load
# config.globs = ["**/*.cr"]
# config.globs
# ```
property globs : Array(String)
# Represents a list of paths to exclude from globs.
# Can have wildcards.
#
# ```
# config = Ameba::Config.load
# config.excluded = ["spec", "src/server/*.cr"]
# ```
property excluded : Array(String)
# Returns `true` if correctable issues should be autocorrected.
property? autocorrect = false
@rule_groups : Hash(String, Array(Rule::Base))
# Creates a new instance of `Ameba::Config` based on YAML parameters.
#
# `Config.load` uses this constructor to instantiate new config by YAML file.
protected def initialize(config : YAML::Any)
@rules = Rule.rules.map &.new(config).as(Rule::Base)
@rule_groups = @rules.group_by &.group
@excluded = load_array_section(config, "Excluded")
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
if formatter_name = load_formatter_name(config)
self.formatter = formatter_name
end
end
# Loads YAML configuration file by `path`.
#
# ```
# config = Ameba::Config.load
# ```
def self.load(path = nil, colors = true, skip_reading_config = false)
Colorize.enabled = colors
content = if skip_reading_config
"{}"
else
read_config(path) || "{}"
end
Config.new YAML.parse(content)
rescue e
raise "Unable to load config file: #{e.message}"
end
protected def self.read_config(path = nil)
if path
return File.read(path) if File.exists?(path)
raise "Config file does not exist"
end
each_config_path do |config_path|
return File.read(config_path) if File.exists?(config_path)
end
end
protected def self.each_config_path(&)
path = Path[DEFAULT_PATH].expand(home: true)
search_paths = path.parents
search_paths.reverse_each do |search_path|
yield search_path / FILENAME
end
DEFAULT_PATHS.each do |default_path|
yield default_path
end
end
def self.formatter_names
AVAILABLE_FORMATTERS.keys.join('|')
end
# Returns a list of sources matching globs and excluded sections.
#
# ```
# config = Ameba::Config.load
# config.sources # => list of default sources
# config.globs = ["**/*.cr"]
# config.excluded = ["spec"]
# config.sources # => list of sources pointing to files found by the wildcards
# ```
def sources
(find_files_by_globs(globs) - find_files_by_globs(excluded))
.map { |path| Source.new File.read(path), path }
end
# Returns a formatter to be used while inspecting files.
# If formatter is not set, it will return default formatter.
#
# ```
# config = Ameba::Config.load
# config.formatter = custom_formatter
# config.formatter
# ```
property formatter : Formatter::BaseFormatter do
Formatter::DotFormatter.new
end
# Sets formatter by name.
#
# ```
# config = Ameba::Config.load
# config.formatter = :progress
# ```
def formatter=(name : String | Symbol)
unless formatter = AVAILABLE_FORMATTERS[name]?
raise "Unknown formatter `#{name}`. Use one of #{Config.formatter_names}."
end
@formatter = formatter.new
end
# Updates rule properties.
#
# ```
# config = Ameba::Config.load
# config.update_rule "MyRuleName", enabled: false
# ```
def update_rule(name, enabled = true, excluded = nil)
rule = @rules.find(&.name.==(name))
raise ArgumentError.new("Rule `#{name}` does not exist") unless rule
rule
.tap(&.enabled = enabled)
.tap(&.excluded = excluded)
end
# Updates rules properties.
#
# ```
# config = Ameba::Config.load
# config.update_rules %w[Rule1 Rule2], enabled: true
# ```
#
# also it allows to update groups of rules:
#
# ```
# config.update_rules %w[Group1 Group2], enabled: true
# ```
def update_rules(names, enabled = true, excluded = nil)
names.try &.each do |name|
if rules = @rule_groups[name]?
rules.each do |rule|
rule.enabled = enabled
rule.excluded = excluded
end
else
update_rule name, enabled, excluded
end
end
end
private def load_formatter_name(config)
name = config["Formatter"]?.try &.["Name"]?
name.try(&.to_s)
end
private def load_array_section(config, section_name, default = [] of String)
case value = config[section_name]?
when .nil? then default
when .as_s? then [value.to_s]
when .as_a? then value.as_a.map(&.as_s)
else
raise "Incorrect '#{section_name}' section in a config files"
end
end
# :nodoc:
module RuleConfig
# Define rule properties
macro properties(&block)
{% definitions = [] of NamedTuple %}
{% if (prop = block.body).is_a? Call %}
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% else %}
{% definitions << {var: prop.name, value: prop.args.first} %}
{% end %}
{% elsif block.body.is_a? Expressions %}
{% for prop in block.body.expressions %}
{% if prop.is_a? Call %}
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% else %}
{% definitions << {var: prop.name, value: prop.args.first} %}
{% end %}
{% end %}
{% end %}
{% end %}
{% properties = {} of MacroId => NamedTuple %}
{% for df in definitions %}
{% name = df[:var].id %}
{% key = name.camelcase.stringify %}
{% value = df[:value] %}
{% type = df[:type] %}
{% converter = nil %}
{% if key == "Severity" %}
{% type = Severity %}
{% converter = SeverityYamlConverter %}
{% end %}
{% unless type %}
{% if value.is_a? BoolLiteral %}
{% type = Bool %}
{% elsif value.is_a? StringLiteral %}
{% type = String %}
{% elsif value.is_a? NumberLiteral %}
{% if value.kind == :i32 %}
{% type = Int32 %}
{% elsif value.kind == :i64 %}
{% type = Int64 %}
{% elsif value.kind == :i128 %}
{% type = Int128 %}
{% elsif value.kind == :f32 %}
{% type = Float32 %}
{% elsif value.kind == :f64 %}
{% type = Float64 %}
{% end %}
{% end %}
{% end %}
{% properties[name] = {key: key, default: value, type: type, converter: converter} %}
@[YAML::Field(key: {{ key }}, converter: {{ converter }})]
{% if type == Bool %}
property? {{ name }}{{ " : #{type}".id if type }} = {{ value }}
{% else %}
property {{ name }}{{ " : #{type}".id if type }} = {{ value }}
{% end %}
{% end %}
{% unless properties["enabled".id] %}
@[YAML::Field(key: "Enabled")]
property? enabled = true
{% end %}
{% unless properties["severity".id] %}
@[YAML::Field(key: "Severity", converter: Ameba::SeverityYamlConverter)]
property severity = {{ @type }}.default_severity
{% end %}
{% unless properties["excluded".id] %}
@[YAML::Field(key: "Excluded")]
property excluded : Array(String)?
{% end %}
end
macro included
GROUP_SEVERITY = {
Documentation: Ameba::Severity::Warning,
Lint: Ameba::Severity::Warning,
Metrics: Ameba::Severity::Warning,
Performance: Ameba::Severity::Warning,
}
class_getter default_severity : Ameba::Severity do
GROUP_SEVERITY[group_name]? || Ameba::Severity::Convention
end
macro inherited
include YAML::Serializable
include YAML::Serializable::Strict
def self.new(config = nil)
if (raw = config.try &.raw).is_a?(Hash)
yaml = raw[rule_name]?.try &.to_yaml
end
from_yaml yaml || "{}"
end
end
end
end
end