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]).
-export_type([target/0]).
-ifdef(TEST).
-export([apply_rule/2]).
% For tests (we can't Xref the tests because rebar3 fails to compile some files).
-ignore_xref([apply_rule/2]).
-endif.
% For eating our own dogfood.
-ignore_xref([main/1]).
% For internal use only
-ignore_xref([do_rock/2]).
% For shell usage.
-ignore_xref([start/0]).
% API exports, not consumed locally.
-ignore_xref([rock/1, rock_this/2]).
-type source_filename() :: nonempty_string().
-type target() :: source_filename() | module().
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Public API
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc Used when starting the application on the shell.
-spec start() -> ok.
start() ->
{ok, _} = application:ensure_all_started(elvis_core),
ok.
validate_config(ElvisConfig) ->
try
elvis_config:validate_config(ElvisConfig)
catch
{invalid_config, _} = Caught ->
{error, {fail, [{throw, Caught}]}}
end.
%% In this context, `throw` means an error, e.g., validation or internal, not an actual
%% call to `erlang:throw/1`.
-spec rock([elvis_config:t()]) ->
ok | {fail, [{throw, term()} | elvis_result:file() | elvis_result:rule()]}.
rock(ElvisConfig) ->
case validate_config(ElvisConfig) of
ok ->
elvis_ruleset:drop_custom(),
Results = lists:map(fun do_parallel_rock/1, ElvisConfig),
lists:foldl(fun combine_results/2, ok, Results);
{error, Error} ->
Error
end.
-spec rock_this(target(), [elvis_config:t()]) ->
ok | {fail, [elvis_result:file() | elvis_result:rule()]}.
rock_this(Module, ElvisConfig) when is_atom(Module) ->
ModuleInfo = Module:module_info(compile),
Path = proplists:get_value(source, ModuleInfo),
rock_this(Path, ElvisConfig);
rock_this(Path, ElvisConfig) ->
case validate_config(ElvisConfig) of
ok ->
elvis_ruleset:drop_custom(),
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, ElvisConfig) of
[] ->
elvis_utils:info("Skipping ~s", [Path]);
FilteredElvisConfig ->
LoadedFile = load_file_data(FilteredElvisConfig, File),
ApplyRulesFun = fun(Cfg) -> apply_rules_and_print(Cfg, LoadedFile) end,
Results = lists:map(ApplyRulesFun, FilteredElvisConfig),
elvis_result_status(Results)
end;
{error, Error} ->
Error
end.
%% In this context, `throw` means an error, e.g., validation or internal, not an actual
%% call to `erlang:throw/1`.
-spec do_parallel_rock(elvis_config:t()) ->
ok
| {fail, [{throw, term()} | elvis_result:file() | elvis_result:rule()]}.
do_parallel_rock(ElvisConfig0) ->
Parallel = elvis_config:parallel(),
ElvisConfig = elvis_config:resolve_files(ElvisConfig0),
Files = elvis_config:files(ElvisConfig),
Result =
elvis_task:chunk_fold(
{?MODULE, do_rock},
fun(Elem, Acc) ->
elvis_result:print_results(Elem),
{ok, [Elem | Acc]}
end,
[],
[ElvisConfig],
Files,
Parallel
),
case Result of
{ok, Results} ->
elvis_result_status(Results);
{error, {T, E}} ->
%% {T, E} will be put into an {error, _} tuple higher on the call stack,
%% let's not encapsulate it multiple times.
{fail, [{T, E}]}
end.
-spec do_rock(elvis_file:t(), [elvis_config:t()] | elvis_config:t()) ->
{ok, elvis_result:file()}.
do_rock(File, ElvisConfig) ->
LoadedFile = load_file_data(ElvisConfig, File),
Results = apply_rules(ElvisConfig, LoadedFile),
{ok, Results}.
-spec load_file_data([elvis_config:t()] | elvis_config:t(), elvis_file:t()) ->
elvis_file:t().
load_file_data(ElvisConfig, File) ->
Path = elvis_file:path(File),
elvis_utils:info("Loading ~s", [Path]),
try
elvis_file:load_file_data(ElvisConfig, File)
catch
_:Reason ->
Msg = "~p when loading file ~p.",
elvis_utils:error(Msg, [Reason, Path]),
File
end.
-spec main([]) -> true | no_return().
main([]) ->
ok = application:load(elvis_core),
{module, _} = code:ensure_loaded(elvis_style),
case rock(elvis_config:config()) of
ok -> true;
_ -> elvis_utils:erlang_halt(1)
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% 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(ElvisConfig, File) ->
Results = apply_rules(ElvisConfig, File),
elvis_result:print_results(Results),
Results.
-spec apply_rules(
[elvis_config:t()] | elvis_config:t(),
File :: elvis_file:t()
) ->
elvis_result:file().
apply_rules(ElvisConfig, File) ->
Rules = elvis_config:rules(ElvisConfig),
Acc = {[], ElvisConfig, File},
{ParseTree, _} = elvis_file:parse_tree(File, ElvisConfig),
{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) ->
{nodes, ElvisAttrs} =
elvis_code:find(#{
of_types => [elvis],
inside => ParseTree
}),
ElvisAttrRules = elvis_attr_rules(ElvisAttrs),
elvis_config:merge_rules(ElvisAttrRules, ElvisConfigRules).
elvis_attr_rules([] = _ElvisAttrs) ->
[];
elvis_attr_rules(ElvisAttrs) ->
[Rule || ElvisAttr <- ElvisAttrs, Rule <- ktn_code:attr(value, ElvisAttr)].
-spec apply_rule(Rule, {Results, ElvisConfig, File}) -> Result when
Rule :: elvis_rule:t(),
Results :: [elvis_result:rule() | elvis_result:elvis_error()],
ElvisConfig :: elvis_config:t(),
File :: elvis_file:t(),
Result :: {Results, ElvisConfig, File}.
apply_rule(Rule, {Result, ElvisConfig, File}) ->
RuleResult =
try
AnalyzedModule = elvis_file:module(File),
IgnorableModule = elvis_rule:ignorable(AnalyzedModule),
case elvis_rule:ignored(IgnorableModule, Rule) of
false ->
Results = elvis_rule:execute(elvis_rule:file(Rule, File), ElvisConfig),
SortFun = fun(#{line_num := L1}, #{line_num := L2}) -> L1 =< L2 end,
SortResults = lists:sort(SortFun, Results),
elvis_result:new(rule, Rule, SortResults);
true ->
elvis_result:new(rule, Rule, [])
end
catch
_:Reason:Stacktrace ->
Msg = "'~p' while applying rule '~p': ~p",
elvis_result:new(error, Msg, [
Reason, {elvis_rule:ns(Rule), elvis_rule:name(Rule)}, Stacktrace
])
end,
{[RuleResult | Result], ElvisConfig, File}.
elvis_result_status(Results) ->
case elvis_result:status(Results) of
fail ->
{fail, elvis_result:clean(Results)};
ok ->
ok
end.