Skip to content

Commit

Permalink
Add opaque state as a type declaration
Browse files Browse the repository at this point in the history
  • Loading branch information
belltoy committed Aug 18, 2024
1 parent 3e327ef commit cdb2f2b
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 11 deletions.
5 changes: 4 additions & 1 deletion doc_rules/elvis_style/state_record_and_type.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# State Record and Type

Every module that implements an OTP behavior in the following list should have a `state` record
(`#state{}`) and a `state` type (`type()`):
(`#state{}`) and a `state` type (`type()`) or a `state` private type (`opaque()`):

- `gen_server`
- `gen_event` (since [0.7.0](https://github.com/inaka/elvis_core/releases/tag/0.7.0))
- `gen_fsm`
- `supervisor_bridge`

If enabled with `export_used_types` together, the `state` record should be defined as a private
type (`opaque()`), and should be exported.

> Works on `.beam` file? Yes!
## Options
Expand Down
15 changes: 14 additions & 1 deletion src/elvis_style.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1834,7 +1834,20 @@ has_state_record(Root) ->
-spec has_state_type(ktn_code:tree_node()) -> boolean().
has_state_type(Root) ->
IsStateType =
fun(Node) -> (type_attr == ktn_code:type(Node)) and (state == ktn_code:attr(name, Node))
fun(Node) ->
case ktn_code:type(Node) of
type_attr ->
state == ktn_code:attr(name, Node);
opaque ->
case ktn_code:attr(value, Node) of
{state, _, _} ->
true;
_ ->
false
end;
_ ->
false
end
end,
elvis_code:find(IsStateType, Root) /= [].

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-module(fail_state_record_and_type_plus_export_used_types).

-dialyzer(no_behaviours).

-behaviour(gen_server).

-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3,
terminate/2
]).

-record(state, {}).

-type state() :: #state{}.

-spec init(term()) -> state().
init(_Args) ->
#state{}.

-spec handle_call(term(), term(), term()) -> ok.
handle_call(_Request, _From, _State) ->
ok.

-spec handle_cast(term(), term()) -> ok.
handle_cast(_Request, _State) ->
ok.

-spec handle_info(term(), term()) -> ok.
handle_info(_Info, _State) ->
ok.

-spec code_change(term(), term(), term()) -> term().
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

-spec terminate(term(), term()) -> ok.
terminate(_Reason, _State) ->
ok.
44 changes: 44 additions & 0 deletions test/examples/pass_state_record_and_type_opaque.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-module(pass_state_record_and_type_opaque).

-dialyzer(no_behaviours).

-behaviour(gen_server).

-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3,
terminate/2
]).

-export_type([state/0]).

-record(state, {}).

-opaque state() :: #state{}.

-spec init(term()) -> state().
init(_Args) ->
#state{}.

-spec handle_call(term(), term(), term()) -> ok.
handle_call(_Request, _From, _State) ->
ok.

-spec handle_cast(term(), term()) -> ok.
handle_cast(_Request, _State) ->
ok.

-spec handle_info(term(), term()) -> ok.
handle_info(_Info, _State) ->
ok.

-spec code_change(term(), term(), term()) -> term().
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

-spec terminate(term(), term()) -> ok.
terminate(_Reason, _State) ->
ok.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-module(pass_state_record_and_type_plus_export_used_types).

-dialyzer(no_behaviours).

-behaviour(gen_server).

-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3,
terminate/2
]).

-export_type([state/0]).

-record(state, {}).

-opaque state() :: #state{}.

-spec init(term()) -> state().
init(_Args) ->
#state{}.

-spec handle_call(term(), term(), term()) -> ok.
handle_call(_Request, _From, _State) ->
ok.

-spec handle_cast(term(), term()) -> ok.
handle_cast(_Request, _State) ->
ok.

-spec handle_info(term(), term()) -> ok.
handle_info(_Info, _State) ->
ok.

-spec code_change(term(), term(), term()) -> term().
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

-spec terminate(term(), term()) -> ok.
terminate(_Reason, _State) ->
ok.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-module(pass_state_record_and_type_plus_export_used_types_gen_statem).

-dialyzer(no_behaviours).

-behaviour(gen_statem).

-export([
init/1,
handle_event/4,
callback_mode/0
]).

-export_type([state/0]).

-record(state, {}).

-opaque state() :: #state{}.

-spec init(term()) -> state().
init(_Args) ->
#state{}.

-spec handle_event(term(), term(), state(), term()) -> {next_state, state(), term()}.
handle_event(_EventType, _EventContent, State, Data) ->
{next_state, State, Data}.

