From 0cdccb628f9d94a43feb9990aceb1842aeefc7b8 Mon Sep 17 00:00:00 2001 From: belltoy Date: Thu, 15 Aug 2024 22:01:07 +0800 Subject: [PATCH] Add opaque state as a type declaration --- .../elvis_style/state_record_and_type.md | 5 +- src/elvis_style.erl | 15 +++++- ...record_and_type_plus_export_used_types.erl | 42 +++++++++++++++++ .../pass_state_record_and_type_opaque.erl | 44 ++++++++++++++++++ ...record_and_type_plus_export_used_types.erl | 44 ++++++++++++++++++ ...type_plus_export_used_types_gen_statem.erl | 29 ++++++++++++ test/style_SUITE.erl | 46 +++++++++++++++---- 7 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 test/examples/fail_state_record_and_type_plus_export_used_types.erl create mode 100644 test/examples/pass_state_record_and_type_opaque.erl create mode 100644 test/examples/pass_state_record_and_type_plus_export_used_types.erl create mode 100644 test/examples/pass_state_record_and_type_plus_export_used_types_gen_statem.erl diff --git a/doc_rules/elvis_style/state_record_and_type.md b/doc_rules/elvis_style/state_record_and_type.md index 34a56f4..b8d8eae 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 5329914..1431170 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_plus_export_used_types.erl b/test/examples/fail_state_record_and_type_plus_export_used_types.erl new file mode 100644 index 0000000..f0ece13 --- /dev/null +++ b/test/examples/fail_state_record_and_type_plus_export_used_types.erl @@ -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. 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 0000000..2d83cee --- /dev/null +++ b/test/examples/pass_state_record_and_type_opaque.erl @@ -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. 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 0000000..7301813 --- /dev/null +++ b/test/examples/pass_state_record_and_type_plus_export_used_types.erl @@ -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. 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 0000000..57bd991 --- /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 d7c2cf5..c59f94e 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"),