Skip to content

Commit 5eff7b9

Browse files
Fix #426: New rule: no_receive_without_timeout (#472)
1 parent ccbb740 commit 5eff7b9

File tree

8 files changed

+163
-5
lines changed

8 files changed

+163
-5
lines changed

RULES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Most, if not all, of the rules will present (opinionated) documentation sections
5757
- [No Nested `try...catch` Blocks](doc_rules/elvis_style/no_nested_try_catch.md)
5858
- [No Operator With Same Values](doc_rules/elvis_style/no_operation_on_same_value.md)
5959
- [No Redundant Blank Lines](doc_rules/elvis_text_style/no_redundant_blank_lines.md)
60+
- [No `receive` Without Timeout](doc_rules/elvis_style/no_receive_without_timeout.md)
6061
- [No Single-Clause Case Statements](doc_rules/elvis_style/no_single_clause_case.md)
6162
- [No Single-Match Maybe Statements](doc_rules/elvis_style/no_single_match_maybe.md)
6263
- [No Space After `#`](doc_rules/elvis_style/no_space_after_pound.md)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# No `receive` Without Timeout [![](https://img.shields.io/badge/since-4.1.0-blue)](https://github.com/inaka/elvis_core/releases/tag/4.1.0) ![](https://img.shields.io/badge/BEAM-yes-orange)
2+
3+
All `receive` expressions should be accompanied by an `after` expressions.
4+
5+
## Avoid
6+
7+
```erlang
8+
receive
9+
something ->
10+
do:something()
11+
end
12+
```
13+
14+
## Prefer
15+
16+
```erlang
17+
receive
18+
something ->
19+
do:something()
20+
after
21+
60_000 ->
22+
exit(nothing_received)
23+
end
24+
```
25+
26+
## Rationale
27+
28+
A `receive` block without a timeout will wait indefinitely if no matching message arrives; by making
29+
your timeout explicit you:
30+
31+
- avoid hanging processes.
32+
- improve testability (deterministic behaviour under test failure conditions).
33+
- ease debug and recovery.
34+
- can implement retry and self-healing.
35+
- potentially avoid denial-of-service scenarios where waits are exploited.
36+
37+
## Options
38+
39+
- None.
40+
41+
## Example configuration
42+
43+
```erlang
44+
{elvis_style, no_receive_without_timeout, #{}}
45+
```

src/elvis_ruleset.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ elvis_style_rules() ->
134134
{elvis_style, no_match_in_condition},
135135
{elvis_style, no_nested_try_catch},
136136
{elvis_style, no_operation_on_same_value},
137+
{elvis_style, no_receive_without_timeout},
137138
{elvis_style, no_single_clause_case},
138139
{elvis_style, no_single_match_maybe},
139140
{elvis_style, no_space_after_pound},

src/elvis_style.erl

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
no_init_lists/3,
5656
ms_transform_included/3,
5757
no_boolean_in_comparison/3,
58-
no_operation_on_same_value/3
58+
no_operation_on_same_value/3,
59+
no_receive_without_timeout/3
5960
]).
6061

6162
-export_type([empty_rule_config/0]).
@@ -272,6 +273,9 @@
272273
"Operation ~p on line ~p is has the same value on both sides."
273274
" Since the result is known, it is redundant."
274275
).
276+
-define(NO_RECEIVE_WITHOUT_TIMEOUT,
277+
"Receive block on line ~p doesn't have an after clause"
278+
).
275279

276280
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
277281
%% Default values
@@ -494,7 +498,8 @@ default(RuleWithEmptyDefault) when
494498
RuleWithEmptyDefault =:= export_used_types;
495499
RuleWithEmptyDefault =:= consistent_variable_casing;
496500
RuleWithEmptyDefault =:= ms_transform_included;
497-
RuleWithEmptyDefault =:= no_boolean_in_comparison
501+
RuleWithEmptyDefault =:= no_boolean_in_comparison;
502+
RuleWithEmptyDefault =:= no_receive_without_timeout
498503
->
499504
#{}.
500505

@@ -1636,6 +1641,32 @@ no_boolean_in_comparison(Config, Target, RuleConfig) ->
16361641

16371642
lists:map(ResultFun, ComparisonsWithBoolean).
16381643

1644+
-spec no_receive_without_timeout(
1645+
elvis_config:config(),
1646+
elvis_file:file(),
1647+
empty_rule_config()
1648+
) ->
1649+
[elvis_result:item()].
1650+
no_receive_without_timeout(Config, Target, RuleConfig) ->
1651+
Root = get_root(Config, Target, RuleConfig),
1652+
1653+
Receives = elvis_code:find_by_types(['receive'], Root),
1654+
1655+
ReceivesWithoutTimeout = lists:filter(fun is_receive_without_timeout/1, Receives),
1656+
1657+
ResultFun =
1658+
fun(Node) ->
1659+
{Line, _} = ktn_code:attr(location, Node),
1660+
Info = [Line],
1661+
Msg = ?NO_RECEIVE_WITHOUT_TIMEOUT,
1662+
elvis_result:new(item, Msg, Info, Line)
1663+
end,
1664+
1665+
lists:map(ResultFun, ReceivesWithoutTimeout).
1666+
1667+
is_receive_without_timeout(Receive) ->
1668+
[] == elvis_code:find_by_types([receive_after], Receive).
1669+
16391670
-type no_operation_on_same_value_config() :: #{operations := [atom()]}.
16401671

