-module(elvis_config). -feature(maybe_expr, enable). -elvis([ {elvis_style, abc_size, #{ignore => [{elvis_config, default_for, 1}]}}, {elvis_style, code_complexity, #{ignore => [{elvis_config, default_for, 1}]}} ]). -export([from_rebar/1, from_file/1, validate/2, default/0]). %% Getters -export([files/1, rules/1, ruleset/1]). %% Files -export([resolve_files/1, resolve_files/2, apply_to_files/2]). %% Rules -export([merge_rules/2]). %% Options -export([config/0, output_format/0, verbose/0, no_output/0, parallel/0, warnings_as_errors/0]). -export([ set_output_format/1, set_verbose/1, set_no_output/1, set_parallel/1, set_warnings_as_errors/1 ]). % Corresponds to each config map in the 'config' key. -opaque t() :: #{ files := [nonempty_string()], ruleset := atom(), rules => [tuple()], resolved_files => dynamic(), ignore => [string()] }. -export_type([t/0]). -type output_format() :: plain | colors | parsable. -export_type([output_format/0]). % API exports, not consumed locally. -ignore_xref([from_rebar/1, from_file/1, default/0, resolve_files/2, apply_to_files/2]). -ignore_xref([ set_output_format/1, set_verbose/1, set_no_output/1, set_parallel/1, set_warnings_as_errors/1 ]). -ifdef(TEST). -export([reset_validation/0]). -export([inject_ignore/2]). -export([file_globs/1]). -export([ignore/1]). -endif. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Public %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% from_static(config, {Type, Config}) -> inject_ignore(do_from_static(config, {Type, Config}), git_check_ignore()); from_static(Key, {Type, Config}) -> _ = elvis_utils:debug("fetching key '~s' from '~s' configuration", [Key, Type]), do_from_static(Key, {Type, Config}). do_from_static(Key, {Type, Config}) -> _ = elvis_utils:debug("fetching key '~s' from '~s' configuration", [Key, Type]), case proplists:get_value(Key, Config) of undefined -> _ = elvis_utils:debug( "no value for key '~s' found in '~s' configuration; going with default", [ Key, Type ] ), default(Key); Value -> _ = elvis_utils:debug("value for key '~s' found in '~s' configuration", [Key, Type]), Value end. -spec config() -> [t()] | {error, Message :: string()}. config() -> for(config). -spec output_format() -> output_format() | {error, Message :: string()}. output_format() -> for(output_format). -spec verbose() -> boolean() | {error, Message :: string()}. verbose() -> for(verbose). -spec no_output() -> boolean() | {error, Message :: string()}. no_output() -> for(no_output). -spec parallel() -> pos_integer() | {error, Message :: string()}. parallel() -> for(parallel). -spec warnings_as_errors() -> boolean() | {error, Message :: string()}. warnings_as_errors() -> for(warnings_as_errors). -spec set_output_format(output_format()) -> ok. set_output_format(OutputFormat) -> set_env(output_format, OutputFormat). -spec set_verbose(boolean()) -> ok. set_verbose(Verbose) -> set_env(verbose, Verbose). -spec set_no_output(boolean()) -> ok. set_no_output(NoOutput) -> set_env(no_output, NoOutput). -spec set_parallel(pos_integer()) -> ok. set_parallel(Parallel) -> set_env(parallel, Parallel). -spec set_warnings_as_errors(boolean()) -> ok. set_warnings_as_errors(WarningsAsErrors) -> set_env(warnings_as_errors, WarningsAsErrors). set_env(Key, Value) -> application:set_env(elvis_core, Key, Value). default(Key) -> case application:get_env(elvis_core, Key) of undefined -> _ = elvis_utils:debug( "no value for key '~s' found in application environment; going with default", [Key] ), default_for(Key); {ok, Value} -> _ = elvis_utils:debug("value for key '~s' (~p) found in application environment", [ Key, Value ]), Value end. for(Key) -> maybe AppDefault = default_for(app), {ok, ElvisConfig} ?= consult_elvis_config("elvis.config"), {ok, AppConfig} ?= case ElvisConfig of AppDefault -> % This might happen whether we fail to parse the file or it actually is [] _ = elvis_utils:debug( "elvis.config is unusable; falling back to rebar.config", [] ), consult_rebar_config("rebar.config"); AppConfig0 -> {ok, AppConfig0} end, from_static(Key, {app, AppConfig}) else {error, _} = Error -> Error end. consult_elvis_config(File) -> case file:consult(File) of {ok, [AppConfig]} when is_list(AppConfig) -> _ = elvis_utils:debug("elvis.config is consultable; using it", []), {ok, AppConfig}; {error, {Line, Mod, Term}} -> {error, lists:flatten( io_lib:format("elvis.config is unconsultable: ~p, ~p, ~p", [Line, Mod, Term]) )}; _ -> _ = elvis_utils:debug("elvis.config is unconsultable", []), {ok, default_for(app)} end. consult_rebar_config(File) -> case file:consult(File) of {ok, AppConfig} when is_list(AppConfig) -> _ = elvis_utils:debug("rebar.config is consultable; using it", []), {ok, from_static(elvis, {'rebar.config', AppConfig})}; {error, {Line, Mod, Term}} -> {error, lists:flatten( io_lib:format("rebar.config is unconsultable: ~p, ~p, ~p", [Line, Mod, Term]) )}; _ -> _ = elvis_utils:debug("rebar.config is unconsultable", []), {ok, default_for('rebar.config')} end. -spec from_rebar(File :: string()) -> [t()] | {error, Message :: string()}. from_rebar(File) -> maybe {ok, AppConfig} ?= consult_rebar_config(File), fetch_elvis_config_from(AppConfig, File) else {error, _} = Error -> Error end. -spec from_file(File :: string()) -> [t()] | {error, Message :: string()}. from_file(File) -> maybe {ok, AppConfig} ?= consult_elvis_config(File), fetch_elvis_config_from(AppConfig, File) else {error, _} = Error -> Error end. fetch_elvis_config_from(AppConfig, File) -> try do_validate({app, AppConfig, File}) of ok -> from_static(config, {app, AppConfig}) catch {invalid_config, Message} -> {error, lists:flatten(Message)} end. default_for(app) -> % This is the top-level element []; default_for(elvis) -> []; default_for('rebar.config') -> [{elvis, default_for(elvis)}]; default_for(config) -> []; default_for(output_format) -> colors; default_for(verbose) -> false; default_for(no_output) -> false; default_for(parallel) -> erlang:system_info(schedulers_online); default_for(warnings_as_errors) -> true; default_for(rulesets) -> #{}; default_for([config, files]) -> []; default_for([config, ignore]) -> []; default_for([config, ruleset]) -> undefined; default_for([config, rules]) -> []. -spec default() -> [t()]. default() -> [ #{ files => ["apps/**/src/*.erl", "src/**/*.erl"], ruleset => erl_files }, #{ files => ["apps/**/src/*.hrl", "apps/**/include/*.hrl", "src/*.hrl", "include/*.hrl"], ruleset => hrl_files }, #{files => ["rebar.config"], ruleset => rebar_config}, #{files => [".gitignore"], ruleset => gitignore} ]. -ifdef(TEST). -spec file_globs(t()) -> [string()]. file_globs(#{files := Globs}) when is_list(Globs) -> Globs; file_globs(#{}) -> []. -endif. -spec ignore([t()] | t()) -> [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 files(RuleGroup :: [t()] | t()) -> [elvis_file:t()]. files(RuleGroup) when is_list(RuleGroup) -> lists:map(fun files/1, RuleGroup); files(#{resolved_files := Files}) -> Files; files(#{}) -> []. -spec rules (RulesL :: [t()]) -> [[elvis_rule:t()]]; (Rules :: t()) -> [elvis_rule:t()]. rules(Rules) when is_list(Rules) -> lists:map(fun rules/1, Rules); rules(#{rules := UserRules, ruleset := Ruleset}) -> DefaultRules = elvis_ruleset:rules(Ruleset), merge_rules(UserRules, DefaultRules); rules(#{rules := Rules}) -> filter_removed_rules(Rules); rules(#{ruleset := Ruleset}) -> elvis_ruleset:rules(Ruleset); rules(#{}) -> []. -spec ruleset(t()) -> Ruleset :: atom() | undefined. ruleset(ElvisConfig) -> maps:get(ruleset, ElvisConfig, undefined). %% @doc Takes a configuration and a list of files, filtering some %% of them according to the 'files' globs and ignore list. %% @end %% resolve_files/2 with a [t()] type is used in elvis project -spec resolve_files(Config :: [t()] | t(), Files :: [elvis_file:t()]) -> [t()] | t(). resolve_files(Config, Files) when is_list(Config) -> Fun = fun(RuleGroup) -> resolve_files(RuleGroup, Files) end, lists:map(Fun, Config); resolve_files(RuleGroup, Files) when is_map(RuleGroup) -> FileGlobs = maps:get(files, RuleGroup, []), Ignore = ignore(RuleGroup), FilteredFiles = elvis_file:filter_files(Files, FileGlobs, Ignore), RuleGroup#{resolved_files => FilteredFiles}. %% @doc Takes a configuration and finds all files according to its 'files' globs. %% @end -spec resolve_files(t()) -> t(). resolve_files(#{resolved_files := _} = RuleGroup) -> RuleGroup; resolve_files(#{files := FileGlobs} = RuleGroup) -> Ignore = ignore(RuleGroup), FoundFiles = elvis_file:find_files(FileGlobs), FilteredFiles = filter_by_ignore(FoundFiles, Ignore), RuleGroup#{resolved_files => FilteredFiles}. filter_by_ignore(Files, Ignore) -> lists:filter( fun(#{path := Path}) -> not lists:any( fun(Regex) -> match =:= re:run(Path, Regex, [{capture, none}]) end, Ignore ) end, 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 :: [t()] | t()) -> [t()] | t(). 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, #{resolved_files := Files} = RuleGroup) -> NewFiles = lists:map(Fun, Files), RuleGroup#{resolved_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_rule:t()]. merge_rules(UserRules0, DefaultRules) -> UserRules = filter_removed_rules(UserRules0), UnduplicatedRules = % Drops repeated rules % If any default rule is in UserRules it means the user lists:filtermap( % wants to override the rule. fun(Tuple) -> Rule = elvis_rule:from_tuple(Tuple), case not is_rule_override(Rule, UserRules) andalso not elvis_rule:disabled(Rule) of true -> {true, Rule}; false -> false end 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:filtermap( fun(Tuple) -> Rule = elvis_rule:from_tuple(Tuple), case elvis_rule:disabled(Rule) of false -> {true, Rule}; true -> false end end, UserRules ), UnduplicatedRules ++ OverrideRules. filter_removed_rules(RuleTuples) -> lists:filter( fun(RuleTuple) -> case elvis_rule:is_valid_from_tuple(RuleTuple) of {removed, _} -> false; _ -> true end end, RuleTuples ). -spec is_rule_override( Rule :: elvis_rule:t(), UserRules :: [{NS :: module(), Name :: atom()}] ) -> boolean(). is_rule_override(Rule, UserRules) -> lists:any( fun(UserRule) -> elvis_rule:same(Rule, elvis_rule:from_tuple(UserRule)) end, UserRules ). -spec validate(term(), undefined | string()) -> ok | {error, Message :: string()}. validate(ElvisConfig, File) -> try do_validate({config, ElvisConfig, File}) catch {invalid_config, Message} -> {error, lists:flatten(Message)} end. get_elvis_opt(OptName, Elvis) -> proplists:get_value(OptName, Elvis, default_for(OptName)). elvis_control_opts() -> [output_format, verbose, no_output, parallel, warnings_as_errors]. do_validate({app = _Option, Elvis, File}) -> maybe ok ?= is_nonempty_list("file '" ++ File ++ "'", Elvis), ok ?= proplist_keys_are_in( File, Elvis, elvis_control_opts() ++ [rulesets, config] ), OutputFormat = get_elvis_opt(output_format, Elvis), ok ?= is_one_of(output_format, OutputFormat, [colors, plain, parsable], File), Verbose = get_elvis_opt(verbose, Elvis), ok ?= is_boolean(verbose, Verbose, File), NoOutput = get_elvis_opt(no_output, Elvis), ok ?= is_boolean(no_output, NoOutput, File), Parallel = get_elvis_opt(parallel, Elvis), ok ?= is_pos_integer(parallel, Parallel, File), WarningsAsErrors = get_elvis_opt(warnings_as_errors, Elvis), ok ?= is_boolean(warnings_as_errors, WarningsAsErrors, File), CustomRulesets = get_elvis_opt(rulesets, Elvis), ok ?= are_valid_rulesets(rulesets, CustomRulesets, File), Configset = get_elvis_opt(config, Elvis), ok ?= is_valid_config(config, maps:keys(CustomRulesets), Configset, File), _ = elvis_ruleset:load_custom(from_static(rulesets, {app, Elvis})) else {v, true} -> ok; {error, FormatData} -> do_validate_throw(FormatData) end; do_validate({config = _Option, ElvisConfig, File}) -> maybe ok ?= is_valid_config(config, elvis_ruleset:custom_names(), ElvisConfig, File) else {v, true} -> ok; {error, FormatData} -> do_validate_throw(FormatData) end. -spec do_validate_throw(_) -> no_return(). do_validate_throw(FormatData) -> {Format, Data} = case is_list(FormatData) of false -> FormatData; true -> % Get only first result, for now % If we wanna be smarter we need to start concatenating readable strings [{Format0, Data0} | _] = FormatData, {Format0, Data0} end, throw({invalid_config, lists:flatten(io_lib:format(Format, Data))}). is_nonempty_list(What, List) when not is_list(List) orelse List =:= [] -> {error, {"~s is expected to exist and be a non-empty list.", [What]}}; is_nonempty_list(_What, _List) -> ok. proplist_keys_are_in(What, List, Keys) -> Filtered = [Element || {Element, _} <- List, not lists:member(Element, Keys)], case Filtered of [] -> ok; _ -> {error, {"in file '~s', the following keys are unknown: ~s.", [ What, elvis_utils:list_to_str(Filtered) ]}} end. is_one_of(What, Value, Possibilities, File) -> case lists:member(Value, Possibilities) of true -> ok; _ -> {error, {"in file '~s', key '~s' is expected to be one of the following: ~s.", [ File, What, elvis_utils:list_to_str(Possibilities) ]}} end. is_boolean(_What, Value, _File) when is_boolean(Value) -> ok; is_boolean(What, _Value, File) -> {error, {"in file '~s', key '~s' is expected to be a boolean.", [File, What]}}. is_pos_integer(_What, Value, _File) when is_integer(Value) andalso Value > 0 -> ok; is_pos_integer(What, _Value, File) -> {error, {"in file '~s', key '~s' is expected to be a positive integer.", [File, What]}}. are_valid_rulesets(What, CustomRulesets, File) -> maybe ok ?= is_map(What, CustomRulesets, File), ok ?= all_map_keys_are_atoms(What, CustomRulesets, File), ok ?= all_custom_rulesets_have_valid_rules(What, CustomRulesets, File), ok ?= no_default_ruleset_override(What, CustomRulesets, File) else {error, FormatData} -> {error, FormatData} end. is_map(_What, Value, _File) when is_map(Value) -> ok; is_map(What, _Value, File) -> {error, {"in file '~s', key '~s' is expected to be a map.", [File, What]}}. all_map_keys_are_atoms(What, Map, File) -> Filtered = [Key || Key <- maps:keys(Map), not is_atom(Key)], case Filtered of [] -> ok; _ -> {error, {"in file '~s', key '~s', map keys are expected to be atoms.", [File, What]}} end. all_custom_rulesets_have_valid_rules(What, CustomRulesets, File) -> AccOut = maps:fold( fun(CustomRuleset, RuleTuples, AccInO) -> lists:foldl( fun(RuleTuple, AccInI) -> case elvis_rule:is_valid_from_tuple(RuleTuple) of {true, _Rule} -> AccInI; {removed, Msg} -> _ = elvis_utils:warn("~s Skipping.", [Msg]), AccInI; {false, ValidError} -> [ {"in file '~s', map key '~s', in ruleset '~s', " ++ ValidError, [ File, What, CustomRuleset ]} | AccInI ] end end, AccInO, RuleTuples ) end, [], CustomRulesets ), case AccOut of [] -> ok; _ -> {error, lists:reverse(AccOut)} end. no_default_ruleset_override(What, CustomRulesets, File) -> Filtered = [ CustomRuleset || CustomRuleset <- maps:keys(CustomRulesets), elvis_ruleset:is_defined(CustomRuleset) ], case Filtered of [] -> ok; _ -> {error, { "in file '~s', key '~s', the following entries are not expected to be " "named after a default ruleset: ~s.", [ File, What, elvis_utils:list_to_str(Filtered) ] }} end. is_valid_config(What0, CustomRulesetNames, Configset0, File0) -> maybe What = case File0 of undefined -> "key '" ++ atom_to_list(What0) ++ "'"; _ -> "in file '" ++ File0 ++ "', key '" ++ atom_to_list(What0) ++ "'" end, ok ?= is_nonempty_list(What, Configset0), Configset = wrap_in_list(Configset0), ok ?= all_configs_are_valid(What, CustomRulesetNames, Configset) else {error, FormatData} -> {error, FormatData} end. wrap_in_list(Term) when is_list(Term) -> Term; wrap_in_list(Term) -> [Term]. all_configs_are_valid(What, CustomRulesetNames, Configset) -> {_PosNumber, ValidErrors} = lists:foldl( fun(Config, {PosNumber, AccIn}) -> AccOut = case config_is_valid(CustomRulesetNames, Config) of ok -> AccIn; {error, ValidError} -> [ {"~s, at list position number ~w, " ++ ValidError, [ What, PosNumber ]} | AccIn ] end, {PosNumber + 1, AccOut} end, {1, []}, Configset ), case ValidErrors of [] -> ok; _ -> {error, lists:reverse(ValidErrors)} end. get_config_opt(OptName, Config, false = _Compulsory) -> {ok, maps:get(OptName, Config, default_for([config, OptName]))}; get_config_opt(OptName, Config, true = _Compulsory) -> case maps:get(OptName, Config, undefined) of undefined -> {error, io_lib:format("is missing compulsory map key '~s'.", [OptName])}; _GetWithDefault -> get_config_opt(OptName, Config, false) end. config_is_valid(CustomRulesetNames, Config) -> maybe ok ?= map_keys_are_in(Config, [files, ignore, ruleset, rules]), {ok, FileGlobs} ?= get_config_opt(files, Config, true), ok ?= all_files_globs_are_valid(FileGlobs), {ok, Ignore} ?= get_config_opt(ignore, Config, false), ok ?= is_list_of_ignorables(ignore, Ignore), {ok, Ruleset} ?= get_config_opt(ruleset, Config, false), ok ?= defined_ruleset_is_custom_or_default(CustomRulesetNames, Ruleset), {ok, Rules} ?= get_config_opt(rules, Config, false), ok ?= all_rules_are_valid(rules, Rules), ok ?= either_rules_is_nonempty_or_ruleset_is_defined(Rules, Ruleset) else {error, {Format, Args}} -> {error, io_lib:format(Format, Args)}; {error, ValidError} -> {error, ValidError} end. map_keys_are_in(Map, _Keys) when not is_map(Map) -> {error, "element is expected to be a map."}; map_keys_are_in(Map, Keys) -> Filtered = [Key || Key <- maps:keys(Map), not lists:member(Key, Keys)], case Filtered of [] -> ok; _ -> {error, io_lib:format("the following keys are unknown: ~s.", [ elvis_utils:list_to_str(Filtered) ])} end. all_files_globs_are_valid(FileGlobs) when not is_list(FileGlobs) orelse FileGlobs =:= [] -> {error, "'files' is expected to be a non-empty list."}; all_files_globs_are_valid(FileGlobs) -> case lists:all(fun(G) -> is_list(G) andalso G =/= [] end, FileGlobs) of true -> case lists:any(fun(G) -> filelib:wildcard(G) =/= [] end, FileGlobs) of true -> ok; false -> {error, io_lib:format("yielded no files to analyse in [\"~s\"].", [ lists:join("\", \"", FileGlobs) ])} end; false -> {error, "'files' is expected to be a non-empty list of non-empty strings."} end. is_list_of_ignorables(What, List) when not is_list(List) -> {error, io_lib:format("'~s' is expected to be a list.", [What])}; is_list_of_ignorables(What, List) -> Filtered = [Element || Element <- List, not elvis_rule:is_ignorable(Element)], case Filtered of [] -> ok; _ -> {error, io_lib:format("in '~s', the following elements are not ignorable: ~s.", [ What, elvis_utils:list_to_str(Filtered) ])} end. defined_ruleset_is_custom_or_default(_CustomRulesetNames, undefined = _Ruleset) -> ok; defined_ruleset_is_custom_or_default(CustomRulesetNames, Ruleset) -> case lists:member(Ruleset, CustomRulesetNames) orelse elvis_ruleset:is_defined(Ruleset) of true -> ok; _ -> {error, io_lib:format("'~s' is expected to be either a custom or a default ruleset.", [ Ruleset ])} end. all_rules_are_valid(What, RuleTuples) when not is_list(RuleTuples) -> {error, io_lib:format("'~s' is expected to be a list.", [What])}; all_rules_are_valid(What, RuleTuples) -> AccOut = lists:foldl( fun(RuleTuple, AccInI) -> case elvis_rule:is_valid_from_tuple(RuleTuple) of {true, Rule} -> check_rule_for_options(Rule, AccInI); {removed, Msg} -> _ = elvis_utils:warn("~s Skipping.", [Msg]), AccInI; {false, ValidError} -> [io_lib:format("in '~s', " ++ ValidError, [What]) | AccInI] end end, [], RuleTuples ), case AccOut of [] -> ok; _ -> {error, lists:reverse(AccOut)} end. either_rules_is_nonempty_or_ruleset_is_defined([_ | _] = _Rules, _Ruleset) -> ok; either_rules_is_nonempty_or_ruleset_is_defined(_Rules, Ruleset) when Ruleset =/= undefined -> ok; either_rules_is_nonempty_or_ruleset_is_defined(_Rules, _Ruleset) -> {error, "either 'rules' is a non-empty list or 'ruleset' is defined."}. check_rule_for_options(Rule, AccInI) -> case elvis_rule:defkeys(Rule) of [] -> % No further validation possible. AccInI; DefKeysInput -> NS = elvis_rule:ns(Rule), Name = elvis_rule:name(Rule), % Bypass new/ constraints. DefKeys = [ignore | maps:keys(NS:default(Name))], case DefKeysInput -- DefKeys of [] -> AccInI; Extra -> [ io_lib:format( "in rule ~w/~w, the following options are unknown: ~s.", [NS, Name, elvis_utils:list_to_str(Extra)] ), AccInI ] end end. -spec inject_ignore([t()], [string()]) -> [t()]. inject_ignore(Configs, GitIgnored) -> [inject_gitignore(C, GitIgnored) || C <- Configs]. inject_gitignore(#{ignore := Ignore} = Config, GitIgnored) -> Config#{ignore => Ignore ++ GitIgnored}; inject_gitignore(Config, GitIgnored) -> Config#{ignore => GitIgnored}. git_check_ignore() -> maybe GitExecutable = os:find_executable("git"), true ?= is_list(GitExecutable), RevParse = os:cmd("git rev-parse --is-inside-work-tree 2>&1"), true ?= lists:prefix("true", RevParse), GitCheckIgnore = do_git_check_ignore(), string:lexemes(GitCheckIgnore, "\r\n") else _ -> [] end. do_git_check_ignore() -> case os:type() of {win32, _} -> os:cmd("git check-ignore '*'"); _ -> os:cmd("git check-ignore **/*") end. -ifdef(TEST). reset_validation() -> [persistent_term:erase(K) || {{elvis_config_validation, _} = K, _} <- persistent_term:get()], ok. -endif.