shard-ameba/src/ameba/config.cr

292 lines
8.2 KiB
Crystal
Raw Normal View History

2017-11-13 21:20:22 +00:00
require "yaml"
require "./glob_utils"
2017-11-13 21:20:22 +00:00
2017-11-15 18:49:09 +00:00
# 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 in a current directory.
#
2017-11-13 21:20:22 +00:00
class Ameba::Config
include GlobUtils
AVAILABLE_FORMATTERS = {
progress: Formatter::DotFormatter,
todo: Formatter::TODOFormatter,
flycheck: Formatter::FlycheckFormatter,
silent: Formatter::BaseFormatter,
disabled: Formatter::DisabledFormatter,
2018-05-11 18:09:15 +00:00
json: Formatter::JSONFormatter,
}
2017-11-23 21:59:59 +00:00
PATH = ".ameba.yml"
2019-12-28 18:59:41 +00:00
DEFAULT_GLOBS = %w(
**/*.cr
!lib
)
2017-11-23 17:49:45 +00:00
getter rules : Array(Rule::Base)
property severity = Severity::Convention
2017-11-13 21:20:22 +00:00
2019-12-28 18:59:41 +00:00
# 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)
2020-03-22 12:18:07 +00:00
# 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)
2022-12-09 23:20:20 +00:00
# Returns `true` if correctable issues should be autocorrected.
2021-10-24 18:58:32 +00:00
property? autocorrect = false
2018-09-02 21:17:56 +00:00
@rule_groups : Hash(String, Array(Rule::Base))
2018-06-18 07:25:06 +00:00
2017-11-15 18:49:09 +00:00
# Creates a new instance of `Ameba::Config` based on YAML parameters.
#
# `Config.load` uses this constructor to instantiate new config by YAML file.
2019-12-28 18:59:41 +00:00
protected def initialize(config : YAML::Any)
@rules = Rule.rules.map &.new(config).as(Rule::Base)
2018-06-18 07:25:06 +00:00
@rule_groups = @rules.group_by &.group
2020-03-22 12:18:07 +00:00
@excluded = load_array_section(config, "Excluded")
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
2021-12-09 20:33:47 +00:00
return unless formatter_name = load_formatter_name(config)
self.formatter = formatter_name
2017-11-13 21:20:22 +00:00
end
2017-11-15 18:49:09 +00:00
# Loads YAML configuration file by `path`.
#
# ```
# config = Ameba::Config.load
# ```
def self.load(path = PATH, colors = true)
Colorize.enabled = colors
2019-12-28 18:59:41 +00:00
content = File.exists?(path) ? File.read path : "{}"
2017-11-30 09:27:52 +00:00
Config.new YAML.parse(content)
rescue e
raise "Config file is invalid: #{e.message}"
end
def self.formatter_names
AVAILABLE_FORMATTERS.keys.join('|')
2017-11-13 21:20:22 +00:00
end
2020-03-22 12:18:07 +00:00
# Returns a list of sources matching globs and excluded sections.
2017-11-15 18:49:09 +00:00
#
# ```
# config = Ameba::Config.load
# config.sources # => list of default sources
# config.globs = ["**/*.cr"]
2020-03-22 12:18:07 +00:00
# config.excluded = ["spec"]
# config.sources # => list of sources pointing to files found by the wildcards
2017-11-15 18:49:09 +00:00
# ```
def sources
2020-03-22 12:18:07 +00:00
(find_files_by_globs(globs) - find_files_by_globs(excluded))
.map { |path| Source.new File.read(path), path }
2017-11-13 21:20:22 +00:00
end
2017-11-15 18:49:09 +00:00
# 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
2017-11-13 21:20:22 +00:00
end
# Sets formatter by name.
#
# ```
# config = Ameba::Config.load
# config.formatter = :progress
# ```
def formatter=(name : String | Symbol)
2022-11-14 00:24:29 +00:00
unless formatter = AVAILABLE_FORMATTERS[name]?
raise "Unknown formatter `#{name}`. Use one of #{Config.formatter_names}."
end
2022-11-14 00:24:29 +00:00
@formatter = formatter.new
end
2017-11-23 17:49:45 +00:00
# Updates rule properties.
2017-11-15 18:49:09 +00:00
#
# ```
# config = Ameba::Config.load
2017-11-23 17:49:45 +00:00
# config.update_rule "MyRuleName", enabled: false
2017-11-15 18:49:09 +00:00
# ```
2017-11-30 21:50:07 +00:00
def update_rule(name, enabled = true, excluded = nil)
2021-01-18 15:45:35 +00:00
rule = @rules.find(&.name.==(name))
raise ArgumentError.new("Rule `#{name}` does not exist") unless rule
2017-11-23 17:49:45 +00:00
2021-01-18 15:45:35 +00:00
rule
.tap(&.enabled = enabled)
.tap(&.excluded = excluded)
2017-11-13 21:20:22 +00:00
end
2018-06-18 07:25:06 +00:00
# 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
# ```
2021-01-18 15:45:35 +00:00
def update_rules(names, enabled = true, excluded = nil)
2018-06-18 07:25:06 +00:00
names.try &.each do |name|
2021-01-18 15:45:35 +00:00
if rules = @rule_groups[name]?
rules.each do |rule|
rule.enabled = enabled
rule.excluded = excluded
end
2018-06-18 07:25:06 +00:00
else
2021-01-18 15:45:35 +00:00
update_rule name, enabled, excluded
2018-06-18 07:25:06 +00:00
end
end
end
2019-12-28 18:59:41 +00:00
private def load_formatter_name(config)
name = config["Formatter"]?.try &.["Name"]?
name.try(&.to_s)
2017-11-13 21:20:22 +00:00
end
2020-03-22 12:18:07 +00:00
private def load_array_section(config, section_name, default = [] of String)
case value = config[section_name]?
2020-03-22 12:58:10 +00:00
when .nil? then default
2020-03-22 12:18:07 +00:00
when .as_s? then [value.to_s]
when .as_a? then value.as_a.map(&.as_s)
2019-12-28 18:59:41 +00:00
else
2022-11-14 00:24:29 +00:00
raise "Incorrect '#{section_name}' section in a config files"
2019-12-28 18:59:41 +00:00
end
2017-11-13 21:20:22 +00:00
end
2017-11-23 08:54:56 +00:00
# :nodoc:
2017-11-23 17:49:45 +00:00
module RuleConfig
2017-11-22 06:44:29 +00:00
macro properties(&block)
{% definitions = [] of NamedTuple %}
{% if block.body.is_a? Assign %}
{% definitions << {var: block.body.target, value: block.body.value} %}
{% elsif block.body.is_a? Call %}
{% definitions << {var: block.body.name, value: block.body.args.first} %}
2017-11-22 06:44:29 +00:00
{% elsif block.body.is_a? TypeDeclaration %}
{% definitions << {var: block.body.var, value: block.body.value, type: block.body.type} %}
{% elsif block.body.is_a? Expressions %}
{% for prop in block.body.expressions %}
{% if prop.is_a? Assign %}
{% definitions << {var: prop.target, value: prop.value} %}
{% elsif prop.is_a? Call %}
{% definitions << {var: prop.name, value: prop.args.first} %}
2017-11-22 06:44:29 +00:00
{% elsif prop.is_a? TypeDeclaration %}
{% definitions << {var: prop.var, value: prop.value, type: prop.type} %}
{% end %}
2017-11-13 21:20:22 +00:00
{% end %}
2017-11-22 06:44:29 +00:00
{% end %}
2017-11-13 21:20:22 +00:00
2017-11-22 06:44:29 +00:00
{% properties = {} of MacroId => NamedTuple %}
{% for df in definitions %}
{% name = df[:var].id %}
{% key = name.camelcase.stringify %}
{% value = df[:value] %}
{% type = df[:type] %}
2019-04-13 18:16:59 +00:00
{% converter = nil %}
{% if key == "Severity" %}
{% type = Severity %}
{% converter = SeverityYamlConverter %}
{% end %}
2017-11-13 21:20:22 +00:00
2017-11-22 06:44:29 +00:00
{% if type == nil %}
{% 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 == :f32 %}
{% type = Float32 %}
{% elsif value.kind == :f64 %}
{% type = Float64 %}
{% end %}
{% end %}
{% type = Nil if type == nil %}
{% end %}
2019-04-13 18:16:59 +00:00
{% properties[name] = {key: key, default: value, type: type, converter: converter} %}
2020-06-15 11:19:23 +00:00
2022-11-14 00:24:29 +00:00
@[YAML::Field(key: {{ key }}, converter: {{ converter }}, type: {{ type }})]
{% if type == Bool %}
property? {{ name }} : {{ type }} = {{ value }}
{% else %}
property {{ name }} : {{ type }} = {{ value }}
{% end %}
2017-11-22 06:44:29 +00:00
{% end %}
2022-11-26 01:16:41 +00:00
{% unless properties["enabled".id] %}
2020-06-15 11:19:23 +00:00
@[YAML::Field(key: "Enabled")]
2022-11-22 18:46:38 +00:00
property? enabled = true
2017-11-22 06:44:29 +00:00
{% end %}
2017-11-13 21:20:22 +00:00
2022-11-26 01:16:41 +00:00
{% unless properties["severity".id] %}
2020-06-15 11:19:23 +00:00
@[YAML::Field(key: "Severity", converter: Ameba::SeverityYamlConverter)]
property severity = {{ @type }}.default_severity
2019-04-13 19:38:37 +00:00
{% end %}
2022-11-26 01:16:41 +00:00
{% unless properties["excluded".id] %}
2020-06-15 11:19:23 +00:00
@[YAML::Field(key: "Excluded")]
property excluded : Array(String)?
2017-11-30 21:50:07 +00:00
{% end %}
2017-11-13 21:20:22 +00:00
end
2017-11-22 06:44:29 +00:00
macro included
GROUP_SEVERITY = {
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
2017-11-22 06:44:29 +00:00
macro inherited
2020-06-15 11:19:23 +00:00
include YAML::Serializable
include YAML::Serializable::Strict
2017-11-22 06:44:29 +00:00
2017-11-23 17:49:45 +00:00
def self.new(config = nil)
2021-01-17 12:25:50 +00:00
if (raw = config.try &.raw).is_a?(Hash)
yaml = raw[rule_name]?.try &.to_yaml
2017-11-30 09:27:52 +00:00
end
from_yaml yaml || "{}"
2017-11-22 06:44:29 +00:00
end
end
2017-11-13 21:20:22 +00:00
end
end
end