Current section

Files

Jump to
elvis_core src elvis_core.erl
Raw

src/elvis_core.erl

-module(elvis_core).
%% Public API
-export([ rock/1
, rock_this/2
]).
-export([start/0]).
%% for internal use only
-export([do_rock/2]).
%% for eating our own dogfood
-export([main/1]).
-ifdef(TEST).
-export([apply_rule/2]).
-endif.
-type source_filename() :: nonempty_string().
-type target() :: source_filename() | module().
-type rule_config() :: #{ atom() => term() }.
-export_type([rule_config/0]).
-type rule() :: {Module :: module(), Function :: atom(), RuleConfig :: rule_config()}
| {Module :: module(), Function :: atom()}.
-export_type([rule/0]).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Public API
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Used when starting the application on the shell.
-spec start() -> ok.
start() ->
{ok, _} = application:ensure_all_started(elvis_core),
ok.
%%% Rock Command
-spec rock(elvis_config:configs()) -> ok | {fail, [elvis_result:file()]}.
rock(Config) ->
ok = elvis_config:validate(Config),
NewConfig = elvis_config:normalize(Config),
Results = lists:map(fun do_parallel_rock/1, NewConfig),
lists:foldl(fun combine_results/2, ok, Results).
-spec rock_this(target(), elvis_config:configs()) ->
ok | {fail, [elvis_result:file() | elvis_result:rule()]}.
rock_this(Module, Config) when is_atom(Module) ->
ModuleInfo = Module:module_info(compile),
Path = proplists:get_value(source, ModuleInfo),
rock_this(Path, Config);
rock_this(Path, Config) ->
elvis_config:validate(Config),
NewConfig = elvis_config:normalize(Config),
Dirname = filename:dirname(Path),
Filename = filename:basename(Path),
File = case elvis_file:find_files([Dirname], Filename) of
[] -> throw({enoent, Path});
[File0] -> File0
end,
FilterFun = fun(Cfg) ->
Filter = elvis_config:filter(Cfg),
Dirs = elvis_config:dirs(Cfg),
IgnoreList = elvis_config:ignore(Cfg),
[] =/= elvis_file:filter_files([File], Dirs, Filter,
IgnoreList)
end,
case lists:filter(FilterFun, NewConfig) of
[] ->
elvis_utils:info("Skipping ~s", [Path]);
FilteredConfig ->
LoadedFile = load_file_data(FilteredConfig, File),
ApplyRulesFun = fun(Cfg) -> apply_rules_and_print(Cfg, LoadedFile) end,
Results = lists:map(ApplyRulesFun, FilteredConfig),
elvis_result_status(Results)
end.
%% @private
-spec do_parallel_rock(elvis_config:config())
-> ok | {fail, [elvis_result:file() | elvis_result:rule()]}.
do_parallel_rock(Config0) ->
Parallel = elvis_config:from_application_or_config(parallel, 1),
Config = elvis_config:resolve_files(Config0),
Files = elvis_config:files(Config),
{ok, Results} =
elvis_task:chunk_fold({?MODULE, do_rock},
fun(Elem, Acc) ->
elvis_result:print_results(Elem),
{ok, [Elem | Acc]}
end,
[], [Config], Files, Parallel),
elvis_result_status(Results).
-spec do_rock(elvis_file:file(), elvis_config:configs() | elvis_config:config())
-> {ok, elvis_result:file()}.
do_rock(File, Config) ->
LoadedFile = load_file_data(Config, File),
Results = apply_rules(Config, LoadedFile),
{ok, Results}.
%% @private
-spec load_file_data(elvis_config:configs() | elvis_config:config(), elvis_file:file())
-> elvis_file:file().
load_file_data(Config, File) ->
Path = elvis_file:path(File),
elvis_utils:info("Loading ~s", [Path]),
try
elvis_file:load_file_data(Config, File)
catch
_:Reason ->
Msg = "~p when loading file ~p.",
elvis_utils:error_prn(Msg, [Reason, Path]),
File
end.
%% @private
-spec main([]) -> true | no_return().
main([]) ->
ok = application:load(elvis_core),
R = rock(elvis_config:from_file("elvis.config")),
R =:= ok orelse halt(1).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec combine_results(ok | {fail, [elvis_result:file()]},
ok | {fail, [elvis_result:file()]}) ->
ok | {fail, [elvis_result:file()]}.
combine_results(ok, Acc) ->
Acc;
combine_results(Item, ok) ->
Item;
combine_results({fail, ItemResults}, {fail, AccResults}) ->
{fail, ItemResults ++ AccResults}.
apply_rules_and_print(Config, File) ->
Results = apply_rules(Config, File),
elvis_result:print_results(Results),
Results.
-spec apply_rules(elvis_config:configs() | elvis_config:config(), File::elvis_file:file()) ->
elvis_result:file().
apply_rules(Config, File) ->
Rules = elvis_config:rules(Config),
Acc = {[], Config, File},
{ParseTree, _} = elvis_file:parse_tree(Config, File),
{RulesResults, _, _} = lists:foldl(fun apply_rule/2, Acc,
merge_rules({file, ParseTree}, lists:flatten(Rules))),
elvis_result:new(file, File, RulesResults).
merge_rules({file, ParseTree}, ElvisConfigRules) ->
ElvisAttrs = elvis_code:find(fun is_elvis_attr/1, ParseTree,
#{ traverse => content, mode => node }),
ElvisAttrRules = elvis_attr_rules(ElvisAttrs),
elvis_config:merge_rules(ElvisAttrRules, ElvisConfigRules).
is_elvis_attr(Node) ->
ktn_code:type(Node) =:= elvis.
elvis_attr_rules([] = _ElvisAttrs) ->
[];
elvis_attr_rules(ElvisAttrs) ->
[Rule || ElvisAttr <- ElvisAttrs, Rule <- ktn_code:attr(value, ElvisAttr)].
-spec apply_rule({Mod, Fun} | {Mod, Fun, RuleCfg}, {Results, ElvisCfg, File}) -> Result
when Mod :: module(),
Fun :: atom(),
RuleCfg :: rule_config(),
Results :: [elvis_result:rule()],
ElvisCfg :: elvis_config:config(),
File :: elvis_file:file(),
Result :: {Results, ElvisCfg, File}.
apply_rule({Module, Function}, {Result, Config, File}) ->
apply_rule({Module, Function, #{}}, {Result, Config, File});
apply_rule({Module, Function, ConfigArgs}, {Result, Config, File}) ->
ConfigMap =
try
ensure_config_map(Module, Function, ConfigArgs)
catch
_:function_clause ->
throw({invalid_config, disable_without_ruleset})
end,
RuleResult =
try
AnalyzedModule = elvis_file:module(File),
Ignores = maps:get(ignore, ConfigMap, []),
case lists:member(AnalyzedModule, Ignores) of
false ->
FilteredConfigMap
= maps:merge(ConfigMap#{ ignore => lists:delete(AnalyzedModule, Ignores) },
ConfigArgs),
Results = Module:Function(Config, File, FilteredConfigMap),
SortFun = fun(#{line_num := L1}, #{line_num := L2}) ->
L1 =< L2
end,
SortResults = lists:sort(SortFun, Results),
elvis_result:new(rule, Function, SortResults);
true ->
elvis_result:new(rule, Function, [])
end
catch
_:Reason ->
Msg = "'~p' while applying rule '~p'.",
elvis_result:new(error, Msg, [Reason, Function])
end,
{[RuleResult | Result], Config, File}.
%% @doc Process a tules configuration argument and converts it to a map.
ensure_config_map(_, _, Map) when is_map(Map) ->
Map;
ensure_config_map(elvis_style, line_length, [Limit]) ->
#{limit => Limit};
ensure_config_map(elvis_style, operator_spaces, Rules) ->
#{rules => Rules};
ensure_config_map(elvis_style, nesting_level, [Level]) ->
#{level => Level};
ensure_config_map(elvis_style, god_modules, [Limit]) ->
#{limit => Limit};
ensure_config_map(elvis_style, god_modules, [Limit, IgnoreModules]) ->
#{limit => Limit, ignore => IgnoreModules};
ensure_config_map(elvis_style, invalid_dynamic_call, IgnoreModules) ->
#{ignore => IgnoreModules};
ensure_config_map(elvis_style,
module_naming_convention,
[Regex, IgnoreModules]) ->
#{regex => Regex, ignore => IgnoreModules};
ensure_config_map(_, _, []) ->
#{}.
%% @private
elvis_result_status(Results) ->
case elvis_result:status(Results) of
fail -> {fail, elvis_result:clean(Results)};
ok -> ok
end.