-spec callback_mode() -> handle_event_function.
callback_mode() ->
handle_event_function.
47 changes: 38 additions & 9 deletions test/style_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
verify_god_modules/1, verify_no_if_expression/1, verify_invalid_dynamic_call/1,
verify_used_ignored_variable/1, verify_no_behavior_info/1,
verify_module_naming_convention/1, verify_state_record_and_type/1,
verify_no_spec_with_records/1, verify_dont_repeat_yourself/1, verify_max_module_length/1,
verify_state_record_and_type_plus_export_used_types/1, verify_no_spec_with_records/1,
verify_dont_repeat_yourself/1, verify_max_module_length/1,
verify_max_anonymous_function_arity/1, verify_max_function_arity/1,
verify_max_function_length/1, verify_no_debug_call/1, verify_no_common_caveats_call/1,
verify_no_call/1, verify_no_nested_try_catch/1, verify_no_successive_maps/1,
Expand Down Expand Up @@ -74,14 +75,14 @@ groups() ->
verify_consistent_variable_casing, verify_nesting_level, verify_god_modules,
verify_no_if_expression, verify_invalid_dynamic_call, verify_used_ignored_variable,
verify_no_behavior_info, verify_module_naming_convention, verify_state_record_and_type,
verify_no_spec_with_records, verify_dont_repeat_yourself, verify_no_debug_call,
verify_no_common_caveats_call, verify_no_call, verify_no_nested_try_catch,
verify_no_successive_maps, verify_atom_naming_convention, verify_no_throw,
verify_no_author, verify_no_import, verify_always_shortcircuit,
verify_no_catch_expressions, verify_no_single_clause_case, verify_no_macros,
verify_export_used_types, verify_max_anonymous_function_arity, verify_max_function_arity,
verify_no_match_in_condition, verify_behaviour_spelling, verify_param_pattern_matching,
verify_private_data_types]}].
verify_state_record_and_type_plus_export_used_types, verify_no_spec_with_records,
verify_dont_repeat_yourself, verify_no_debug_call, verify_no_common_caveats_call,
verify_no_call, verify_no_nested_try_catch, verify_no_successive_maps,
verify_atom_naming_convention, verify_no_throw, verify_no_author, verify_no_import,
verify_always_shortcircuit, verify_no_catch_expressions, verify_no_single_clause_case,
verify_no_macros, verify_export_used_types, verify_max_anonymous_function_arity,
verify_max_function_arity, verify_no_match_in_condition, verify_behaviour_spelling,
verify_param_pattern_matching, verify_private_data_types]}].

-spec init_per_suite(config()) -> config().
init_per_suite(Config) ->
Expand Down Expand Up @@ -691,6 +692,14 @@ verify_state_record_and_type(Config) ->
PathPass = "pass_state_record_and_type." ++ Ext,
[] = elvis_core_apply_rule(Config, elvis_style, state_record_and_type, #{}, PathPass),

PathPassWithOpaque = "pass_state_record_and_type_opaque." ++ Ext,
[] =
elvis_core_apply_rule(Config,
elvis_style,
state_record_and_type,
#{},
PathPassWithOpaque),

PathPassGenStateM = "pass_state_record_and_type_gen_statem." ++ Ext,
[] =
elvis_core_apply_rule(Config, elvis_style, state_record_and_type, #{}, PathPassGenStateM),
Expand Down Expand Up @@ -721,6 +730,26 @@ verify_state_record_and_type(Config) ->
#{},
PathPassGenStateMState).

-spec verify_state_record_and_type_plus_export_used_types(config()) -> any().
verify_state_record_and_type_plus_export_used_types(Config) ->
Ext = proplists:get_value(test_file_ext, Config, "erl"),

PathPass = "pass_state_record_and_type_plus_export_used_types." ++ Ext,
[] = elvis_core_apply_rule(Config, elvis_style, state_record_and_type, #{}, PathPass),
[] = elvis_core_apply_rule(Config, elvis_style, export_used_types, #{}, PathPass),

PathPassGenStateM =
"pass_state_record_and_type_plus_export_used_types_gen_statem." ++ Ext,
[] =
elvis_core_apply_rule(Config, elvis_style, state_record_and_type, #{}, PathPassGenStateM),
[] =
elvis_core_apply_rule(Config, elvis_style, export_used_types, #{}, PathPassGenStateM),

PathFail = "fail_state_record_and_type_plus_export_used_types." ++ Ext,
[] = elvis_core_apply_rule(Config, elvis_style, state_record_and_type, #{}, PathFail),
[_] = elvis_core_apply_rule(Config, elvis_style, export_used_types, #{}, PathFail),
ok.

-spec verify_behaviour_spelling(config()) -> any().
verify_behaviour_spelling(Config) ->
Ext = proplists:get_value(test_file_ext, Config, "erl"),
Expand Down

0 comments on commit cdb2f2b

Please sign in to comment.