-module(elvis_config).
-export([from_rebar/1, from_file/1, from_application_or_config/2, validate/1,
normalize/1]).
%% Geters
-export([dirs/1, ignore/1, filter/1, files/1, rules/1]).
%% Files
-export([resolve_files/1, resolve_files/2, apply_to_files/2]).
%% Rules
-export([merge_rules/2]).
-export_type([config/0]).
-export_type([configs/0]).
-type config() :: map().
-type configs() :: [config()].
-define(DEFAULT_FILTER, "*.erl").
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Public
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec from_rebar(string()) -> configs().
from_rebar(Path) ->
case file:consult(Path) of
{ok, AppConfig} ->
load(config, load_initial(AppConfig), []);
{error, Reason} ->
throw(Reason)
end.
-spec from_file(string()) -> configs().
from_file(Path) ->
from_file(Path, config, []).
-spec from_file(string(), atom(), term()) -> configs().
from_file(Path, Key, Default) ->
case file:consult(Path) of
{ok, [AppConfig]} ->
load(Key, load_initial(AppConfig), Default);
{error, {_Line, _Mod, _Term} = Reason} ->
throw(Reason);
{error, _Reason} ->
Default
end.
-spec from_application_or_config(atom(), term()) -> term().
from_application_or_config(Key, Default) ->
case application:get_env(elvis_core, Key) of
{ok, Value} ->
Value;
_ ->
from_file("elvis.config", Key, Default)
end.
-spec load(atom(), term(), term()) -> configs().
load(Key, ElvisConfig, Default) ->
ensure_config_list(Key, proplists:get_value(Key, ElvisConfig, Default)).
-spec load_initial(term()) -> [term()].
load_initial(AppConfig) ->
ElvisConfig = proplists:get_value(elvis, AppConfig, []),
RulesetsConfig = proplists:get_value(rulesets, ElvisConfig, #{}),
elvis_rulesets:set_rulesets(RulesetsConfig),
ElvisConfig.
ensure_config_list(config, Config) when is_map(Config) ->
[Config];
ensure_config_list(_Other, Config) ->
Config.
-spec validate(Config :: configs()) -> ok.
validate([]) ->
throw({invalid_config, empty_config});
validate(Config) ->
lists:foreach(fun do_validate/1, Config).
do_validate(RuleGroup) ->
case maps:is_key(src_dirs, RuleGroup) or maps:is_key(dirs, RuleGroup) of
false ->
throw({invalid_config, {missing_dirs, RuleGroup}});
true ->
ok
end,
case maps:is_key(dirs, RuleGroup) of
true ->
case maps:is_key(filter, RuleGroup) of
false ->
throw({invalid_config, {missing_filter, RuleGroup}});
true ->
ok
end;
false ->
ok
end,
case maps:is_key(rules, RuleGroup) orelse maps:is_key(ruleset, RuleGroup) of
false ->
throw({invalid_config, {missing_rules, RuleGroup}});
true ->
ok
end.
-spec normalize(configs()) -> configs().
normalize(Config) when is_list(Config) ->
lists:map(fun do_normalize/1, Config).
%% @private
do_normalize(#{src_dirs := Dirs} = Config) ->
%% NOTE: Provided for backwards compatibility.
%% Rename 'src_dirs' key to 'dirs'.
Config1 = maps:remove(src_dirs, Config),
Config1#{dirs => Dirs};
do_normalize(Config) ->
Config.
-spec dirs(Config :: configs() | config()) -> [string()].
dirs(Config) when is_list(Config) ->
lists:flatmap(fun dirs/1, Config);
dirs(#{dirs := Dirs}) ->
Dirs;
dirs(#{}) ->
[].
-spec ignore(configs() | config()) -> [string()].
ignore(Config) when is_list(Config) ->
lists:flatmap(fun ignore/1, Config);
ignore(#{ignore := Ignore}) ->
lists:map(fun ignore_to_regexp/1, Ignore);
ignore(#{}) ->
[].
-spec filter(configs() | config()) -> [string()].
filter(Config) when is_list(Config) ->
lists:flatmap(fun filter/1, Config);
filter(#{filter := Filter}) ->
Filter;
filter(#{}) ->
?DEFAULT_FILTER.
-spec files(RuleGroup :: configs() | config()) -> [elvis_file:file()].
files(RuleGroup) when is_list(RuleGroup) ->
lists:map(fun files/1, RuleGroup);
files(#{files := Files}) ->
Files;
files(#{}) ->
[].
-spec rules(RulesL :: configs()) -> [[elvis_core:rule()]];
(Rules :: config()) -> [elvis_core:rule()].
rules(Rules) when is_list(Rules) ->
lists:map(fun rules/1, Rules);
rules(#{rules := UserRules, ruleset := RuleSet}) ->
DefaultRules = elvis_rulesets:rules(RuleSet),
merge_rules(UserRules, DefaultRules);
rules(#{rules := Rules}) ->
Rules;
rules(#{ruleset := RuleSet}) ->
elvis_rulesets:rules(RuleSet);
rules(#{}) ->
[].
%% @doc Takes a configuration and a list of files, filtering some
%% of them according to the 'filter' key, or if not specified
%% uses '*.erl'.
%% @end
%% resolve_files/2 with a configs() type is used in elvis project
-spec resolve_files(Config :: configs() | config(), Files :: [elvis_file:file()]) ->
configs() | config().
resolve_files(Config, Files) when is_list(Config) ->
Fun = fun(RuleGroup) -> resolve_files(RuleGroup, Files) end,
lists:map(Fun, Config);
resolve_files(RuleGroup, Files) ->
Filter = filter(RuleGroup),
Dirs = dirs(RuleGroup),
Ignore = ignore(RuleGroup),
FilteredFiles = elvis_file:filter_files(Files, Dirs, Filter, Ignore),
_ = case FilteredFiles of
[] ->
RuleSet = maps:get(ruleset, RuleGroup, undefined),
Error =
elvis_result:new(warn,
"Searching for files in ~p, for ruleset ~p, "
"with filter ~p, yielded none. "
"Update your configuration.",
[Dirs, RuleSet, Filter]),
ok = elvis_result:print_results([Error]);
_ ->
ok
end,
RuleGroup#{files => FilteredFiles}.
%% @doc Takes a configuration and finds all files according to its 'dirs'
%% end 'filter' key, or if not specified uses '*.erl'.
%% @end
-spec resolve_files(config()) -> config().
resolve_files(#{files := _Files} = RuleGroup) ->
RuleGroup;
resolve_files(#{dirs := Dirs} = RuleGroup) ->
Filter = filter(RuleGroup),
Files = elvis_file:find_files(Dirs, Filter),
resolve_files(RuleGroup, Files).
%% @doc Takes a function and configuration and applies the function to all
%% file in the configuration.
%% @end
-spec apply_to_files(Fun :: fun(), Config :: configs() | config()) ->
configs() | config().
apply_to_files(Fun, Config) when is_list(Config) ->
ApplyFun = fun(RuleGroup) -> apply_to_files(Fun, RuleGroup) end,
lists:map(ApplyFun, Config);
apply_to_files(Fun, #{files := Files} = RuleGroup) ->
NewFiles = lists:map(Fun, Files),
RuleGroup#{files => NewFiles}.
%% @doc Ensures the ignore is a regexp, this is used
%% to allow using 'module name' atoms in the ignore
%% list by taking advantage of the fact that erlang
%% enforces the module and the file name to be the
%% same.
%% @end
-spec ignore_to_regexp(string() | atom()) -> string().
ignore_to_regexp(R) when is_list(R) ->
R;
ignore_to_regexp(A) when is_atom(A) ->
"/" ++ atom_to_list(A) ++ "\\.erl$".
%% @doc Merge user rules (override) with elvis default rules.
-spec merge_rules(UserRules :: list(), DefaultRules :: list()) -> [elvis_core:rule()].
merge_rules(UserRules, DefaultRules) ->
UnduplicatedRules =
% Drops repeated rules
lists:filter(% If any default rule is in UserRules it means the user
% wants to override the rule.
fun ({FileName, RuleName}) ->
not is_rule_override(FileName, RuleName, UserRules);
({FileName, RuleName, _}) ->
not is_rule_override(FileName, RuleName, UserRules);
(_) ->
false
end,
DefaultRules),
OverrideRules =
% Remove the rules that the user wants to "disable" and after that,
% remains just the rules the user wants to override.
lists:filter(fun ({_FileName, _RuleName, OverrideOptions}) ->
disable /= OverrideOptions;
({_FileName, _RuleName}) ->
true; % not disabled
(_) ->
false
end,
UserRules),
UnduplicatedRules ++ OverrideRules.
-spec is_rule_override(FileName :: atom(),
RuleName :: atom(),
UserRules :: [elvis_core:rule()]) ->
boolean().
is_rule_override(FileName, RuleName, UserRules) ->
lists:any(fun(UserRule) ->
case UserRule of
{FileName, RuleName, _} ->
true;
_ ->
false
end
end,
UserRules).