Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #284: New Rule: Param Pattern Matching #305

Merged
merged 6 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ identified with `(since ...)` for convenience purposes.
- [No Types](doc_rules/elvis_style/no_types.md)
- [Numeric Format](doc_rules/elvis_style/numeric_format.md)
- [Operator Spaces](doc_rules/elvis_style/operator_spaces.md)
- [Param Pattern Matching](doc_rules/elvis_style/param_pattern_matching.md)
- [State Record and Type](doc_rules/elvis_style/state_record_and_type.md)
- [Used Ignored Variable](doc_rules/elvis_style/used_ignored_variable.md)
- [Variable Naming Convention](doc_rules/elvis_style/variable_naming_convention.md)
Expand Down
3 changes: 2 additions & 1 deletion config/test.config
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
[{elvis_text_style, line_length, #{limit => 80, skip_comments => false}},
{elvis_style, nesting_level, #{level => 3}},
{elvis_style, invalid_dynamic_call, #{ignore => [elvis]}},
{elvis_style, no_macros}],
{elvis_style, no_macros},
{elvis_style, param_pattern_matching, #{side => right}}],
ruleset => erl_files},
#{dirs => ["../../_build/test/lib/elvis_core/test/examples"],
filter => "*.hrl",
Expand Down
21 changes: 21 additions & 0 deletions doc_rules/elvis_style/param_pattern_matching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Param Pattern-Matching

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

When capturing a parameter using pattern matching you can either put the parameter
name on the left (`Param = #{pattern := ToMatch}`) or right (`#{pattern := ToMatch} = Param`) side
of the pattern that you use in the function clause.
This rule will make sure you are consistent through your code and use always the same style.
elbrujohalcon marked this conversation as resolved.
Show resolved Hide resolved

> Works on `.beam` file? Yes!

## Options

- `side :: left | right`.
- default: `right`.

## Example

```erlang
{elvis_style, param_pattern_matching, #{side => left}}
```
12 changes: 6 additions & 6 deletions src/elvis_code.erl
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ content_zipper(Root) ->
-spec all_zipper(ktn_code:tree_node()) -> zipper:zipper(_).
all_zipper(Root) ->
IsBranch =
fun(Node = #{}) -> ktn_code:content(Node) =/= [] orelse maps:is_key(node_attrs, Node) end,
fun(#{} = Node) -> ktn_code:content(Node) =/= [] orelse maps:is_key(node_attrs, Node) end,
elbrujohalcon marked this conversation as resolved.
Show resolved Hide resolved
Children =
fun (#{content := Content, node_attrs := NodeAttrs}) ->
Content
Expand Down Expand Up @@ -124,7 +124,7 @@ find_by_location(Root, Location) ->
{ok, Node}
end.

is_at_location(Node = #{attrs := #{location := {Line, NodeCol}}}, {Line, Column}) ->
is_at_location(#{attrs := #{location := {Line, NodeCol}}} = Node, {Line, Column}) ->
Text = ktn_code:attr(text, Node),
Length = length(Text),
NodeCol =< Column andalso Column < NodeCol + Length;
Expand Down Expand Up @@ -168,7 +168,7 @@ print_node(Node) ->
print_node(Node, 0).

-spec print_node(ktn_code:tree_node(), integer()) -> ok.
print_node(Node = #{type := Type}, CurrentLevel) ->
print_node(#{type := Type} = Node, CurrentLevel) ->
Type = ktn_code:type(Node),
Indentation = lists:duplicate(CurrentLevel * 4, $\s),
Content = ktn_code:content(Node),
Expand Down Expand Up @@ -230,19 +230,19 @@ level_increment(#{type := Type}) ->
%% @doc Returns an anonymous Fun to be flatmapped over node content, as
%% appropriate for the exported function whose name is the argument given.
make_extractor_fun(exported_functions) ->
fun (Node = #{type := export}) ->
fun (#{type := export} = Node) ->
ktn_code:attr(value, Node);
(_) ->
[]
end;
make_extractor_fun(exported_types) ->
fun (Node = #{type := export_type}) ->
fun (#{type := export_type} = Node) ->
ktn_code:attr(value, Node);
(_) ->
[]
end;
make_extractor_fun(function_names) ->
fun (Node = #{type := function}) ->
fun (#{type := function} = Node) ->
[ktn_code:attr(name, Node)];
(_) ->
[]
Expand Down
16 changes: 8 additions & 8 deletions src/elvis_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ normalize(Config) when is_list(Config) ->
lists:map(fun do_normalize/1, Config).

%% @private
do_normalize(Config = #{src_dirs := Dirs}) ->
do_normalize(#{src_dirs := Dirs} = Config) ->
%% NOTE: Provided for backwards compatibility.
%% Rename 'src_dirs' key to 'dirs'.
Config1 = maps:remove(src_dirs, Config),
Expand All @@ -117,31 +117,31 @@ do_normalize(Config) ->
-spec dirs(Config :: configs() | config()) -> [string()].
dirs(Config) when is_list(Config) ->
lists:flatmap(fun dirs/1, Config);
dirs(_RuleGroup = #{dirs := Dirs}) ->
dirs(#{dirs := Dirs}) ->
Dirs;
dirs(#{}) ->
[].

-spec ignore(configs() | config()) -> [string()].
ignore(Config) when is_list(Config) ->
lists:flatmap(fun ignore/1, Config);
ignore(_RuleGroup = #{ignore := Ignore}) ->
ignore(#{ignore := Ignore}) ->
lists:map(fun ignore_to_regexp/1, Ignore);
ignore(#{}) ->
[].

-spec filter(configs() | config()) -> [string()].
filter(Config) when is_list(Config) ->
lists:flatmap(fun filter/1, Config);
filter(_RuleGroup = #{filter := Filter}) ->
filter(#{filter := Filter}) ->
Filter;
filter(#{}) ->
?DEFAULT_FILTER.

-spec files(RuleGroup :: configs() | config()) -> [elvis_file:file()].
files(RuleGroup) when is_list(RuleGroup) ->
lists:map(fun files/1, RuleGroup);
files(_RuleGroup = #{files := Files}) ->
files(#{files := Files}) ->
Files;
files(#{}) ->
[].
Expand Down Expand Up @@ -194,9 +194,9 @@ resolve_files(RuleGroup, Files) ->
%% end 'filter' key, or if not specified uses '*.erl'.
%% @end
-spec resolve_files(config()) -> config().
resolve_files(RuleGroup = #{files := _Files}) ->
resolve_files(#{files := _Files} = RuleGroup) ->
RuleGroup;
resolve_files(RuleGroup = #{dirs := Dirs}) ->
resolve_files(#{dirs := Dirs} = RuleGroup) ->
Filter = filter(RuleGroup),
Files = elvis_file:find_files(Dirs, Filter),
resolve_files(RuleGroup, Files).
Expand All @@ -209,7 +209,7 @@ resolve_files(RuleGroup = #{dirs := Dirs}) ->
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, RuleGroup = #{files := Files}) ->
apply_to_files(Fun, #{files := Files} = RuleGroup) ->
NewFiles = lists:map(Fun, Files),
RuleGroup#{files => NewFiles}.

Expand Down
21 changes: 10 additions & 11 deletions src/elvis_file.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

%% @doc Returns a tuple with the contents of the file and the file itself.
-spec src(file()) -> {binary(), file()} | {error, enoent}.
src(File = #{content := Content, encoding := _}) ->
src(#{content := Content, encoding := _} = File) ->
{Content, File};
src(File = #{content := Content}) ->
src(#{content := Content} = File) ->
{Content, File#{encoding => find_encoding(Content)}};
src(File = #{path := Path}) ->
src(#{path := Path} = File) ->
case file:read_file(Path) of
{ok, Content} ->
Encoding = find_encoding(Content),
Expand Down Expand Up @@ -49,27 +49,27 @@ parse_tree(Config, Target) ->
file(),
elvis_core:rule_config()) ->
{ktn_code:tree_node(), file()}.
parse_tree(_Config, File = #{parse_tree := ParseTree0}, RuleConfig) ->
parse_tree(_Config, #{parse_tree := ParseTree0} = File, RuleConfig) ->
Ignore = maps:get(ignore, RuleConfig, []),
Mod = module(File),
{filter_tree_for(ParseTree0, Mod, Ignore), File};
parse_tree(Config, File = #{path := Path, content := Content}, RuleConfig) ->
parse_tree(Config, #{path := Path, content := Content} = File, RuleConfig) ->
Ext = filename:extension(Path),
ExtStr = elvis_utils:to_str(Ext),
Mod = module(File),
Ignore = maps:get(ignore, RuleConfig, []),
ParseTree = resolve_parse_tree(ExtStr, Content, Mod, Ignore),
File1 = maybe_add_abstract_parse_tree(Config, File, Mod, Ignore),
parse_tree(Config, File1#{parse_tree => ParseTree}, RuleConfig);
parse_tree(Config, File0 = #{path := _Path}, RuleConfig) ->
parse_tree(Config, #{path := _Path} = File0, RuleConfig) ->
{_, File} = src(File0),
parse_tree(Config, File, RuleConfig);
parse_tree(_Config, File, _RuleConfig) ->
throw({invalid_file, File}).

%% @doc Loads and adds all related file data.
-spec load_file_data(elvis_config:configs() | elvis_config:config(), file()) -> file().
load_file_data(Config, File0 = #{path := _Path}) ->
load_file_data(Config, #{path := _Path} = File0) ->
{_, File1} = src(File0),
{_, File2} = parse_tree(Config, File1),
File2.
Expand All @@ -87,14 +87,13 @@ find_files(Dirs, Pattern) ->
<- lists:usort(
lists:flatmap(Fun, Dirs))].

dir_to(Filter, _Dir = ".") ->
dir_to(Filter, ".") ->
Filter;
dir_to(Filter, Dir) ->
filename:join(Dir, Filter).

file_in(ExpandedFilter, Files) ->
lists:filter(fun(_File = #{path := Path}) -> lists:member(Path, ExpandedFilter) end,
Files).
lists:filter(fun(#{path := Path}) -> lists:member(Path, ExpandedFilter) end, Files).

%% @doc Filter files based on the glob provided.
-spec filter_files([file()], [string()], string(), [string()]) -> [file()].
Expand Down Expand Up @@ -162,7 +161,7 @@ find_encoding(Content) ->
Ignore :: [elvis_style:ignorable()],
Res :: file().
maybe_add_abstract_parse_tree(#{ruleset := beam_files},
File = #{path := Path},
#{path := Path} = File,
Mod,
Ignore) ->
AbstractParseTree = get_abstract_parse_tree(Path, Mod, Ignore),
Expand Down
6 changes: 4 additions & 2 deletions src/elvis_rulesets.erl
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ rules(erl_files) ->
{elvis_style, behaviour_spelling},
{elvis_style, export_used_types},
{elvis_style, max_function_arity},
{elvis_style, max_anonymous_function_arity}]);
{elvis_style, max_anonymous_function_arity},
{elvis_style, param_pattern_matching}]);
rules(beam_files) ->
lists:map(fun(Rule) -> {elvis_style, Rule, elvis_style:default(Rule)} end,
[nesting_level,
Expand All @@ -107,7 +108,8 @@ rules(beam_files) ->
behaviour_spelling,
export_used_types,
max_function_arity,
max_anonymous_function_arity]);
max_anonymous_function_arity,
param_pattern_matching]);
rules(rebar_config) ->
lists:map(fun(Rule) -> {elvis_project, Rule, elvis_project:default(Rule)} end,
[no_branch_deps, protocol_for_deps]);
Expand Down
74 changes: 69 additions & 5 deletions src/elvis_style.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
atom_naming_convention/3, no_throw/3, no_dollar_space/3, no_author/3, no_import/3,
no_catch_expressions/3, no_single_clause_case/3, numeric_format/3, behaviour_spelling/3,
always_shortcircuit/3, consistent_generic_type/3, export_used_types/3,
no_match_in_condition/3, option/3]).
no_match_in_condition/3, param_pattern_matching/3, option/3]).

-export_type([empty_rule_config/0]).
-export_type([ignorable/0]).
Expand All @@ -27,7 +27,8 @@
no_common_caveats_call_config/0, atom_naming_convention_config/0, no_author_config/0,
no_import_config/0, no_catch_expressions_config/0, numeric_format_config/0,
no_single_clause_case_config/0, consistent_variable_casing_config/0,
no_match_in_condition_config/0]).
no_match_in_condition_config/0, behaviour_spelling_config/0,
param_pattern_matching_config/0]).

-define(INVALID_MACRO_NAME_REGEX_MSG,
"The macro named ~p on line ~p does not respect the format "
Expand Down Expand Up @@ -121,9 +122,12 @@
-define(NUMERIC_FORMAT_MSG,
"Number ~p on line ~p does not respect the format "
"defined by the regular expression '~p'.").
-define(BEHAVIOUR_SPELLING,
-define(BEHAVIOUR_SPELLING_MSG,
"The behavior/behaviour in line ~p is misspelt, please use the "
"~p spelling.").
-define(PARAM_PATTERN_MATCHING_MSG,
"Variable ~ts, used to match a parameter in line ~p is placed on "
elbrujohalcon marked this conversation as resolved.
Show resolved Hide resolved
"the wrong side of the match. It was expected on the ~p side.").
-define(ALWAYS_SHORTCIRCUIT_MSG,
"Non-shortcircuiting operator (~p) found in line ~p. "
"It's recommended to use ~p, instead.").
Expand Down Expand Up @@ -196,6 +200,8 @@ default(numeric_format) ->
float_regex => same};
default(behaviour_spelling) ->
#{spelling => behaviour};
default(param_pattern_matching) ->
#{side => right};
default(consistent_generic_type) ->
#{preferred_type => term};
default(RuleWithEmptyDefault)
Expand Down Expand Up @@ -1125,7 +1131,12 @@ numeric_format(Config, Target, RuleConfig) ->
IntNodes,
check_numeric_format(FloatRegex, FloatNodes, [])).

-spec behaviour_spelling(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
-type behaviour_spelling_config() ::
#{ignore => [ignorable()], spelling => behaviour | behavior}.

-spec behaviour_spelling(elvis_config:config(),
elvis_file:file(),
behaviour_spelling_config()) ->
elbrujohalcon marked this conversation as resolved.
Show resolved Hide resolved
[elvis_result:item()].
behaviour_spelling(Config, Target, RuleConfig) ->
Spelling = option(spelling, RuleConfig, behaviour_spelling),
Expand All @@ -1143,11 +1154,64 @@ behaviour_spelling(Config, Target, RuleConfig) ->
fun(Node) ->
{Line, _} = ktn_code:attr(location, Node),
Info = [Line, Spelling],
elvis_result:new(item, ?BEHAVIOUR_SPELLING, Info, Line)
elvis_result:new(item, ?BEHAVIOUR_SPELLING_MSG, Info, Line)
end,
lists:map(ResultFun, InconsistentBehaviorNodes)
end.

-type param_pattern_matching_config() :: #{ignore => [ignorable()], side => left | right}.

-spec param_pattern_matching(elvis_config:config(),
elvis_file:file(),
param_pattern_matching_config()) ->
[elvis_result:item()].
param_pattern_matching(Config, Target, RuleConfig) ->
Side = option(side, RuleConfig, param_pattern_matching),
Root = get_root(Config, Target, RuleConfig),

FunctionClausePatterns =
lists:flatmap(fun(Clause) -> ktn_code:node_attr(pattern, Clause) end,
elvis_code:find(fun is_function_clause/1,
Root,
#{mode => zipper, traverse => all})),

MatchesInFunctionClauses =
lists:filter(fun(Pattern) -> ktn_code:type(Pattern) == match end, FunctionClausePatterns),

lists:filtermap(fun(Match) ->
case lists:map(fun ktn_code:type/1, ktn_code:content(Match)) of
[var, var] ->
false;
[var, _] when Side == right ->
{Line, _} = ktn_code:attr(location, Match),
[Var, _] = ktn_code:content(Match),
VarName = ktn_code:attr(name, Var),
Info = [VarName, Line, Side],
{true,
elvis_result:new(item, ?PARAM_PATTERN_MATCHING_MSG, Info, Line)};
[_, var] when Side == left ->
{Line, _} = ktn_code:attr(location, Match),
[_, Var] = ktn_code:content(Match),
VarName = ktn_code:attr(name, Var),
Info = [VarName, Line, Side],
{true,
elvis_result:new(item, ?PARAM_PATTERN_MATCHING_MSG, Info, Line)};
_ ->
false
end
end,
MatchesInFunctionClauses).

is_function_clause(Zipper) ->
clause
== ktn_code:type(
zipper:node(Zipper))
andalso lists:member(
ktn_code:type(
zipper:node(
zipper:up(Zipper))),
[function, 'fun']).
elbrujohalcon marked this conversation as resolved.
Show resolved Hide resolved

-spec consistent_generic_type(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
Expand Down
2 changes: 1 addition & 1 deletion src/elvis_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ context(List, CtxCount) ->

context([], _Past, _CtxCount, Results) ->
lists:reverse(Results);
context([Current | Future], Past, CtxCount = {PrevCount, NextCount}, Results) ->
context([Current | Future], Past, {PrevCount, NextCount} = CtxCount, Results) ->
Prev = lists:sublist(Past, PrevCount),
Next = lists:sublist(Future, NextCount),
Item = {Current, lists:reverse(Prev), Next},
Expand Down
Loading