diff --git a/.gitignore b/.gitignore index c3704821..5f4b8e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ _build/ doc/ /rebar3.crashdump .rebar3/ -logs test/**/*.beam test/logs/ _checkouts/ diff --git a/README.md b/README.md index 8a9f7141..8816c27c 100644 --- a/README.md +++ b/README.md @@ -37,19 +37,30 @@ Once this is done you can apply the style rules in the following ways. #### Loading configuration from a file ```shell -1> ElvisConfig = elvis_config:from_file("elvis.config"). +1> ElvisConfig = elvis_config:config(). -2> elvis_core:rock(ElvisConfig). +2> application:set_env(elvis_core, verbose, true), elvis_core:rock(ElvisConfig). +Loading src/elvis_code.erl +# src/elvis_code.erl [OK] +Loading src/elvis_config.erl +# src/elvis_config.erl [OK] +Loading src/elvis_core.erl # src/elvis_core.erl [OK] -# src/elvis_result.erl [OK] -# src/elvis_style.erl [OK] -# src/elvis_utils.erl [OK] +Loading src/elvis_file.erl +# src/elvis_file.erl [OK] +... ok 3> ``` This will load the [configuration](#configuration), specified in file `elvis.config`, from the -current directory. If no configuration is found `{invalid_config, _}` is thrown. +current directory. + +If `elvis.config` is not present, the application will fall back to searching for configuration +parameters in `rebar.config`. If `rebar.config` is also unavailable, the application proceeds to +perform a tertiary lookup within its application environment (which can also be set via the +`app/sys.config` file, or e.g., via `application:set_env(elvis_core, Key, Value).` for the required +settings. #### Providing configuration as a value @@ -57,17 +68,11 @@ Another option for using `elvis_core` from the shell is to explicitly provide th an argument to `elvis_core:rock/1`: ```shell -1> ElvisConfig = [#{dirs => ["src"], filter => "*.erl", rules => []}]. +1> ElvisConfig = [#{dirs => ["src"], filter => "elvis_rule.erl", ruleset => erl_files}]. [#{dirs => ["src"],filter => "*.erl",rules => []}] -2> elvis_core:rock(ElvisConfig). -Loading src/elvis_core.erl -# src/elvis_core.erl [OK] -Loading src/elvis_result.erl -# src/elvis_result.erl [OK] -Loading src/elvis_style.erl -# src/elvis_style.erl [OK] -Loading src/elvis_utils.erl -# src/elvis_utils.erl [OK] +2> application:set_env(elvis_core, verbose, true), elvis_core:rock(ElvisConfig). +Loading src/elvis_rule.erl +# src/elvis_rule.erl [OK] ok 3> ``` @@ -118,6 +123,32 @@ An `elvis.config` file looks something like this: ]}]. ``` +To look at what is considered the "current default" configuration, do: + +```console +rebar3 shell +... +1> elvis_config:default(). +[#{filter => "*.erl", + dirs => ["apps/**/src/**","src/**"], + ruleset => erl_files}, + #{filter => "*.erl", + dirs => + ["apps/**/src/**","src/**","apps/**/include/**", + "include/**"], + ruleset => hrl_files}, + #{filter => "rebar.config", + dirs => ["."], + ruleset => rebar_config}, + #{filter => ".gitignore", + dirs => ["."], + ruleset => gitignore}] +2> +``` + +**Note**: this element might change with time. The above was what was generated when this +documentation was updated. + ### Files, rules and rulesets The `dirs` key is a list that tells `elvis_core` where it should look for the files that match @@ -238,7 +269,7 @@ found in this repository's [RULES.md](https://github.com/inaka/elvis_core/blob/m The implementation of a new rule is a function that takes 2 arguments in the following order: 1. `t:elvis_rule:t()`: the opaque rule to implement -1. `t:elvis_config:config()`: the value of option `config` as found in the +1. `t:elvis_config:t()`: the value of each element in list `config` as found in the [configuration](#configuration), This means you can define rules of your own (user-defined rules) as long as the functions that diff --git a/RULES.md b/RULES.md index 39cca9cd..3e0885aa 100644 --- a/RULES.md +++ b/RULES.md @@ -222,10 +222,6 @@ for `.hrl` files. , filter => "*.hrl" , ruleset => hrl_files } - , #{ dirs => ["."] - , filter => "Makefile" - , ruleset => makefiles - , rules => [] } , #{ dirs => ["."] , filter => "rebar.config" , ruleset => rebar_config diff --git a/config/elvis-test-custom-ruleset.config b/config/elvis-test-custom-ruleset.config index 0ce339a9..b5e084b0 100644 --- a/config/elvis-test-custom-ruleset.config +++ b/config/elvis-test-custom-ruleset.config @@ -3,7 +3,7 @@ {rulesets, #{project => [{elvis_text_style, no_tabs}]}}, {config, [ #{ - dirs => ["."], + dirs => ["../../../../_build/test/lib/elvis_core/test/dirs/src"], filter => "*.erl", ruleset => project } diff --git a/config/elvis-test-hrl-files.config b/config/elvis-test-hrl-files.config index bdf89b41..ffacb296 100644 --- a/config/elvis-test-hrl-files.config +++ b/config/elvis-test-hrl-files.config @@ -2,7 +2,7 @@ {elvis, [ {config, [ #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples/"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples/"], filter => "test_*.hrl", ruleset => hrl_files } diff --git a/config/elvis-test-pa.config b/config/elvis-test-pa.config index c9e0063d..f2539a2a 100644 --- a/config/elvis-test-pa.config +++ b/config/elvis-test-pa.config @@ -2,7 +2,7 @@ {elvis, [ {config, [ #{ - dirs => ["../../test/examples"], + dirs => ["../../../../test/examples"], filter => "user_defined_rules.erl", rules => [{user_defined_rules, rule}] } diff --git a/config/elvis-test.config b/config/elvis-test.config index 06623e21..339310f4 100644 --- a/config/elvis-test.config +++ b/config/elvis-test.config @@ -2,7 +2,7 @@ {elvis, [ {config, [ #{ - dirs => ["../../src"], + dirs => ["../../../../src"], filter => "*.erl", rules => [ @@ -27,7 +27,7 @@ ruleset => erl_files }, #{ - dirs => ["../../_build/test/lib/elvis_core/ebin"], + dirs => ["../../../../_build/test/lib/elvis_core/ebin"], filter => "*.beam", rules => [ diff --git a/config/elvis-umbrella.config b/config/elvis-umbrella.config index ad3f8dff..2f411f9a 100644 --- a/config/elvis-umbrella.config +++ b/config/elvis-umbrella.config @@ -4,10 +4,10 @@ #{ dirs => [ - "../../_build/test/lib/elvis_core/test/dirs/src/**", - "../../_build/test/lib/elvis_core/test/dirs/test/**", - "../../_build/test/lib/elvis_core/test/dirs/apps/**/src", - "../../_build/test/lib/elvis_core/test/dirs/apps/**/test" + "../../../../_build/test/lib/elvis_core/test/dirs/src", + "../../../../_build/test/lib/elvis_core/test/dirs/test", + "../../../../_build/test/lib/elvis_core/test/dirs/apps/**/src", + "../../../../_build/test/lib/elvis_core/test/dirs/apps/**/test" ], filter => "*.erl", ruleset => erl_files diff --git a/config/elvis.config b/config/elvis.config index a68f4ced..1d0d5da4 100644 --- a/config/elvis.config +++ b/config/elvis.config @@ -2,27 +2,27 @@ {elvis, [ {config, [ #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.erl", ruleset => erl_files }, #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.hrl", ruleset => hrl_files }, #{ - dirs => ["../../_build/test/lib/elvis_core/test/non_compilable_examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/non_compilable_examples"], filter => "*.erl", ruleset => erl_files }, #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.beam", ruleset => beam_files }, #{ - dirs => ["."], + dirs => ["../../../.."], filter => "rebar.config", ruleset => rebar_config } diff --git a/config/rebar.config b/config/rebar.config index d8755dfe..e503ccd7 100644 --- a/config/rebar.config +++ b/config/rebar.config @@ -8,7 +8,7 @@ {elvis, [ {config, [ #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.erl", rules => [{elvis_text_style, line_length, #{limit => 135}}] } diff --git a/config/test.config b/config/test.config index 1cdd1dd9..3fc32768 100644 --- a/config/test.config +++ b/config/test.config @@ -2,7 +2,7 @@ {elvis_core, [ {config, [ #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.erl", rules => [ @@ -15,13 +15,13 @@ ruleset => erl_files }, #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.hrl", rules => [], ruleset => hrl_files }, #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.beam", rules => [ @@ -33,15 +33,15 @@ ruleset => beam_files }, #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], - filter => "rebar.config", + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], + filter => "rebar3.config.success", ruleset => rebar_config }, #{ dirs => [ - "../../_build/test/lib/elvis_core/test/dirs/apps/app1", - "../../_build/test/lib/elvis_core/test/dirs/apps/app2" + "../../../../_build/test/lib/elvis_core/test/dirs/apps/app1", + "../../../../_build/test/lib/elvis_core/test/dirs/apps/app2" ], filter => ".gitignore", ruleset => gitignore diff --git a/config/test.pass.config b/config/test.pass.config index 41449693..1a428119 100644 --- a/config/test.pass.config +++ b/config/test.pass.config @@ -3,7 +3,7 @@ {elvis, [ {config, [ #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "*.erl", rules => [{elvis_text_style, line_length, #{limit => 800}}] } diff --git a/elvis.config b/elvis.config index e90c5bed..2c3aece5 100644 --- a/elvis.config +++ b/elvis.config @@ -7,17 +7,24 @@ rules => [ {elvis_style, no_invalid_dynamic_calls, #{ - ignore => [{elvis_core, rock_this, 2}] + ignore => [ + {elvis_core, rock_this, 2}, + {elvis_config, check_rule_for_options, 2} + ] }}, {elvis_style, dont_repeat_yourself, #{min_complexity => 20}}, {elvis_style, no_debug_call, #{ - ignore => [{elvis_result, print_item, 4}, {elvis_utils, print, 2}] + ignore => [ + {elvis_result, print_item, 4}, + {elvis_utils, do_output, 2}, + {elvis_core, main, 1} + ] }}, {elvis_style, no_god_modules, #{ignore => [elvis_style]}}, {elvis_style, no_throw, disable}, {elvis_style, max_function_length, disable}, {elvis_style, max_function_clause_length, disable}, - {elvis_style, max_module_length, #{ignore => [elvis_style]}}, + {elvis_style, max_module_length, #{ignore => [elvis_style, elvis_config]}}, {elvis_style, no_common_caveats_call, #{ ignore => [ {elvis_file, module, 1}, @@ -34,9 +41,13 @@ filter => "*.beam", rules => [ - {elvis_style, no_invalid_dynamic_calls, #{ignore => [elvis_core]}}, + {elvis_style, no_invalid_dynamic_calls, #{ + ignore => [elvis_core, elvis_config] + }}, {elvis_style, dont_repeat_yourself, #{min_complexity => 20}}, - {elvis_style, no_debug_call, #{ignore => [elvis_result, elvis_utils]}}, + {elvis_style, no_debug_call, #{ + ignore => [elvis_result, elvis_utils, elvis_core] + }}, {elvis_style, no_god_modules, #{ignore => [elvis_style]}}, {elvis_style, no_throw, disable}, {elvis_style, no_common_caveats_call, disable} diff --git a/rebar.config b/rebar.config index 91aea5b4..c2bfdafd 100644 --- a/rebar.config +++ b/rebar.config @@ -16,7 +16,7 @@ {deps, [{meck, "0.9.2"}]}, {erl_opts, [nowarn_missing_spec, nowarn_export_all]}, {dialyzer, [{warnings, [no_return, error_handling]}, {plt_extra_apps, [common_test]}]}, - {ct_opts, [{sys_config, ["./config/test.config"]}, {logdir, "./logs"}, {verbose, true}]}, + {ct_opts, [{sys_config, ["./config/test.config"]}, {verbose, true}]}, {cover_enabled, true}, {cover_opts, [verbose]} ]} @@ -63,6 +63,8 @@ {dialyzer, [{warnings, [no_return, unmatched_returns, error_handling, unknown]}]}. -{xref_checks, [undefined_function_calls, deprecated_function_calls, deprecated_functions]}. +{xref_checks, [ + undefined_function_calls, deprecated_function_calls, deprecated_functions, exports_not_used +]}. {xref_extra_paths, ["test/**"]}. diff --git a/src/elvis_code.erl b/src/elvis_code.erl index 09c0701b..a8bc8fc2 100644 --- a/src/elvis_code.erl +++ b/src/elvis_code.erl @@ -12,6 +12,9 @@ print_node/2 ]). +% These are local debug functions. +-ignore_xref([print_node/1, print_node/2]). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Public API %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -186,6 +189,6 @@ print_node(#{type := Type} = Node, CurrentLevel) -> Indentation = lists:duplicate(CurrentLevel * 4, $\s), Content = ktn_code:content(Node), - ok = elvis_utils:info("~s - [~p] ~p~n", [Indentation, CurrentLevel, Type]), + ok = elvis_utils:info("~s - [~p] ~p", [Indentation, CurrentLevel, Type]), _ = lists:map(fun(Child) -> print_node(Child, CurrentLevel + 1) end, Content), ok. diff --git a/src/elvis_config.erl b/src/elvis_config.erl index baf7871c..c909846b 100644 --- a/src/elvis_config.erl +++ b/src/elvis_config.erl @@ -2,136 +2,232 @@ -feature(maybe_expr, enable). --export([ - from_rebar/1, - from_file/1, - from_application_or_config/2, - validate/1 -]). -%% Geters +-export([from_rebar/1, from_file/1, validate_config/1, default/0]). +%% Getters -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([t/0]). +%% Options +-export([config/0, output_format/0, verbose/0, no_output/0, parallel/0]). +-export([set_output_format/1, set_verbose/1, set_no_output/1, set_parallel/1]). +% Corresponds to the 'config' key. -type t() :: map(). +-export_type([t/0]). + +-type fail_validation() :: {fail, [{throw, {invalid_config, Message :: string()}}]}. +-export_type([fail_validation/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]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Public %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec from_rebar(string()) -> [t()]. -from_rebar(Path) -> - case file:consult(Path) of - {ok, AppConfig} -> - load(config, load_initial(AppConfig), []); - {error, Reason} -> - throw(Reason) +fetch_elvis_config(Key, AppConfig) -> + Elvis = from_static(elvis, {app, AppConfig}), + _ = + case lists:member(Key, elvis_control_opts()) of + true -> + ok; + _ -> + do_validate({elvis, Elvis}) + end, + _ = elvis_ruleset:load_custom(from_static(rulesets, {elvis, Elvis})), + Elvis. + +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 from_file(string()) -> [t()]. -from_file(Path) -> - from_file(Path, config, []). - --spec from_file(string(), atom(), term()) -> [t()]. -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 +-spec config() -> [t()] | fail_validation(). +config() -> + try + for(config) + catch + {invalid_config, _} = Caught -> + {fail, [{throw, Caught}]} 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. +output_format() -> + for(output_format). --spec load(atom(), term(), term()) -> [t()]. -load(Key, ElvisConfig, Default) -> - proplists:get_value(Key, ElvisConfig, Default). +verbose() -> + for(verbose). --spec load_initial(term()) -> [term()]. -load_initial(AppConfig) -> - ElvisConfig = proplists:get_value(elvis, AppConfig, []), - RulesetsConfig = proplists:get_value(rulesets, ElvisConfig, #{}), - elvis_ruleset:set_rulesets(RulesetsConfig), - ElvisConfig. +no_output() -> + for(no_output). --spec validate(Config :: [t()]) -> ok. -validate([]) -> - throw({invalid_config, empty_config}); -validate(Config) -> - lists:foreach(fun do_validate/1, Config). +parallel() -> + for(parallel). -do_validate(RuleGroup) -> - maybe - ok ?= maybe_missing_dirs(RuleGroup), - ok ?= maybe_missing_filter(RuleGroup), - ok ?= maybe_missing_rules(RuleGroup), - ok ?= maybe_invalid_rules(RuleGroup) - else - {error, Error} -> - throw({invalid_config, Error}) - end. +set_output_format(OutputFormat) -> + set_env(output_format, OutputFormat). -maybe_missing_dirs(RuleGroup) -> - maybe_boolean_wrapper( - not (maps:is_key(dirs, RuleGroup) andalso not maps:is_key(filter, RuleGroup)), missing_dir - ). +set_verbose(Verbose) -> + set_env(verbose, Verbose). -maybe_missing_filter(RuleGroup) -> - maybe_boolean_wrapper( - maps:is_key(dirs, RuleGroup), missing_filter - ). +set_no_output(NoOutput) -> + set_env(no_output, NoOutput). -maybe_missing_rules(RuleGroup) -> - maybe_boolean_wrapper( - maps:is_key(rules, RuleGroup) orelse maps:is_key(ruleset, RuleGroup), missing_rules - ). +set_parallel(Parallel) -> + set_env(parallel, Parallel). -maybe_boolean_wrapper(true, _Flag) -> ok; -maybe_boolean_wrapper(false, Flag) -> {error, Flag}. +set_env(Key, Value) -> + application:set_env(elvis_core, Key, Value). -maybe_invalid_rules(#{rules := Rules}) -> - case invalid_rules(Rules) of - [] -> ok; - InvalidRules -> {error, {invalid_rules, InvalidRules}} - end; -maybe_invalid_rules(_) -> - ok. +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' found in application environment", [Key]), + Value + end. -invalid_rules(Rules) -> - lists:filtermap(fun is_invalid_rule/1, Rules). +for(Key) -> + AppDefault = default_for(app), + AppConfig = + case consult_elvis_config("elvis.config") of + AppDefault -> + % This might happen whether we fail to parse the file or it actually is [] + elvis_utils:debug("elvis.config unusable; falling back to rebar.config", []), + consult_rebar_config("rebar.config"); + AppConfig0 -> + AppConfig0 + end, + % If we got this far, the configuration is valid... + Elvis = fetch_elvis_config(Key, AppConfig), + from_static(Key, {elvis, Elvis}). + +consult_elvis_config(File) -> + case file:consult(File) of + {ok, [AppConfig]} when is_list(AppConfig) -> + elvis_utils:debug("elvis.config consultable; using it", []), + AppConfig; + {error, {Line, Mod, Term}} -> + % In this very specific case we prefer to throw, since we make efforts + % to provide a valid config., but we also need to make sure the file + % is readable + exit( + lists:flatten( + io_lib:format("elvis.config unconsultable: ~p, ~p, ~p", [Line, Mod, Term]) + ) + ); + _ -> + elvis_utils:debug("elvis.config unconsultable", []), + default_for(app) + end. -is_invalid_rule({NS, Rule, _}) -> - is_invalid_rule({NS, Rule}); -is_invalid_rule({NS, Rule}) -> - maybe - {module, NS} ?= code:ensure_loaded(NS), - ExportedRules = erlang:get_module_info(NS, exports), - case lists:keymember(Rule, 1, ExportedRules) of - false -> {true, {invalid_rule, {NS, Rule}}}; - _ -> false - end - else - {error, _} -> - elvis_utils:warn_prn( - "Invalid module (~p) specified in elvis.config.~n", - [NS] - ), - {true, {invalid_rule, {NS, Rule}}} +consult_rebar_config(File) -> + case file:consult(File) of + {ok, AppConfig0} when is_list(AppConfig0) -> + elvis_utils:debug("rebar.config consultable; using it", []), + AppConfig0; + _ -> + elvis_utils:debug("rebar.config unconsultable", []), + default_for(app) end. +-spec from_rebar(File :: string()) -> [t()] | fail_validation(). +from_rebar(File) -> + AppConfig = consult_rebar_config(File), + fetch_elvis_config_from(AppConfig). + +-spec from_file(File :: string()) -> [t()] | fail_validation(). +from_file(File) -> + AppConfig = consult_elvis_config(File), + fetch_elvis_config_from(AppConfig). + +fetch_elvis_config_from(AppConfig) -> + try fetch_elvis_config(undefined, AppConfig) of + Elvis -> + from_static(config, {elvis, Elvis}) + catch + {invalid_config, Message} -> + {fail, [{throw, {invalid_config, lists:flatten(Message)}}]} + end. + +default_for(app) -> + % This is the top-level element, before 'elvis' + []; +default_for(elvis) -> + []; +default_for(config) -> + []; +default_for(output_format) -> + colors; +default_for(verbose) -> + false; +default_for(no_output) -> + false; +default_for(parallel) -> + 1; +default_for(rulesets) -> + #{}; +default_for([config, dirs]) -> + []; +default_for([config, filter]) -> + ""; +default_for([config, ignore]) -> + []; +default_for([config, ruleset]) -> + undefined; +default_for([config, rules]) -> + []. + +default() -> + [ + #{ + dirs => [ + "apps/**/src", + "src" + ], + filter => "*.erl", + ruleset => erl_files + }, + #{ + dirs => [ + "apps/**/src", + "src", + "apps/**/include", + "include" + ], + filter => "*.hrl", + ruleset => hrl_files + }, + #{ + dirs => ["."], + filter => "rebar.config", + ruleset => rebar_config + }, + #{ + dirs => ["."], + filter => ".gitignore", + ruleset => gitignore + } + ]. + -spec dirs(Config :: [t()] | t()) -> [string()]. dirs(Config) when is_list(Config) -> lists:flatmap(fun dirs/1, Config); @@ -194,22 +290,6 @@ resolve_files(RuleGroup, Files) -> 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' @@ -221,7 +301,9 @@ resolve_files(#{files := _Files} = RuleGroup) -> resolve_files(#{dirs := Dirs} = RuleGroup) -> Filter = filter(RuleGroup), Files = elvis_file:find_files(Dirs, Filter), - resolve_files(RuleGroup, Files). + resolve_files(RuleGroup, Files); +resolve_files(#{}) -> + []. %% @doc Takes a function and configuration and applies the function to all %% file in the configuration. @@ -258,7 +340,7 @@ merge_rules(UserRules, DefaultRules) -> % wants to override the rule. fun(Tuple) -> Rule = elvis_rule:from_tuple(Tuple), - case not is_rule_override(Rule, UserRules) of + case not is_rule_override(Rule, UserRules) andalso not elvis_rule:disabled(Rule) of true -> {true, Rule}; false -> @@ -297,3 +379,421 @@ is_rule_override(Rule, UserRules) -> end, UserRules ). + +validate_config(ElvisConfig) -> + do_validate({config, ElvisConfig}). + +get_elvis_opt(OptName, Elvis) -> + proplists:get_value(OptName, Elvis, default_for(OptName)). + +elvis_control_opts() -> + [output_format, verbose, no_output, parallel]. + +do_validate({elvis = Option, Elvis}) -> + case check_flag(validation_started(Option)) of + true -> + ok; + _ -> + maybe + ok = flag(validation_started(Option)), + ok ?= is_nonempty_list(elvis, Elvis), + ok ?= + proplist_keys_are_in( + elvis, Elvis, elvis_control_opts() ++ [rulesets, config] + ), + OutputFormat = get_elvis_opt(output_format, Elvis), + ok ?= is_one_of('elvis.output_format', OutputFormat, [colors, plain, parsable]), + Verbose = get_elvis_opt(verbose, Elvis), + ok ?= is_boolean('elvis.verbose', Verbose), + NoOutput = get_elvis_opt(no_output, Elvis), + ok ?= is_boolean('elvis.no_output', NoOutput), + Parallel = get_elvis_opt(parallel, Elvis), + ok ?= is_pos_integer('elvis.parallel', Parallel), + CustomRulesets = get_elvis_opt(rulesets, Elvis), + ok ?= are_valid_rulesets('elvis.rulesets', CustomRulesets), + ElvisConfig = get_elvis_opt(config, Elvis), + ok ?= is_valid_config('elvis.config', maps:keys(CustomRulesets), ElvisConfig) + else + {error, FormatData} -> + do_validate_throw(FormatData) + end + end; +do_validate({config = Option, ElvisConfig}) -> + case check_flag(validation_started(Option)) of + true -> + ok; + _ -> + maybe + ok ?= is_valid_config('elvis.config', elvis_ruleset:custom_names(), ElvisConfig) + else + {error, FormatData} -> + do_validate_throw(FormatData) + end + 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, 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 '~s', the following keys are unknown: ~s.", [ + What, elvis_utils:list_to_str(Filtered) + ]}} + end. + +is_one_of(What, Value, Possibilities) -> + case lists:member(Value, Possibilities) of + true -> + ok; + _ -> + {error, + {"'~s' is expected to be one of the following: ~s.", [ + What, elvis_utils:list_to_str(Possibilities) + ]}} + end. + +is_boolean(_What, Value) when is_boolean(Value) -> + ok; +is_boolean(What, _Value) -> + {error, {"'~s' is expected to be a boolean.", [What]}}. + +is_pos_integer(_What, Value) when is_integer(Value) andalso Value > 0 -> + ok; +is_pos_integer(What, _Value) -> + {error, {"'~s' is expected to be a positive integer.", [What]}}. + +are_valid_rulesets(What, CustomRulesets) -> + maybe + ok ?= is_map(What, CustomRulesets), + ok ?= all_map_keys_are_atoms(What, CustomRulesets), + ok ?= all_custom_rulesets_have_valid_rules(What, CustomRulesets), + ok ?= no_default_ruleset_override(What, CustomRulesets) + else + {error, FormatData} -> + {error, FormatData} + end. + +is_map(_What, Value) when is_map(Value) -> + ok; +is_map(What, _Value) -> + {error, {"'~s' is expected to be a map.", [What]}}. + +all_map_keys_are_atoms(What, Map) -> + Filtered = [Key || Key <- maps:keys(Map), not is_atom(Key)], + case Filtered of + [] -> + ok; + _ -> + {error, {"in '~s', keys are expected to be atoms.", [What]}} + end. + +all_custom_rulesets_have_valid_rules(What, CustomRulesets) -> + AccOut = maps:fold( + fun(CustomRuleset, RuleTuples, AccInO) -> + lists:foldl( + fun(RuleTuple, AccInI) -> + case elvis_rule:is_valid_from_tuple(RuleTuple) of + {true, _Rule} -> + AccInI; + {false, ValidError} -> + [ + {"in '~s', in ruleset '~s', " ++ ValidError, [What, CustomRuleset]} + | AccInI + ] + end + end, + AccInO, + RuleTuples + ) + end, + [], + CustomRulesets + ), + case AccOut of + [] -> + ok; + _ -> + {error, lists:reverse(AccOut)} + end. + +no_default_ruleset_override(What, CustomRulesets) -> + Filtered = [ + CustomRuleset + || CustomRuleset <- maps:keys(CustomRulesets), elvis_ruleset:is_defined(CustomRuleset) + ], + case Filtered of + [] -> + ok; + _ -> + {error, + { + "in '~s', the following rulesets are not expected to be " + "named after a default ruleset: ~s.", + [ + What, elvis_utils:list_to_str(Filtered) + ] + }} + end. + +is_valid_config(What, CustomRulesetNames, Configset0) -> + maybe + 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} -> + [ + {"in '~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) -> + maps:get(OptName, Config, default_for([config, OptName])). + +config_is_valid(CustomRulesetNames, Config) -> + maybe + % We're keeping files, for the time being, because of elvis + % and the fact it knows about elvis_core's internals, but we + % should shortly revisit this + ok ?= map_keys_are_in(Config, [dirs, filter, ignore, ruleset, rules, files]), + Dirs = get_config_opt(dirs, Config), + ok ?= is_nonempty_list_of_dirs(dirs, Dirs), + Filter = get_config_opt(filter, Config), + ok ?= is_nonempty_string(filter, Filter), + ok ?= all_dirs_filter_combos_are_valid(Dirs, Filter), + Ignore = get_config_opt(ignore, Config), + ok ?= is_list_of_ignorables(ignore, Ignore), + Ruleset = get_config_opt(ruleset, Config), + ok ?= defined_ruleset_is_custom_or_default(CustomRulesetNames, Ruleset), + Rules = get_config_opt(rules, Config), + ok ?= all_rules_are_valid(rules, Rules), + ok ?= either_rules_is_nonempty_or_ruleset_is_defined(Rules, Ruleset) + else + {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. + +is_nonempty_list_of_dirs(What, List) when not is_list(List) orelse List =:= [] -> + {error, io_lib:format("'~s' is expected to be a non-empty list.", [What])}; +is_nonempty_list_of_dirs(What, List) -> + Filtered = [ + Element + || Element <- List, not io_lib:char_list(Element) orelse not holds_dir(Element) + ], + case Filtered of + [] -> + ok; + _ -> + {error, + io_lib:format( + "in '~s', the following elements are not (or don't contain) directories: ~s.", + [What, elvis_utils:list_to_str(Filtered)] + )} + end. + +holds_dir(Element) -> + Dirs = filelib:wildcard(Element), + Filtered = [Dir || Dir <- Dirs, filelib:is_dir(Dir)], + Filtered =/= []. + +is_nonempty_string(What, String) -> + case io_lib:char_list(String) andalso length(String) > 0 of + true -> + ok; + _ -> + {error, io_lib:format("'~s' is expected to be a non-empty string.", [What])} + end. + +all_dirs_filter_combos_are_valid(Dirs, Filter) -> + AccOut = lists:foldl( + fun(Dir, AccIn) -> + case filelib:wildcard(filename:join(Dir, Filter)) of + [_ | _] -> + AccIn; + _ -> + [ + io_lib:format( + "'' + '' combo '~s' + '~s' yielded no files to analyse.", [ + Dir, Filter + ] + ) + | AccIn + ] + end + end, + [], + Dirs + ), + case AccOut of + [] -> + ok; + _ -> + {error, lists:reverse(AccOut)} + 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); + {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, io_lib:format("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 = maps:keys(NS:default(Name)) ++ [ignore], + 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. + +flag({_Option, _What} = Obj) -> + _ = create_table(elvis_config), + true = ets:insert(elvis_config, Obj), + ok. + +check_flag({Option, _What} = Obj) -> + table_exists() andalso ets:lookup(elvis_config, Option) =:= [Obj]. + +validation_started(Option) -> + {Option, validation_started}. + +create_table(Table) -> + case table_exists() of + false -> + _ = ets:new(Table, [public, named_table]); + _ -> + ok + end. + +table_exists() -> + ets:info(elvis_config) =/= undefined. diff --git a/src/elvis_core.erl b/src/elvis_core.erl index d3307645..dfebc526 100644 --- a/src/elvis_core.erl +++ b/src/elvis_core.erl @@ -14,9 +14,20 @@ -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(). @@ -30,12 +41,27 @@ 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) -> - ok = elvis_config:validate(ElvisConfig), - Results = lists:map(fun do_parallel_rock/1, ElvisConfig), - lists:foldl(fun combine_results/2, ok, Results). + 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()]}. @@ -44,39 +70,46 @@ rock_this(Module, ElvisConfig) when is_atom(Module) -> Path = proplists:get_value(source, ModuleInfo), rock_this(Path, ElvisConfig); rock_this(Path, ElvisConfig) -> - elvis_config:validate(ElvisConfig), - 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) + 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:from_application_or_config(parallel, 1), + Parallel = elvis_config:parallel(), ElvisConfig = elvis_config:resolve_files(ElvisConfig0), Files = elvis_config:files(ElvisConfig), @@ -118,7 +151,7 @@ load_file_data(ElvisConfig, File) -> catch _:Reason -> Msg = "~p when loading file ~p.", - elvis_utils:error_prn(Msg, [Reason, Path]), + elvis_utils:error(Msg, [Reason, Path]), File end. @@ -126,9 +159,9 @@ load_file_data(ElvisConfig, File) -> main([]) -> ok = application:load(elvis_core), {module, _} = code:ensure_loaded(elvis_style), - case rock(elvis_config:from_file("elvis.config")) of + case rock(elvis_config:config()) of ok -> true; - _ -> halt(1) + _ -> elvis_utils:erlang_halt(1) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/elvis_gitignore.erl b/src/elvis_gitignore.erl index c2a9f16f..86414933 100644 --- a/src/elvis_gitignore.erl +++ b/src/elvis_gitignore.erl @@ -5,6 +5,9 @@ -export([required_patterns/2, forbidden_patterns/2]). +% The whole file is considered to have either callback functions or rules. +-ignore_xref(elvis_gitignore). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Default values %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/elvis_project.erl b/src/elvis_project.erl index 86084079..a050b70a 100644 --- a/src/elvis_project.erl +++ b/src/elvis_project.erl @@ -5,6 +5,9 @@ -export([no_branch_deps/2, protocol_for_deps/2]). +% The whole file is considered to have either callback functions or rules. +-ignore_xref(elvis_project). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Default values %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -70,12 +73,11 @@ no_branch_deps(Rule, _ElvisConfig) -> true -> false; false -> - {true, [ + {true, elvis_result:new_item( "Dependency '~s' uses a branch; prefer a tag or a specific commit", [AppName] - ) - ]} + )} end end, BadDeps diff --git a/src/elvis_result.erl b/src/elvis_result.erl index c2cc10c1..4b64bf11 100644 --- a/src/elvis_result.erl +++ b/src/elvis_result.erl @@ -16,6 +16,9 @@ %% Types -export_type([item/0, rule/0, file/0, elvis_error/0, elvis_warn/0, attrs/0]). +% API exports, not consumed locally. +-ignore_xref([get_path/1, get_rules/1, get_items/1, get_message/1, get_info/1, get_line_num/1]). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Records %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -167,7 +170,7 @@ get_line_num(#{line_num := LineNum}) -> -spec print_results(file() | [elvis_warn()]) -> ok. print_results(Results) -> - Format = elvis_config:from_application_or_config(output_format, colors), + Format = elvis_config:output_format(), print(Format, Results). -spec print(plain | colors | parsable, [file()] | file()) -> ok. @@ -182,11 +185,14 @@ print(Format, #{file := Path, rules := Rules}) -> parsable -> ok; _ -> + Verbose = elvis_config:verbose(), case status(Rules) of - ok -> + ok when Verbose -> elvis_utils:notice("# ~s [{{green-bold}}OK{{white-bold}}]", [Path]); + ok -> + ok; fail -> - elvis_utils:error("# ~s [{{red-bold}}FAIL{{white-bold}}]", [Path]) + elvis_utils:notice("# ~s [{{red-bold}}FAIL{{white-bold}}]", [Path]) end end, print_rules(Format, Path, Rules); @@ -213,7 +219,7 @@ print_rules( parsable -> ok; _ -> - elvis_utils:error( + elvis_utils:notice( " - ~p " "(https://github.com/inaka/elvis_core/tree/main/doc_rules/~p/~p.md)", [Name, Scope, Name] @@ -245,7 +251,7 @@ print_item( FMsg = io_lib:format(Msg, Info), io:format("~s:~p:~p:~s~n", [File, Ln, Name, FMsg]); _ -> - elvis_utils:error(" - " ++ Msg, Info) + elvis_utils:notice(" - " ++ Msg, Info) end, print_item(Format, File, Name, Items); print_item(Format, File, Name, [Error | Items]) -> @@ -255,9 +261,9 @@ print_item(_Format, _File, _Name, []) -> ok. print_error(#{error_msg := Msg, info := Info}) -> - elvis_utils:error_prn(Msg, Info); + elvis_utils:error(Msg, Info); print_error(#{warn_msg := Msg, info := Info}) -> - elvis_utils:warn_prn(Msg, Info). + elvis_utils:warn(Msg, Info). -spec status([file() | rule()]) -> ok | fail. status([]) -> diff --git a/src/elvis_rule.erl b/src/elvis_rule.erl index d00394df..fb7a7ce8 100644 --- a/src/elvis_rule.erl +++ b/src/elvis_rule.erl @@ -3,16 +3,18 @@ -export([ new/2, new/3, from_tuple/1, + is_valid_from_tuple/1, + is_ignorable/1, ns/1, name/1, def/1, - ignores/1, disabled/1, file/1, file/2, ignored/2, execute/2, option/2, defmap/1, + defkeys/1, ignorable/1, same/2 ]). @@ -49,7 +51,7 @@ new(NS, Name, Def) -> ignores = maps:get(ignore, Def, []) }. --spec from_tuple(Rule | NSName | NSNameDef) -> t() when +-spec from_tuple(Rule | NSName | NSNameDef) -> t() | invalid_tuple when Rule :: t(), NSName :: {NS :: module(), Name :: atom()}, NSNameDef :: {NS :: module(), Name :: atom(), Def :: disable | map()}. @@ -57,7 +59,7 @@ from_tuple(Rule) when is_record(Rule, rule) -> Rule; from_tuple({NS, Name}) -> from_tuple({NS, Name, #{}}); -from_tuple({NS, Name, Def0}) -> +from_tuple({NS, Name, Def0}) when is_map(Def0) orelse Def0 =:= disable -> {Def, Disable} = case Def0 of disable -> @@ -71,6 +73,74 @@ from_tuple({NS, Name, Def0}) -> disable(Rule); false -> Rule + end; +from_tuple(_) -> + invalid_tuple. + +is_valid_from_tuple(Tuple) -> + case from_tuple(Tuple) of + invalid_tuple -> + {false, "got an invalid tuple (is def. a map or 'disable'?)."}; + Rule -> + NS = ns(Rule), + _ = maybe_ensure_loaded(NS), + Name = name(Rule), + ArityForExecute = 2, + case + is_atom(NS) andalso is_atom(Name) andalso + erlang:function_exported(NS, Name, ArityForExecute) + of + true -> + {true, Rule}; + _ -> + {false, + io_lib:format("got an unexpected/invalid ~p:~p/~p combo.", [ + NS, Name, ArityForExecute + ])} + end + end. + +maybe_ensure_loaded(NS) when not is_atom(NS) -> + ok; +maybe_ensure_loaded(NS) -> + code:ensure_loaded(NS). + +% Module - invalid type +is_ignorable(Module) when not is_tuple(Module) andalso not is_atom(Module) -> + false; +% Module - test if valid +is_ignorable(Module) when is_atom(Module) -> + case maybe_ensure_loaded(Module) of + {module, _} -> + true; + _ -> + false + end; +% {Module, Function} - invalid type +is_ignorable({Module, Function}) when not is_atom(Module) orelse not is_atom(Function) -> + false; +% {Module, Function} - test if valid +is_ignorable({Module, Function}) -> + case is_ignorable(Module) of + true -> + Exports = Module:module_info(exports), + proplists:get_value(Function, Exports) =/= undefined; + false -> + false + end; +% {Module, Function, Arity} - invalid type +is_ignorable({Module, Function, Arity}) when + not is_atom(Module) orelse not is_atom(Function) orelse not is_integer(Arity) orelse Arity < 0 +-> + false; +% {Module, Function, Arity} - test if valid +is_ignorable({Module, Function, Arity}) -> + case is_ignorable(Module) of + true -> + Exports = Module:module_info(exports), + proplists:get_value(Function, Exports) =:= Arity; + false -> + false end. -spec ns(t()) -> module(). @@ -111,7 +181,7 @@ disable(Rule) -> -spec ignored(Needle :: ignorable(), t()) -> boolean(). ignored(Needle, Rule) -> - lists:member(Needle, Rule#rule.ignores). + lists:member(Needle, ignores(Rule)). -spec execute(t(), ElvisConfig) -> Results when ElvisConfig :: elvis_config:t(), @@ -138,12 +208,24 @@ default(Rule) -> -spec default(NS :: module(), Name :: atom()) -> def(). default(NS, Name) -> - NS:default(Name). + _ = maybe_ensure_loaded(NS), + ArityForDefault = 1, + case erlang:function_exported(NS, default, ArityForDefault) of + false -> + #{}; + true -> + NS:default(Name) + end. -spec defmap(map()) -> def(). defmap(Map) -> Map. +-spec defkeys(t()) -> [atom()]. +defkeys(Rule) -> + Def = def(Rule), + maps:keys(Def). + -spec ignorable(module() | {module(), atom()} | {module(), atom(), arity()}) -> ignorable(). ignorable(Ignorable) -> Ignorable. diff --git a/src/elvis_ruleset.erl b/src/elvis_ruleset.erl index c201fa45..80b059b5 100644 --- a/src/elvis_ruleset.erl +++ b/src/elvis_ruleset.erl @@ -2,18 +2,43 @@ -format(#{inline_items => none}). --export([rules/1, set_rulesets/1]). - --spec set_rulesets(#{atom() => list()}) -> ok. -set_rulesets(Rulesets) -> - Tid = ensure_clean_table(), - lists:foreach( - fun({Ruleset, NSNameDefs}) -> - Rules = [elvis_rule:from_tuple(NSNameDef) || NSNameDef <- NSNameDefs], - true = ets:insert(Tid, {Ruleset, Rules}) - end, - maps:to_list(Rulesets) - ). +-export([rules/1, load_custom/1, drop_custom/0, is_defined/1, custom_names/0]). + +-spec load_custom(#{atom() => list()}) -> ok. +load_custom(Rulesets) -> + {Existed, Tid} = ensure_table(), + case Existed of + false -> + elvis_utils:debug("loading custom rulesets into state", []), + lists:foreach( + fun({Ruleset, NSNameDefs}) -> + Rules = [elvis_rule:from_tuple(NSNameDef) || NSNameDef <- NSNameDefs], + true = ets:insert(Tid, {Ruleset, Rules}) + end, + maps:to_list(Rulesets) + ); + true -> + ok + end. + +custom_names() -> + case table_exists() of + false -> + []; + _ -> + proplists:get_keys(ets:tab2list(elvis_ruleset)) + end. + +drop_custom() -> + case table_exists() of + false -> + ok; + _ -> + ets:delete(elvis_ruleset) + end. + +is_defined(Ruleset) -> + elvis_ruleset:rules(Ruleset) =/= []. -spec rules(Group :: atom()) -> [elvis_rule:t()]. rules(gitignore) -> @@ -35,22 +60,32 @@ rules(rebar_config) -> rules(erl_files_test) -> erl_files_test_rules(); rules(Group) -> - try - ets:lookup_element(?MODULE, Group, 2) - catch - error:badarg -> + case table_exists() of + false -> + []; + _ -> + lookup(elvis_ruleset, Group) + end. + +lookup(Table, Group) -> + case ets:lookup(Table, Group) of + [{Group, Rules}] -> + Rules; + _ -> [] end. -ensure_clean_table() -> - case ets:info(?MODULE) of - undefined -> - ets:new(?MODULE, [set, named_table, {keypos, 1}, public]); +ensure_table() -> + case table_exists() of + false -> + {false, ets:new(elvis_ruleset, [set, named_table, {keypos, 1}, public])}; _ -> - true = ets:delete_all_objects(?MODULE), - ?MODULE + {true, elvis_ruleset} end. +table_exists() -> + ets:info(elvis_ruleset) =/= undefined. + gitignore_rules() -> [ elvis_rule:new(elvis_gitignore, forbidden_patterns), @@ -142,11 +177,11 @@ elvis_style_rules() -> erl_files_test_rules() -> trim( + elvis_style_rules(), [ elvis_rule:new(elvis_style, dont_repeat_yourself), elvis_rule:new(elvis_style, no_god_modules) - ], - elvis_style_rules() + ] ). elvis_text_style_rules() -> diff --git a/src/elvis_style.erl b/src/elvis_style.erl index b5f1ed2a..b56d32b2 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -63,6 +63,9 @@ prefer_include/2 ]). +% The whole file is considered to have either callback functions or rules. +-ignore_xref(elvis_style). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Default values %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/elvis_task.erl b/src/elvis_task.erl index 102fbdc6..97b36b60 100644 --- a/src/elvis_task.erl +++ b/src/elvis_task.erl @@ -50,9 +50,8 @@ do_in_parallel(FunWork, FunAcc, ExtraArgs, List, MaxW, 0, AccR, AccG) -> do_in_parallel(FunWork, FunAcc, ExtraArgs, List, MaxW, erlang:min(N, MaxW), AccR1, AccG1); do_in_parallel(FunWork, FunAcc, ExtraArgs, List, MaxW, RemainW, AccR, AccG) -> {WorkToBeDone, WorkRemain} = - try lists:split(RemainW, List) of - Res -> - Res + try + lists:split(RemainW, List) catch error:badarg -> {List, []} diff --git a/src/elvis_text_style.erl b/src/elvis_text_style.erl index c1ba7e05..2cf97618 100644 --- a/src/elvis_text_style.erl +++ b/src/elvis_text_style.erl @@ -10,6 +10,9 @@ no_redundant_blank_lines/2 ]). +% The whole file is considered to have either callback functions or rules. +-ignore_xref(elvis_text_style). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Default values %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/elvis_utils.erl b/src/elvis_utils.erl index 079a3e30..350ba137 100644 --- a/src/elvis_utils.erl +++ b/src/elvis_utils.erl @@ -3,15 +3,18 @@ -compile({no_auto_import, [error/2]}). %% General --export([erlang_halt/1, to_str/1, split_all_lines/1, split_all_lines/2]). -%% Output --export([info/2, notice/2, error/2, error_prn/2, warn_prn/2]). +-export([erlang_halt/1, list_to_str/1, to_str/1, split_all_lines/1, split_all_lines/2]). +%% Output / rebar3 +-export([debug/2, info/2, notice/2, warn/2, error/2, abort/2]). + +% These call (but verify if exported) rebar3-specific functions. +-ignore_xref(do_output/2). +-ignore_xref(abort/2). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Public %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% @doc This is defined so that it can be mocked for tests. -spec erlang_halt(integer()) -> no_return(). erlang_halt(Code) -> halt(Code). @@ -26,6 +29,23 @@ to_str(Arg) when is_integer(Arg) -> to_str(Arg) when is_list(Arg) -> Arg. +list_to_str(What) -> + list_to_str(What, []). + +list_to_str([], Acc) -> + "[" ++ string:join(Acc, ", ") ++ "]"; +list_to_str([H0 | T], Acc) -> + H = + case H0 of + H0 when is_list(H0) -> + "\"" ++ H0 ++ "\""; + H0 when is_binary(H0) -> + "<<\"" ++ to_str(H0) ++ "\">>"; + _ -> + to_str(H0) + end, + list_to_str(T, [H | Acc]). + -spec split_all_lines(binary()) -> [binary(), ...]. split_all_lines(Binary) -> split_all_lines(Binary, []). @@ -34,51 +54,6 @@ split_all_lines(Binary) -> split_all_lines(Binary, Opts) -> binary:split(Binary, [<<"\r\n">>, <<"\n">>], [global | Opts]). --spec info(string(), [term()]) -> ok. -info(Message, Args) -> - ColoredMessage = Message ++ "{{reset}}~n", - print_info(ColoredMessage, Args). - --spec notice(string(), [term()]) -> ok. -notice(Message, Args) -> - ColoredMessage = "{{white-bold}}" ++ Message ++ "{{reset}}~n", - print_info(ColoredMessage, Args). - --spec error(string(), [term()]) -> ok. -error(Message, Args) -> - ColoredMessage = "{{white-bold}}" ++ Message ++ "{{reset}}~n", - print(ColoredMessage, Args). - --spec error_prn(string(), [term()]) -> ok. -error_prn(Message, Args) -> - ColoredMessage = "{{red}}Error: {{reset}}" ++ Message ++ "{{reset}}~n", - print(ColoredMessage, Args). - --spec warn_prn(string(), [term()]) -> ok. -warn_prn(Message, Args) -> - ColoredMessage = "{{magenta}}Warning: {{reset}}" ++ Message ++ "{{reset}}~n", - print(ColoredMessage, Args). - --spec print_info(string(), [term()]) -> ok. -print_info(Message, Args) -> - case elvis_config:from_application_or_config(verbose, false) of - true -> - print(Message, Args); - false -> - ok - end. - --spec print(string(), [term()]) -> ok. -print(Message, Args) -> - case elvis_config:from_application_or_config(no_output, false) of - true -> - ok; - _ -> - Output = io_lib:format(Message, Args), - EscapedOutput = escape_format_str(Output), - io:format(parse_colors(EscapedOutput)) - end. - -spec parse_colors(string()) -> string(). parse_colors(Message) -> Colors = @@ -93,7 +68,7 @@ parse_colors(Message) -> "reset" => "\e[0m" }, Opts = [global, {return, list}], - case elvis_config:from_application_or_config(output_format, colors) of + case elvis_config:output_format() of P when P =:= plain; P =:= parsable -> re:replace(Message, "{{.*?}}", "", Opts); colors -> @@ -105,9 +80,73 @@ parse_colors(Message) -> lists:foldl(Fun, Message, maps:keys(Colors)) end. --spec escape_format_str(string()) -> string(). -escape_format_str(String) -> +-spec escape_chars(string()) -> string(). +escape_chars(String) -> Binary = list_to_binary(String), Result = re:replace(Binary, "[^~]~", "~~", [global]), ResultBin = iolist_to_binary(Result), binary_to_list(ResultBin). + +debug(Format, Data) -> + output(debug, "Elvis: " ++ Format, Data). + +info(Format, Data) -> + output(info, Format ++ "{{reset}}~n", Data). + +notice(Format, Data) -> + output(notice, "{{white-bold}}" ++ Format ++ "{{reset}}~n", Data). + +warn(Format, Data) -> + output(warn, "{{magenta}}Warning: {{reset}}" ++ Format ++ "{{reset}}~n", Data). + +error(Format, Data) -> + output(error, "{{red}}Error: {{reset}}" ++ Format ++ "{{reset}}~n", Data). + +-spec output(debug | info | notice | warn | error, Format :: io:format(), Data :: [term()]) -> ok. +output(debug = _Type, Format, Data) -> + Chars = io_lib:format(Format, Data), + do_output(debug, Chars); +output(Type, Format, Data) -> + Chars = io_lib:format(Format, Data), + EscapedChars = escape_chars(Chars), + ColorParsedChars = parse_colors(EscapedChars), + case elvis_config:no_output() of + true -> + ok; + _ -> + do_output(Type, ColorParsedChars) + end. + +-dialyzer({nowarn_function, do_output/2}). +do_output(debug, Chars) -> + case erlang:function_exported(rebar_api, debug, 2) of + true -> + rebar_api:debug(Chars, []); + false -> + ok + end; +do_output(info, Chars) -> + case elvis_config:verbose() of + true -> + io:format(Chars); + false -> + ok + end; +do_output(notice, Chars) -> + io:format(Chars); +do_output(warn, Chars) -> + io:format(Chars); +do_output(error, Chars) -> + io:format(Chars). + +-dialyzer({nowarn_function, abort/2}). +-spec abort(Format :: io:format(), Data :: [term()]) -> no_return(). +abort(Format0, Data) -> + Format = "Elvis: " ++ Format0 ++ "~n", + case erlang:function_exported(rebar_api, abort, 2) of + true -> + rebar_api:abort(Format, [Data]); + false -> + output(error, Format, Data), + throw(elvis_abort) + end. diff --git a/test/dirs/apps/app3/include/app3_example.hrl b/test/dirs/apps/app3/include/app3_example.hrl new file mode 100644 index 00000000..e69de29b diff --git a/test/dirs/apps/app3/rebar.config b/test/dirs/apps/app3/rebar.config new file mode 100644 index 00000000..0ec61832 --- /dev/null +++ b/test/dirs/apps/app3/rebar.config @@ -0,0 +1 @@ +{}. diff --git a/test/dirs/apps/app3/src/app3_example.erl b/test/dirs/apps/app3/src/app3_example.erl new file mode 100644 index 00000000..536991ea --- /dev/null +++ b/test/dirs/apps/app3/src/app3_example.erl @@ -0,0 +1 @@ +-module(app3_example). diff --git a/test/elvis_SUITE.erl b/test/elvis_SUITE.erl index 6a410097..377c13a1 100644 --- a/test/elvis_SUITE.erl +++ b/test/elvis_SUITE.erl @@ -8,8 +8,6 @@ rock_with_empty_list_config/1, rock_with_incomplete_config/1, rock_with_list_config/1, - rock_with_file_config/1, - rock_with_rebar_default_config/1, rock_this/1, rock_without_colors/1, rock_with_parsable/1, @@ -24,7 +22,6 @@ rock_with_umbrella_apps/1, custom_ruleset/1, hrl_ruleset/1, - throw_configuration/1, find_file_and_check_src/1, find_file_with_ignore/1, invalid_file/1, @@ -61,74 +58,28 @@ end_per_suite(Config) -> %%% Rocking rock_with_empty_map_config(_Config) -> - ok = - try - ok = elvis_core:rock([#{}]), - fail - catch - {invalid_config, _} -> - ok - end, - ok = - try - ok = elvis_core:rock([#{} || X <- lists:seq(1, 10), X < 1]), - fail - catch - {invalid_config, _} -> - ok - end. + {fail, [{throw, {invalid_config, _}}]} = elvis_core:rock([#{}]), + {fail, [{throw, {invalid_config, _}}]} = elvis_core:rock([]), + ok. rock_with_empty_list_config(_Config) -> - ok = - try - ok = elvis_core:rock([#{}, #{}]), - fail - catch - {invalid_config, _} -> - ok - end. + {fail, [{throw, {invalid_config, _}}]} = elvis_core:rock([#{}, #{}]), + ok. rock_with_incomplete_config(_Config) -> ElvisConfig = [#{dirs => ["src"]}], - ok = - try - ok = elvis_core:rock(ElvisConfig), - fail - catch - {invalid_config, _} -> - ok - end. - -rock_with_list_config(_Config) -> - ElvisConfig = [#{dirs => ["src"], rules => [], filter => "*.erl"}], - ok = - try - ok = elvis_core:rock(ElvisConfig) - catch - {invalid_config, _} -> - fail - end. - -rock_with_file_config(_Config) -> - ConfigPath = "../../config/elvis.config", - ElvisConfig = elvis_config:from_file(ConfigPath), - Fun = fun() -> elvis_core:rock(ElvisConfig) end, - Expected = - "# \\.\\./\\.\\./_build/test/lib/elvis_core/test/" ++ "examples/.*\\.erl.*FAIL", - [_ | _] = check_some_line_output(Fun, Expected, fun matches_regex/2), + {fail, [{throw, {invalid_config, _}}]} = elvis_core:rock(ElvisConfig), ok. -rock_with_rebar_default_config(_Config) -> - {ok, _} = file:copy("../../config/rebar.config", "rebar.config"), - ElvisConfig = elvis_config:from_rebar("rebar.config"), - [#{name := line_length}] = - try - {fail, Results} = elvis_core:rock(ElvisConfig), - [Rule || #{rules := [Rule]} <- Results] - after - file:delete("rebar.config") - end, - ok. +rock_with_list_config(_Config) -> + ElvisConfig = [ + #{ + dirs => ["../../../../test/dirs/src"], + rules => [{elvis_text_style, line_length, disable}], + filter => "*.erl" + } + ], + ok = elvis_core:rock(ElvisConfig). rock_this(_Config) -> ElvisConfig = elvis_test_utils:config(), @@ -142,7 +93,7 @@ rock_this(_Config) -> ok end, - Path = "../../_build/test/lib/elvis_core/test/examples/fail_line_length.erl", + Path = "../../../../_build/test/lib/elvis_core/test/examples/fail_line_length.erl", {fail, _} = elvis_core:rock_this(Path, ElvisConfig), ok. @@ -152,7 +103,11 @@ rock_without_colors(_Config) -> Fun = fun() -> elvis_core:rock(ElvisConfig) end, Expected = "\\e.*?m", ok = - try check_some_line_output(Fun, Expected, fun matches_regex/2) of + try + elvis_test_utils:check_some_line_output( + Fun, Expected, fun elvis_test_utils:matches_regex/2 + ) + of Result -> ct:fail("Unexpected result ~p", [Result]) catch @@ -167,7 +122,11 @@ rock_with_parsable(_Config) -> Fun = fun() -> elvis_core:rock(ElvisConfig) end, Expected = ".*\\.erl:\\d:[a-zA-Z0-9_]+:.*", ok = - try check_some_line_output(Fun, Expected, fun matches_regex/2) of + try + elvis_test_utils:check_some_line_output( + Fun, Expected, fun elvis_test_utils:matches_regex/2 + ) + of Result -> io:format("~p~n", [Result]) catch @@ -180,7 +139,7 @@ rock_with_parsable(_Config) -> rock_with_non_parsable_file(_Config) -> ElvisConfig = elvis_test_utils:config(), Path = - "../../_build/test/lib/elvis_core/test/non_compilable_examples/fail_non_parsable_file.erl", + "../../../../_build/test/lib/elvis_core/test/non_compilable_examples/fail_non_parsable_file.erl", try elvis_core:rock_this(Path, ElvisConfig) catch @@ -200,11 +159,13 @@ rock_with_errors_has_output(_Config) -> ElvisConfig = elvis_test_utils:config(), Fun = fun() -> elvis_core:rock(ElvisConfig) end, Expected = "FAIL", - [_ | _] = check_some_line_output(Fun, Expected, fun matches_regex/2), + [_ | _] = elvis_test_utils:check_some_line_output( + Fun, Expected, fun elvis_test_utils:matches_regex/2 + ), ok. rock_without_errors_has_no_output(_Config) -> - ConfigPath = "../../config/test.pass.config", + ConfigPath = "../../../../config/test.pass.config", ElvisConfig = elvis_config:from_file(ConfigPath), Fun = fun() -> elvis_core:rock(ElvisConfig) end, Output = get_output(Fun), @@ -226,43 +187,34 @@ rock_without_errors_and_with_verbose_has_output(_Config) -> ElvisConfig = elvis_test_utils:config(), Fun = fun() -> elvis_core:rock(ElvisConfig) end, Expected = "OK", - [_ | _] = check_some_line_output(Fun, Expected, fun matches_regex/2), + [_ | _] = elvis_test_utils:check_some_line_output( + Fun, Expected, fun elvis_test_utils:matches_regex/2 + ), application:unset_env(elvis_core, verbose), ok. rock_with_rule_groups(_Config) -> % elvis_config will load default elvis_core rules for every - % rule_group in the config. + % rule_group in the configuration RulesGroupConfig = [ #{ - dirs => ["src"], + dirs => ["../../../../test/dirs/apps/app3/src"], filter => "*.erl", ruleset => erl_files }, #{ - dirs => ["include"], - filter => "*.erl", + dirs => ["../../../../test/dirs/apps/app3/include"], + filter => "*.hrl", ruleset => hrl_files }, #{ - dirs => ["_build/test/lib/elvis_core/ebin"], - filter => "*.beam", - ruleset => beam_files - }, - #{ - dirs => ["."], + dirs => ["../../../../test/dirs/apps/app3"], filter => "rebar.config", ruleset => rebar_config } ], - ok = - try - ok = elvis_core:rock(RulesGroupConfig) - catch - {invalid_config, _} -> - fail - end, + ok = elvis_core:rock(RulesGroupConfig), % Override default elvis_core rules without ruleset should fail. OverrideFailConfig = [ @@ -275,19 +227,12 @@ rock_with_rule_groups(_Config) -> ] } ], - ok = - try - _ = elvis_core:rock(OverrideFailConfig), - fail - catch - {invalid_config, _} -> - ok - end, + {fail, [{throw, {invalid_config, _}}]} = elvis_core:rock(OverrideFailConfig), % Override default elvis_core rules. OverrideConfig = [ #{ - dirs => ["src"], + dirs => ["../../../../test/dirs/apps/app3/src"], filter => "*.erl", ruleset => erl_files, rules => @@ -299,25 +244,21 @@ rock_with_rule_groups(_Config) -> ] }, #{ - dirs => ["."], + dirs => ["../../../../test/dirs/apps/app3"], filter => "rebar.config", ruleset => rebar_config } ], - ok = - try - ok = elvis_core:rock(OverrideConfig) - catch - {invalid_config, _} -> - fail - end. + ok = elvis_core:rock(OverrideConfig). rock_this_skipping_files(_Config) -> meck:new(elvis_file, [passthrough]), - Dirs = ["../../_build/test/lib/elvis_core/test/examples"], + Dirs = ["../../../../_build/test/lib/elvis_core/test/examples"], [File] = elvis_file:find_files(Dirs, "small.erl"), Path = elvis_file:path(File), - ConfigPath = "../../config/elvis-test-pa.config", + ConfigPath = "../../../../config/elvis-test-pa.config", + {ok, user_defined_rules} = compile:file("../../../../test/examples/user_defined_rules.erl"), + {module, user_defined_rules} = code:ensure_loaded(user_defined_rules), ElvisConfig = elvis_config:from_file(ConfigPath), ok = elvis_core:rock_this(Path, ElvisConfig), 0 = meck:num_calls(elvis_file, load_file_data, '_'), @@ -326,7 +267,7 @@ rock_this_skipping_files(_Config) -> rock_this_not_skipping_files(_Config) -> meck:new(elvis_file, [passthrough]), - Dirs = ["../../_build/test/lib/elvis_core/test/examples"], + Dirs = ["../../../../_build/test/lib/elvis_core/test/examples"], [File] = elvis_file:find_files(Dirs, "small.erl"), Path = elvis_file:path(File), ElvisConfig = elvis_test_utils:config(), @@ -336,94 +277,77 @@ rock_this_not_skipping_files(_Config) -> ok. rock_with_umbrella_apps(_Config) -> - ElvisUmbrellaConfigFile = "../../config/elvis-umbrella.config", + ElvisUmbrellaConfigFile = "../../../../config/elvis-umbrella.config", ElvisConfig = elvis_config:from_file(ElvisUmbrellaConfigFile), {fail, [ - #{file := "../../_build/test/lib/elvis_core/test/dirs/test/dir_test.erl"}, - #{file := "../../_build/test/lib/elvis_core/test/dirs/src/dirs_src.erl"}, + #{file := "../../../../_build/test/lib/elvis_core/test/dirs/test/dir_test.erl"}, + #{file := "../../../../_build/test/lib/elvis_core/test/dirs/src/dirs_src.erl"}, #{ file := - "../../_build/test/lib/elvis_core/test/dirs/apps/app2/test/dirs_apps_app2_test.erl" + "../../../../_build/test/lib/elvis_core/test/dirs/apps/app3/src/app3_example.erl" }, #{ file := - "../../_build/test/lib/elvis_core/test/dirs/apps/app2/src/dirs_apps_app2_src.erl" + "../../../../_build/test/lib/elvis_core/test/dirs/apps/app2/test/dirs_apps_app2_test.erl" }, #{ file := - "../../_build/test/lib/elvis_core/test/dirs/apps/app1/test/dirs_apps_app1_test.erl" + "../../../../_build/test/lib/elvis_core/test/dirs/apps/app2/src/dirs_apps_app2_src.erl" }, #{ file := - "../../_build/test/lib/elvis_core/test/dirs/apps/app1/src/dirs_apps_app1_src.erl" + "../../../../_build/test/lib/elvis_core/test/dirs/apps/app1/test/dirs_apps_app1_test.erl" + }, + #{ + file := + "../../../../_build/test/lib/elvis_core/test/dirs/apps/app1/src/dirs_apps_app1_src.erl" } ]} = elvis_core:rock(ElvisConfig), ok. rock_with_invalid_rules(_Config) -> - ConfigPath = "../../test/examples/invalid_rules.elvis.config", - ElvisConfig = elvis_config:from_file(ConfigPath), - ExpectedErrorMessage = - {invalid_rules, [ - {invalid_rule, {elvis_style, not_existing_rule}}, - {invalid_rule, {elvis_style, what_is_this_rule}}, - {invalid_rule, {not_existing_module, dont_repeat_yourself}}, - {invalid_rule, {not_existing_module, dont_repeat_yourself}} - ]}, - try - ok = elvis_core:rock(ElvisConfig), - ct:fail("Elvis should not have rocked with ~p", [ElvisConfig]) - catch - {invalid_config, ExpectedErrorMessage} -> - ok - end. + ConfigPath = "../../../../test/examples/invalid_rules.elvis.config", + {fail, [{throw, {invalid_config, _}}]} = elvis_config:from_file(ConfigPath), + ok. %%%%%%%%%%%%%%% %%% Utils custom_ruleset(_Config) -> - ConfigPath = "../../config/elvis-test-custom-ruleset.config", + ConfigPath = "../../../../config/elvis-test-custom-ruleset.config", ElvisConfig = elvis_config:from_file(ConfigPath), NoTabs = elvis_rule:new(elvis_text_style, no_tabs), [[NoTabs]] = elvis_config:rules(ElvisConfig), + %% this is also done by :rock and :rock_this + _ = elvis_ruleset:drop_custom(), + %% read unknown ruleset configuration to ensure rulesets from %% previous load do not stick around - ConfigPathMissing = "../../config/elvis-test-unknown-ruleset.config", + ConfigPathMissing = "../../../../config/elvis-test-unknown-ruleset.config", ElvisConfigMissing = elvis_config:from_file(ConfigPathMissing), [[]] = elvis_config:rules(ElvisConfigMissing), ok. hrl_ruleset(_Config) -> - ConfigPath = "../../config/elvis-test-hrl-files.config", + ConfigPath = "../../../../config/elvis-test-hrl-files.config", ElvisConfig = elvis_config:from_file(ConfigPath), {fail, [ - #{file := "../../_build/test/lib/elvis_core/test/examples/test_good.hrl", rules := []}, #{ - file := "../../_build/test/lib/elvis_core/test/examples/test_bad.hrl", + file := "../../../../_build/test/lib/elvis_core/test/examples/test_good.hrl", + rules := [] + }, + #{ + file := "../../../../_build/test/lib/elvis_core/test/examples/test_bad.hrl", rules := [#{name := line_length}] } ]} = elvis_core:rock(ElvisConfig), ok. -throw_configuration(_Config) -> - Filename = "./elvis.config", - ok = file:write_file(Filename, <<"-">>), - ok = - try - _ = elvis_config:from_file(Filename), - fail - catch - _ -> - ok - after - file:delete(Filename) - end. - find_file_and_check_src(_Config) -> - Dirs = ["../../test/examples"], + Dirs = ["../../../../test/examples"], [] = elvis_file:find_files(Dirs, "doesnt_exist.erl"), [File] = elvis_file:find_files(Dirs, "small.erl"), @@ -439,12 +363,12 @@ find_file_and_check_src(_Config) -> {error, enoent} = elvis_file:src(#{path => "doesnt_exist.erl"}). find_file_with_ignore(_Config) -> - Dirs = ["../../test/examples"], + Dirs = ["../../../../test/examples"], Filter = "find_test*.erl", Ignore = elvis_config:ignore(#{ignore => [find_test1]}), Files = [_, _] = elvis_file:find_files(Dirs, Filter), [_, _] = elvis_file:filter_files(Files, Dirs, Filter, []), - [#{path := "../../test/examples/find_test2.erl"}] = + [#{path := "../../../../test/examples/find_test2.erl"}] = elvis_file:filter_files(Files, Dirs, Filter, Ignore). invalid_file(_Config) -> @@ -496,24 +420,8 @@ chunk_fold_task(Elem, Multiplier) -> %%% Private %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -check_some_line_output(Fun, Expected, FilterFun) -> - _ = ct:capture_start(), - _ = Fun(), - _ = ct:capture_stop(), - Lines = ct:capture_get([]), - ListFun = fun(Line) -> FilterFun(Line, Expected) end, - [_ | _] = lists:filter(ListFun, Lines). - get_output(Fun) -> _ = ct:capture_start(), _ = Fun(), _ = ct:capture_stop(), ct:capture_get([]). - -matches_regex(Result, Regex) -> - case re:run(Result, Regex) of - {match, _} -> - true; - nomatch -> - false - end. diff --git a/test/elvis_config_SUITE.erl b/test/elvis_config_SUITE.erl new file mode 100644 index 00000000..e6f6a492 --- /dev/null +++ b/test/elvis_config_SUITE.erl @@ -0,0 +1,62 @@ +-module(elvis_config_SUITE). + +-behaviour(ct_suite). + +% ct_suite +-export([all/0, init_per_suite/1, end_per_suite/1]). + +% Tests +-export([rock_with_file_config/1]). +-export([rock_with_rebar_default_config/1]). +-export([throw_configuration/1]). + +% ct_suite +all() -> + Exports = ?MODULE:module_info(exports), + [F || {F, _} <- Exports, not lists:member(F, elvis_test_utils:excluded_funs_all())]. + +init_per_suite(Config) -> + {ok, _} = application:ensure_all_started(elvis_core), + Config. + +end_per_suite(Config) -> + ok = application:stop(elvis_core), + Config. + +% Tests +rock_with_file_config(_Config) -> + ConfigPath = "../../../../config/elvis.config", + ElvisConfig = elvis_config:from_file(ConfigPath), + Fun = fun() -> elvis_core:rock(ElvisConfig) end, + Expected = + "# \\.\\./\\.\\./\\.\\./\\.\\./_build/test/lib/elvis_core/test/" ++ + "examples/.*\\.erl.*FAIL", + [_ | _] = elvis_test_utils:check_some_line_output( + Fun, Expected, fun elvis_test_utils:matches_regex/2 + ), + ok. + +rock_with_rebar_default_config(_Config) -> + {ok, _} = file:copy("../../../../config/rebar.config", "rebar.config"), + ElvisConfig = elvis_config:from_rebar("rebar.config"), + [#{name := line_length}] = + try + {fail, Results} = elvis_core:rock(ElvisConfig), + [Rule || #{rules := [Rule]} <- Results] + after + file:delete("rebar.config") + end, + ok. + +throw_configuration(_Config) -> + Filename = "./elvis.config", + ok = file:write_file(Filename, <<"-">>), + ok = + try + elvis_config:from_file(Filename), + fail + catch + exit:"elvis.config unconsultable: 1, erl_parse, [\"syntax error before: \",[]]" -> + ok + end, + _ = file:delete(Filename). diff --git a/test/elvis_test_utils.erl b/test/elvis_test_utils.erl index 794a62fb..d66c6355 100644 --- a/test/elvis_test_utils.erl +++ b/test/elvis_test_utils.erl @@ -1,6 +1,13 @@ -module(elvis_test_utils). --export([config/0, config/1, find_file/2, elvis_core_apply_rule/5, excluded_funs_all/0]). +-export([ + config/0, config/1, + find_file/2, + elvis_core_apply_rule/5, + excluded_funs_all/0, + check_some_line_output/3, + matches_regex/2 +]). excluded_funs_all() -> [ @@ -44,3 +51,14 @@ elvis_core_apply_rule(Config, Module, Function, RuleConfig, Filename) -> #{items := Items} -> Items end. + +check_some_line_output(Fun, Expected, FilterFun) -> + _ = ct:capture_start(), + _ = Fun(), + _ = ct:capture_stop(), + Lines = ct:capture_get([]), + ListFun = fun(Line) -> FilterFun(Line, Expected) end, + [_ | _] = lists:filter(ListFun, Lines). + +matches_regex(Result, Regex) -> + nomatch =/= re:run(Result, Regex). diff --git a/test/examples/invalid_rules.elvis.config b/test/examples/invalid_rules.elvis.config index 958f1fa6..c16787ae 100644 --- a/test/examples/invalid_rules.elvis.config +++ b/test/examples/invalid_rules.elvis.config @@ -2,7 +2,7 @@ {elvis, [ {config, [ #{ - dirs => ["src"], + dirs => ["../../_build/default/lib/elvis_core/src"], filter => "*.erl", rules => [ diff --git a/test/examples/user_defined_rules.erl b/test/examples/user_defined_rules.erl index 33240cbf..656cd171 100644 --- a/test/examples/user_defined_rules.erl +++ b/test/examples/user_defined_rules.erl @@ -1,6 +1,6 @@ -module(user_defined_rules). --export([rule/3]). +-export([rule/2]). -rule(_Config, _Target, _) -> +rule(_Config, _Target) -> [elvis_result:new_item("this will always FAIL", [], #{line => 10, column => 2})]. diff --git a/test/style_SUITE.erl b/test/style_SUITE.erl index 62cbe9c0..bf14548e 100644 --- a/test/style_SUITE.erl +++ b/test/style_SUITE.erl @@ -2864,7 +2864,7 @@ oddities(_Config) -> ElvisConfig = [ #{ - dirs => ["../../_build/test/lib/elvis_core/test/examples"], + dirs => ["../../../../_build/test/lib/elvis_core/test/examples"], filter => "odditiesß.erl", ruleset => erl_files, rules => [{elvis_style, no_god_modules, #{limit => 0}}]