16411672
-spec no_operation_on_same_value(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-module(fail_no_receive_without_timeout).
2+
3+
-export([no_after/0, try_after/0, nested/0]).
4+
5+
no_after() ->
6+
receive X -> X end.
7+
8+
try_after() ->
9+
try
10+
receive X -> X end
11+
catch
12+
timeout -> timeout
13+
after
14+
this:is_not(the, 'after', "that you are looking for")
15+
end.
16+
17+
nested() ->
18+
receive
19+
good ->
20+
receive bad -> "This one doesn't have an after clause" end
21+
after 1_000 ->
22+
"This one is fine"
23+
end.

test/examples/pass_nesting_level_elvis_attr.erl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ exceed_with_receive() ->
134134
false -> false
135135
end;
136136
4 -> ok
137+
after 1000 -> ok
137138
end;
138139
3 -> 3
139140
end.
@@ -181,6 +182,7 @@ exceed_with_list_compr() ->
181182
end
182183
|| X <- [1, 2, 3]];
183184
4 -> ok
185+
after 1000 -> ok
184186
end;
185187
3 -> 3
186188
end.
@@ -193,6 +195,7 @@ exceed_with_fun() ->
193195
2 -> ok;
194196
3 -> fun() -> ok end;
195197
4 -> ok
198+
after 1000 -> ok
196199
end;
197200
3 -> 3
198201
end.
@@ -205,6 +208,7 @@ dont_exceed_with_fun() ->
205208
2 -> ok;
206209
3 -> fun erlang:display/1;
207210
4 -> ok
211+
after 1000 -> ok
208212
end;
209213
3 -> 3
210214
end.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
-module(pass_no_receive_without_timeout).
2+
3+
-define(TIMEOUT, 1_000).
4+
5+
-export([literal/0, macro/0, variable/0, function_call/0, nested/0]).
6+
7+
literal() ->
8+
receive X -> X
9+
after 1_000 -> ok
10+
end.
11+
12+
macro() ->
13+
receive X -> X
14+
after ?TIMEOUT -> ok
15+
end.
16+
17+
variable() ->
18+
Timeout = 1_000,
19+
receive X -> X
20+
after Timeout -> ok
21+
end.
22+
23+
function_call() ->
24+
receive X -> X
25+
after default:timeout() -> ok
26+
end.
27+
28+
nested() ->
29+
try
30+
receive X -> X
31+
after 10_000 -> throw(timeout)
32+
end
33+
catch
34+
timeout -> timeout
35+
end.

test/style_SUITE.erl

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
verify_ms_transform_included/1,
7272
verify_redundant_blank_lines/1,
7373
verify_no_boolean_in_comparison/1,
74-
verify_no_operation_on_same_value/1
74+
verify_no_operation_on_same_value/1,
75+
verify_no_receive_without_timeout/1
7576
]).
7677
%% -elvis attribute
7778
-export([
@@ -1987,6 +1988,21 @@ verify_no_boolean_in_comparison(Config) ->
19871988
] =
19881989
elvis_core_apply_rule(Config, elvis_style, no_boolean_in_comparison, #{}, FailPath).
19891990

1991+
-spec verify_no_receive_without_timeout(config()) -> any().
1992+
verify_no_receive_without_timeout(Config) ->
1993+
Ext = proplists:get_value(test_file_ext, Config, "erl"),
1994+
1995+
PassPath = "pass_no_receive_without_timeout." ++ Ext,
1996+
[] = elvis_core_apply_rule(Config, elvis_style, no_receive_without_timeout, #{}, PassPath),
1997+
1998+
FailPath = "fail_no_receive_without_timeout." ++ Ext,
1999+
[
2000+
#{line_num := 6},
2001+
#{line_num := 10},
2002+
#{line_num := 20}
2003+
] =
2004+
elvis_core_apply_rule(Config, elvis_style, no_receive_without_timeout, #{}, FailPath).
2005+
19902006
-spec verify_atom_naming_convention(config()) -> any().
19912007
verify_atom_naming_convention(Config) ->
19922008
Group = proplists:get_value(group, Config, erl_files),
@@ -2736,11 +2752,13 @@ verify_elvis_attr(Config, FilenameNoExt) ->
27362752
SrcDirs = elvis_config:dirs(ElvisConfig),
27372753
Ext = proplists:get_value(test_file_ext, Config, "erl"),
27382754

2739-
{ok, File} = elvis_test_utils:find_file(SrcDirs, FilenameNoExt ++ "." ++ Ext),
2755+
FullFilename = FilenameNoExt ++ "." ++ Ext,
2756+
{ok, File} = elvis_test_utils:find_file(SrcDirs, FullFilename),
27402757

2758+
ct:comment("Checking ~ts", [FullFilename]),
27412759
{ok, #{rules := RuleResults}} = elvis_core:do_rock(File, ElvisConfig),
27422760
[[] = Items || #{items := Items} <- RuleResults],
2743-
true.
2761+
{comment, ""}.
27442762

27452763
-spec is_item_line_sort([elvis_result:file()]) -> [boolean()].
27462764
is_item_line_sort(Result) ->

0 commit comments

Comments
 (0)