Skip to content

Commit

Permalink
Add new ruleset, gitignore, with rules `gitignore_required_patterns…
Browse files Browse the repository at this point in the history
…` and `gitignore_forbidden_patterns` (#357)

* Add new ruleset, gitignore, with rule gitignore_patterns

It'll use files identified by `dirs`+`filter`
(which should be `.gitignore`) and apply
(at the moment, the only existing) rule
`gitignore_patterns` with the default value found in
the documentation

* Act on CI results: thanks Windows!

* Refactor it into two rules, different option name and different ruleset

* Update `since`

* Make naming more consistent with reality

* Tweak whitespace

Using OTP 27 locally :-(

---------

Co-authored-by: Brujo Benavides <[email protected]>
  • Loading branch information
paulo-ferraz-oliveira and elbrujohalcon authored Sep 23, 2024
1 parent 74260eb commit 478df9c
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 1 deletion.
9 changes: 9 additions & 0 deletions RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ identified with `(since ...)` for convenience purposes.
- [Variable Naming Convention](doc_rules/elvis_style/variable_naming_convention.md)
- [Prefer Unquoted Atoms](doc_rules/elvis_text_style/prefer_unquoted_atoms.md)

## `.gitignore` rules

- [`.gitignore` required patterns](doc_rules/elvis_gitignore/required_patterns.md)
- [`.gitignore` forbidden patterns](doc_rules/elvis_gitignore/forbidden_patterns.md)

## Project rules

- [No deps master erlang.mk - *deprecated*](doc_rules/elvis_project/no_deps_master_erlang_mk.md)
Expand All @@ -84,6 +89,7 @@ The six pre-defined rulesets are:
- `elvis_config`, for elvis configuration files.
- `erl_files`, for Erlang source files (pre-defined rule set).
- `erl_files_strict`, for Erlang source files (all available rules).
- `gitignore`, for `.gitignore` files.
- `hrl_files`, for Erlang header files.
- `makefiles`, for Makefiles.
- `rebar_config`, for rebar configuration files.
Expand Down Expand Up @@ -201,6 +207,9 @@ line numbers will most surely not correspond with those in the source file.
, filter => "elvis.config"
, ruleset => elvis_config
, rules => [] }
, #{ dirs => ["."]
, filter => ".gitignore"
, ruleset => gitignore }
]}
, {verbose, true}
]}].
Expand Down
7 changes: 6 additions & 1 deletion config/test.config
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,10 @@
ruleset => rebar_config},
#{dirs => ["../../_build/test/lib/elvis_core/test/examples"],
filter => "elvis.config",
ruleset => elvis_config}]},
ruleset => elvis_config},
#{dirs =>
["../../_build/test/lib/elvis_core/test/dirs/apps/app1",
"../../_build/test/lib/elvis_core/test/dirs/apps/app2"],
filter => ".gitignore",
ruleset => gitignore}]},
{output_format, plain}]}].
16 changes: 16 additions & 0 deletions doc_rules/elvis_gitignore/forbidden_patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# `.gitignore` forbidden patterns

