diff --git a/RULES.md b/RULES.md index b37c8364..19df9cdd 100644 --- a/RULES.md +++ b/RULES.md @@ -76,6 +76,7 @@ Most, if not all, of the rules will present (opinionated) documentation sections - [Param Pattern-Matching](doc_rules/elvis_style/param_pattern_matching.md) - [Prefer Unquoted Atoms](doc_rules/elvis_text_style/prefer_unquoted_atoms.md) - [Private Data Types](doc_rules/elvis_style/private_data_types.md) +- [Simplify Anonymous Functions](doc_rules/elvis_style/simplify_anonymous_functions.md) - [State Record And Type](doc_rules/elvis_style/state_record_and_type.md) - [Variable Casing](doc_rules/elvis_style/variable_casing.md) - [Variable Naming Convention](doc_rules/elvis_style/variable_naming_convention.md) diff --git a/doc_rules/elvis_style/simplify_anonymous_functions.md b/doc_rules/elvis_style/simplify_anonymous_functions.md new file mode 100644 index 00000000..3c1648dc --- /dev/null +++ b/doc_rules/elvis_style/simplify_anonymous_functions.md @@ -0,0 +1,32 @@ +# Simplify Anonymous Functions [![](https://img.shields.io/badge/since-4.2.0-blue)](https://github.com/inaka/elvis_core/releases/tag/4.2.0) ![](https://img.shields.io/badge/BEAM-yes-orange) + +Anonymous functions that simply call a named function (with the same arguments in the same order) +should be avoided; they can be more concisely expressed using the function reference syntax. + +## Avoid + +```erlang +fun(Pattern) -> is_match_node(Pattern) end +``` + +## Prefer + +```erlang +fun is_match_node/1 +``` + +## Rationale + +The `fun F/A` syntax is clearer and more concise when the anonymous function does nothing more than +call another function. It reduces noise and makes the code easier to read and maintain by removing +unnecessary bindings and boilerplate. + +## Options + +- None. + +## Example configuration + +```erlang +{elvis_style, simplify_anonymous_functions, #{}} +``` diff --git a/src/elvis_ruleset.erl b/src/elvis_ruleset.erl index 9c408caf..5f00777c 100644 --- a/src/elvis_ruleset.erl +++ b/src/elvis_ruleset.erl @@ -136,7 +136,8 @@ elvis_style_rules() -> elvis_rule:new(elvis_style, private_data_types), elvis_rule:new(elvis_style, no_used_ignored_variables), elvis_rule:new(elvis_style, variable_naming_convention), - elvis_rule:new(elvis_style, guard_operators) + elvis_rule:new(elvis_style, guard_operators), + elvis_rule:new(elvis_style, simplify_anonymous_functions) ]. erl_files_test_rules() -> diff --git a/src/elvis_style.erl b/src/elvis_style.erl index bbbc2e3e..dbc64eff 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -57,7 +57,8 @@ no_boolean_in_comparison/2, no_operation_on_same_value/2, no_receive_without_timeout/2, - guard_operators/2 + guard_operators/2, + simplify_anonymous_functions/2 ]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -1544,7 +1545,7 @@ is_list_node(#{type := cons}) -> is_list_node(#{type := nil}) -> true; is_list_node(#{type := match, content := Content}) -> - lists:any(fun(Elem) -> is_list_node(Elem) end, Content); + lists:any(fun is_list_node/1, Content); is_list_node(_) -> false. @@ -1666,6 +1667,10 @@ same_value_on_both_sides(Node) -> false end. +nodes_same_except_location([_ | _], []) -> + false; +nodes_same_except_location([], [_ | _]) -> + false; nodes_same_except_location([], []) -> true; nodes_same_except_location([LeftNode | LeftNodes], [RightNode | RigthNodes]) -> @@ -2175,6 +2180,51 @@ always_shortcircuit(Rule, ElvisConfig) -> || OpNode <- OpNodes ]. +simplify_anonymous_functions(Rule, ElvisConfig) -> + {nodes, FunNodes} = elvis_code:find(#{ + of_types => ['fun'], + inside => elvis_code:root(Rule, ElvisConfig), + filtered_by => fun is_simple_anonymous_function/1, + traverse => all + }), + + [ + elvis_result:new_item( + "an unnecessary anonymous function wrapper was found; prefer 'fun M:F/A'", + [], + #{node => Fun} + ) + || Fun <- FunNodes + ]. + +%% @doc Has this anonymous function just one clause that is a call to a +%% regular function with the same args? i.e., something like +%% fun() -> x() end ; or +%% fun(A) -> some:funct(A) end ; or +%% fun(A, B, C) -> x:y(A, B, C) end +%% Note that we assume that FunNode is, in fact, an anonymous function node. +is_simple_anonymous_function(FunNode) -> + case + elvis_code:find(#{ + of_types => [clause], + inside => FunNode + }) + of + {nodes, [Clause]} -> + case ktn_code:content(Clause) of + [Expression] -> + ktn_code:node_attr(guards, Clause) =:= [] andalso + ktn_code:type(Expression) =:= call andalso + nodes_same_except_location( + ktn_code:node_attr(pattern, Clause), ktn_code:content(Expression) + ); + _ -> + false + end; + _ -> + false + end. + export_used_types(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), diff --git a/test/examples/fail_simplify_anonymous_functions.erl b/test/examples/fail_simplify_anonymous_functions.erl new file mode 100644 index 00000000..39043b4b --- /dev/null +++ b/test/examples/fail_simplify_anonymous_functions.erl @@ -0,0 +1,14 @@ +-module(fail_simplify_anonymous_functions). + +-export([functions/0]). + +functions() -> + #{ + no_args => fun() -> rand:uniform() end, + one_arg => fun(X) -> erlang:display(X) end, + two_args => fun(A, B) -> io:format(A, B) end, + local => fun() -> local() end, + auto_import => fun(A) -> atom_to_list(A) end + }. + +local() -> local. diff --git a/test/examples/pass_simplify_anonymous_functions.erl b/test/examples/pass_simplify_anonymous_functions.erl new file mode 100644 index 00000000..ed29dbd7 --- /dev/null +++ b/test/examples/pass_simplify_anonymous_functions.erl @@ -0,0 +1,23 @@ +-module(pass_simplify_anonymous_functions). + +-export([functions/0]). + +functions() -> + #{ + no_args => fun rand:uniform/0, + one_arg => fun erlang:display/1, + two_args => fun io:format/2, + local => fun local/0, + auto_import => fun atom_to_list/1, + flip_param_order => fun(A, B) -> io:format(B, A) end, + more_than_a_call => fun() -> second:call(first:call()) end, + op => fun(X) -> not X end, + bi_op => fun(A, B) -> A + B end, + id => fun(X) -> X end, + with_guards => fun(X) when is_binary(X) -> binary_to_list(X) end, + with_matching => fun(<>) -> erlang:binary_to_atom(X) end, + multiple_clauses => fun (x) -> atom_to_list(x); (X) -> binary_to_list(X) end, + ignored_arg => fun(X, _) -> erlang:display(X) end + }. + +local() -> local. diff --git a/test/style_SUITE.erl b/test/style_SUITE.erl index 66a2471e..dd6eef05 100644 --- a/test/style_SUITE.erl +++ b/test/style_SUITE.erl @@ -64,7 +64,8 @@ verify_ms_transform_included/1, verify_no_boolean_in_comparison/1, verify_no_operation_on_same_value/1, - verify_no_receive_without_timeout/1 + verify_no_receive_without_timeout/1, + verify_simplify_anonymous_functions/1 ]). %% -elvis attribute -export([ @@ -166,7 +167,8 @@ groups() -> verify_no_match_in_condition, verify_behaviour_spelling, verify_param_pattern_matching, - verify_private_data_types + verify_private_data_types, + verify_simplify_anonymous_functions ]} ]. @@ -1235,6 +1237,34 @@ verify_always_shortcircuit(Config) -> Config, elvis_style, always_shortcircuit, #{}, PathPass ). +verify_simplify_anonymous_functions(Config) -> + Group = proplists:get_value(group, Config, erl_files), + Ext = proplists:get_value(test_file_ext, Config, "erl"), + + PathFail = "fail_simplify_anonymous_functions." ++ Ext, + Warnings = + elvis_test_utils:elvis_core_apply_rule( + Config, elvis_style, simplify_anonymous_functions, #{}, PathFail + ), + + case Group of + beam_files -> + [_, _, _, _, _] = Warnings; + _ -> + [ + #{line_num := 7}, + #{line_num := 8}, + #{line_num := 9}, + #{line_num := 10}, + #{line_num := 11} + ] = Warnings + end, + + PathPass = "pass_simplify_anonymous_functions." ++ Ext, + [] = elvis_test_utils:elvis_core_apply_rule( + Config, elvis_style, simplify_anonymous_functions, #{}, PathPass + ). + verify_no_spec_with_records(Config) -> Ext = proplists:get_value(test_file_ext, Config, "erl"),