diff --git a/doc_rules/elvis_style/state_record_and_type.md b/doc_rules/elvis_style/state_record_and_type.md index 34a56f48..b8d8eaeb 100644 --- a/doc_rules/elvis_style/state_record_and_type.md +++ b/doc_rules/elvis_style/state_record_and_type.md @@ -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 diff --git a/src/elvis_style.erl b/src/elvis_style.erl index 5329914d..14311707 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -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) /= []. diff --git a/test/examples/fail_state_record_and_type.erl b/test/examples/fail_state_record_and_type.erl index ec811b37..c31ef141 100644 --- a/test/examples/fail_state_record_and_type.erl +++ b/test/examples/fail_state_record_and_type.erl @@ -7,10 +7,7 @@ -export([ init/1, handle_call/3, - handle_cast/2, - handle_info/2, - code_change/3, - terminate/2 + handle_cast/2 ]). -spec init(term()) -> ok. @@ -24,15 +21,3 @@ handle_call(_Request, _From, _State) -> -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. diff --git a/test/examples/fail_state_record_and_type_behaviour.erl b/test/examples/fail_state_record_and_type_behaviour.erl index 88f5f7f0..3caba161 100644 --- a/test/examples/fail_state_record_and_type_behaviour.erl +++ b/test/examples/fail_state_record_and_type_behaviour.erl @@ -7,10 +7,7 @@ -export([ init/1, handle_call/3, - handle_cast/2, - handle_info/2, - code_change/3, - terminate/2 + handle_cast/2 ]). -spec init(term()) -> ok. @@ -24,15 +21,3 @@ handle_call(_Request, _From, _State) -> -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. diff --git a/test/examples/fail_state_record_and_type_plus_export_used_types.erl b/test/examples/fail_state_record_and_type_plus_export_used_types.erl new file mode 100644 index 00000000..3cc5de64 --- /dev/null +++ b/test/examples/fail_state_record_and_type_plus_export_used_types.erl @@ -0,0 +1,27 @@ +-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 + ]). + +-record(state, {}). + +-type state() :: #state{}. + +-spec init(term()) -> state(). +init(_Args) -> + #state{}. + +-spec handle_call(term(), term(), state()) -> {noreply, state()}. +handle_call(_Request, _From, State) -> + {noreply, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast(_Request, State) -> + {noreply, State}. diff --git a/test/examples/pass_state_record_and_type.erl b/test/examples/pass_state_record_and_type.erl index a09c7507..04e34e9c 100644 --- a/test/examples/pass_state_record_and_type.erl +++ b/test/examples/pass_state_record_and_type.erl @@ -7,10 +7,7 @@ -export([ init/1, handle_call/3, - handle_cast/2, - handle_info/2, - code_change/3, - terminate/2 + handle_cast/2 ]). -record(state, {}). @@ -21,22 +18,10 @@ init(_Args) -> #state{}. --spec handle_call(term(), term(), term()) -> ok. -handle_call(_Request, _From, _State) -> - ok. +-spec handle_call(term(), term(), state()) -> {noreply, state()}. +handle_call(_Request, _From, State) -> + {noreply, State}. --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. +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast(_Request, State) -> + {noreply, State}. diff --git a/test/examples/pass_state_record_and_type_elvis_attr.erl b/test/examples/pass_state_record_and_type_elvis_attr.erl index e8fb5984..839f5a4e 100644 --- a/test/examples/pass_state_record_and_type_elvis_attr.erl +++ b/test/examples/pass_state_record_and_type_elvis_attr.erl @@ -7,10 +7,7 @@ -export([ init/1, handle_call/3, - handle_cast/2, - handle_info/2, - code_change/3, - terminate/2 + handle_cast/2 ]). -elvis([{elvis_style, state_record_and_type, disable}]). @@ -19,22 +16,10 @@ init(_Args) -> ok. --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_call(term(), term(), term()) -> {noreply, term()}. +handle_call(_Request, _From, State) -> + {noreply, State}. --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. +-spec handle_cast(term(), term()) -> {noreply, term()}. +handle_cast(_Request, State) -> + {noreply, State}. diff --git a/test/examples/pass_state_record_and_type_opaque.erl b/test/examples/pass_state_record_and_type_opaque.erl new file mode 100644 index 00000000..b086b46d --- /dev/null +++ b/test/examples/pass_state_record_and_type_opaque.erl @@ -0,0 +1,29 @@ +-module(pass_state_record_and_type_opaque). + +-dialyzer(no_behaviours). + +-behaviour(gen_server). + +-export([ + init/1, + handle_call/3, + handle_cast/2 + ]). + +-export_type([state/0]). + +-record(state, {}). + +-opaque state() :: #state{}. + +-spec init(term()) -> state(). +init(_Args) -> + #state{}. + +-spec handle_call(term(), term(), state()) -> {noreply, state()}. +handle_call(_Request, _From, State) -> + {noreply, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast(_Request, State) -> + {noreply, State}. diff --git a/test/examples/pass_state_record_and_type_plus_export_used_types.erl b/test/examples/pass_state_record_and_type_plus_export_used_types.erl new file mode 100644 index 00000000..67d0f715 --- /dev/null +++ b/test/examples/pass_state_record_and_type_plus_export_used_types.erl @@ -0,0 +1,29 @@ +-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 + ]). + +-export_type([state/0]). + +-record(state, {}). + +-opaque state() :: #state{}. + +-spec init(term()) -> state(). +init(_Args) -> + #state{}. + +-spec handle_call(term(), term(), state()) -> {noreply, state()}. +handle_call(_Request, _From, State) -> + {noreply, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast(_Request, State) -> + {noreply, State}. diff --git a/test/examples/pass_state_record_and_type_plus_export_used_types_gen_statem.erl b/test/examples/pass_state_record_and_type_plus_export_used_types_gen_statem.erl new file mode 100644 index 00000000..57bd9917 --- /dev/null +++ b/test/examples/pass_state_record_and_type_plus_export_used_types_gen_statem.erl @@ -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. diff --git a/test/style_SUITE.erl b/test/style_SUITE.erl index d7c2cf55..c59f94e7 100644 --- a/test/style_SUITE.erl +++ b/test/style_SUITE.erl @@ -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, @@ -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) -> @@ -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), @@ -721,6 +730,25 @@ 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). + -spec verify_behaviour_spelling(config()) -> any(). verify_behaviour_spelling(Config) -> Ext = proplists:get_value(test_file_ext, Config, "erl"),