(since [4.0.0](https://github.com/inaka/elvis_core/releases/tag/4.0.0))

Exclude, from the project's `.gitignore` file, the patterns identified by the rule.

## Options

- `regexes :: [string()]`.
- default: `["^rebar.lock$"]`.

## Example

```erlang
{elvis_gitignore, forbidden_patterns, #{}}
```
22 changes: 22 additions & 0 deletions doc_rules/elvis_gitignore/required_patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# `.gitignore` required patterns

(since [4.0.0](https://github.com/inaka/elvis_core/releases/tag/4.0.0))

Include, in the project's `.gitignore` file, the patterns identified by the rule.

## Options

- `regexes :: [string()]`.
- default: `["^.rebar3/$",
"^_build/$",
"^_checkouts/$",
"^doc/$",
"^/erl_crash.dump$",
"^/rebar3.crashdump$",
"^test/logs/$"]`.

## Example

```erlang
{elvis_gitignore, required_patterns, #{}}
```
107 changes: 107 additions & 0 deletions src/elvis_gitignore.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
-module(elvis_gitignore).

-export([required_patterns/3, forbidden_patterns/3]).

-define(REQUIRED_PATTERN, "Your .gitignore file should contain pattern '~s'.").
-define(FORBIDDEN_PATTERN, "Your .gitignore file should not contain pattern '~s'.").

-hank([{unnecessary_function_arguments,
[{required_patterns, 3}, {forbidden_patterns, 3}]}]).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Default values
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec default(Rule :: atom()) -> DefaultRuleConfig :: term().
default(required_patterns) ->
#{regexes =>
["^.rebar3/$",
"^_build/$",
"^_checkouts/$",
"^doc/$",
"^/erl_crash.dump$",
"^/rebar3.crashdump$",
"^test/logs/$"]};
default(forbidden_patterns) ->
#{regexes => ["^rebar.lock$"]}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Rules
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec required_patterns(elvis_config:config(),
elvis_file:file(),
elvis_style:empty_rule_config()) ->
[elvis_result:item()].
required_patterns(_Config, #{path := Path}, RuleConfig) ->
Regexes = option(regexes, RuleConfig, required_patterns),
case file:read_file(Path) of
{ok, PatternsBin} ->
Patterns = elvis_utils:split_all_lines(PatternsBin),
check_patterns_in_lines(Patterns, Regexes, [], required);
{error, _} ->
[]
end.

-spec forbidden_patterns(elvis_config:config(),
elvis_file:file(),
elvis_style:empty_rule_config()) ->
[elvis_result:item()].
forbidden_patterns(_Config, #{path := Path}, RuleConfig) ->
Regexes = option(regexes, RuleConfig, forbidden_patterns),
case file:read_file(Path) of
{ok, PatternsBin} ->
Patterns = elvis_utils:split_all_lines(PatternsBin),
check_patterns_in_lines(Patterns, Regexes, [], forbidden);
{error, _} ->
[]
end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% .gitignore
%% @private
check_patterns_in_lines(_Lines, [], Results, _Mode) ->
{ok, Results};
check_patterns_in_lines(Lines, [Pattern | Rest], Results0, Mode) ->
ModeRespected =
case Mode of
required ->
lists:any(fun(Line) -> re:run(Line, Pattern) =/= nomatch end, Lines);
forbidden ->
lists:all(fun(Line) -> re:run(Line, Pattern) =:= nomatch end, Lines)
end,
Results =
case ModeRespected of
true ->
Results0;
false when Mode =:= required ->
[elvis_result:new(item, ?REQUIRED_PATTERN, [Pattern]) | Results0];
false when Mode =:= forbidden ->
[elvis_result:new(item, ?FORBIDDEN_PATTERN, [Pattern]) | Results0]
end,
check_patterns_in_lines(Lines, Rest, Results, Mode).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Internal Function Definitions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec option(OptionName, RuleConfig, Rule) -> OptionValue
when OptionName :: atom(),
RuleConfig :: elvis_config:config(),
Rule :: atom(),
OptionValue :: term().
option(OptionName, RuleConfig, Rule) ->
maybe_default_option(maps:get(OptionName, RuleConfig, undefined), OptionName, Rule).

-spec maybe_default_option(UserDefinedOptionValue, OptionName, Rule) -> OptionValue
when UserDefinedOptionValue :: undefined | term(),
OptionName :: atom(),
Rule :: atom(),
OptionValue :: term().
maybe_default_option(undefined = _UserDefinedOptionValue, OptionName, Rule) ->
maps:get(OptionName, default(Rule));
maybe_default_option(UserDefinedOptionValue, _OptionName, _Rule) ->
UserDefinedOptionValue.
3 changes: 3 additions & 0 deletions src/elvis_rulesets.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ set_rulesets(RuleSets) ->
maps:to_list(RuleSets)).

-spec rules(Group :: atom()) -> [elvis_core:rule()].
rules(gitignore) ->
lists:map(fun({Mod, Rule}) -> {Mod, Rule, apply(Mod, default, [Rule])} end,
[{elvis_gitignore, Rule} || Rule <- [required_patterns, forbidden_patterns]]);
rules(hrl_files) ->
lists:map(fun({Mod, Rule}) -> {Mod, Rule, apply(Mod, default, [Rule])} end,
[{elvis_text_style, Rule} || Rule <- [line_length, no_tabs, no_trailing_whitespace]]
Expand Down
10 changes: 10 additions & 0 deletions test/dirs/apps/app1/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.rebar3/
_build/

_checkouts/
extra
doc/
/erl_crash.dump
/rebar3.crashdump
test/logs/
rebar0.lock
9 changes: 9 additions & 0 deletions test/dirs/apps/app2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.rebar3/
_build/
_checkouts/
#doc/

/erl_crash.dump
/rebar3.crashdump
test/logs/
rebar.lock
63 changes: 63 additions & 0 deletions test/gitignore_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
-module(gitignore_SUITE).

-behaviour(ct_suite).

-export([all/0, init_per_suite/1, end_per_suite/1]).
-export([verify_required_patterns/1, verify_forbidden_patterns/1]).

-define(EXCLUDED_FUNS, [module_info, all, test, init_per_suite, end_per_suite]).

-type config() :: [{atom(), term()}].

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Common test
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec all() -> [atom()].
all() ->
Exports = ?MODULE:module_info(exports),
[F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)].

-spec init_per_suite(config()) -> config().
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(elvis_core),
Config.

-spec end_per_suite(config()) -> config().
end_per_suite(Config) ->
ok = application:stop(elvis_core),
Config.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Test Cases
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec verify_required_patterns(config()) -> any().
verify_required_patterns(_Config) ->
GitIgnoreConfig = elvis_test_utils:config(gitignore),
[SrcDirPass, SrcDirFail] = elvis_config:dirs(GitIgnoreConfig),
NoRuleConfig = #{},

PathPass = ".gitignore",
{ok, FilePass} = elvis_test_utils:find_file([SrcDirPass], PathPass),
{ok, []} = elvis_gitignore:required_patterns(GitIgnoreConfig, FilePass, NoRuleConfig),

PathFail = ".gitignore",
{ok, FileFail} = elvis_test_utils:find_file([SrcDirFail], PathFail),
{ok, [Res]} = elvis_gitignore:required_patterns(GitIgnoreConfig, FileFail, NoRuleConfig),
#{info := ["^doc/$"]} = Res.

-spec verify_forbidden_patterns(config()) -> any().
verify_forbidden_patterns(_Config) ->
GitIgnoreConfig = elvis_test_utils:config(gitignore),
[SrcDirPass, SrcDirFail] = elvis_config:dirs(GitIgnoreConfig),
NoRuleConfig = #{},

PathPass = ".gitignore",
{ok, FilePass} = elvis_test_utils:find_file([SrcDirPass], PathPass),
{ok, []} = elvis_gitignore:forbidden_patterns(GitIgnoreConfig, FilePass, NoRuleConfig),

PathFail = ".gitignore",
{ok, FileFail} = elvis_test_utils:find_file([SrcDirFail], PathFail),
{ok, [Res]} = elvis_gitignore:forbidden_patterns(GitIgnoreConfig, FileFail, NoRuleConfig),
#{info := ["^rebar.lock$"]} = Res.

0 comments on commit 478df9c

Please sign in to comment.