From 4e94c15fc5dc16f7736f2ead9e1e11a64ec5e183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Mon, 2 Sep 2024 21:06:19 +0200 Subject: [PATCH 1/8] feat(#297): New rule: Prefer Unquoted Atoms --- src/elvis_style.erl | 49 ++++++++++++++++++++++++++- test/examples/fail_quoted_atoms.erl | 11 ++++++ test/examples/pass_unquoted_atoms.erl | 9 +++++ test/style_SUITE.erl | 25 ++++++++++++-- 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 test/examples/fail_quoted_atoms.erl create mode 100644 test/examples/pass_unquoted_atoms.erl diff --git a/src/elvis_style.erl b/src/elvis_style.erl index 14311707..882a248c 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -12,7 +12,8 @@ atom_naming_convention/3, no_throw/3, no_dollar_space/3, no_author/3, no_import/3, no_catch_expressions/3, no_single_clause_case/3, numeric_format/3, behaviour_spelling/3, always_shortcircuit/3, consistent_generic_type/3, export_used_types/3, - no_match_in_condition/3, param_pattern_matching/3, private_data_types/3, option/3]). + no_match_in_condition/3, param_pattern_matching/3, private_data_types/3, option/3 + , prefer_unquoted_atoms/3]). -export_type([empty_rule_config/0]). -export_type([ignorable/0]). @@ -107,6 +108,9 @@ -define(ATOM_NAMING_CONVENTION_MSG, "Atom ~p on line ~p does not respect the format " "defined by the regular expression '~p'."). +-define(ATOM_PREFERRED_QUOTES_MSG, + "Atom ~p on line ~p has quotes on them " + "but its not needed on it."). -define(NO_THROW_MSG, "Usage of throw/1 on line ~p is not recommended"). -define(NO_DOLLAR_SPACE_MSG, "'$ ' was found on line ~p. It's use is discouraged. " @@ -196,6 +200,8 @@ default(no_common_caveats_call) -> {erlang, size, 1}]}; default(atom_naming_convention) -> #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$", enclosed_atoms => ".*"}; +default(prefer_unquoted_atoms) -> + #{regex => "^'([a-z_0-9)]+)'$", enclosed_atoms => ".*"}; %% Not restrictive. Those who want more restrictions can set it like "^[^_]*$" default(numeric_format) -> #{regex => ".*", @@ -1015,6 +1021,19 @@ atom_naming_convention(Config, Target, RuleConfig) -> AtomNodes = elvis_code:find(fun is_atom_node/1, Root, #{traverse => all, mode => node}), check_atom_names(Regex, RegexEnclosed, AtomNodes, []). +-spec prefer_unquoted_atoms(elvis_config:config(), + elvis_file:file(), + empty_rule_config()) -> + [elvis_result:item()]. +prefer_unquoted_atoms(_Config, Target, RuleConfig) -> + Regex = option(regex, RuleConfig, prefer_unquoted_atoms), + RegexEnclosed = + specific_or_default(option(enclosed_atoms, RuleConfig, atom_naming_convention), Regex), + {Content, #{encoding := _Encoding}} = elvis_file:src(Target), + Tree = ktn_code:parse_tree(Content), + AtomNodes = elvis_code:find(fun is_atom_node/1, Tree, #{traverse => all, mode => node}), + check_atom_quotes(Regex, RegexEnclosed, AtomNodes, []). + -spec no_throw(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. no_throw(Config, Target, RuleConfig) -> @@ -1456,6 +1475,34 @@ check_atom_names(Regex, RegexEnclosed, [AtomNode | RemainingAtomNodes], AccIn) - end, check_atom_names(Regex, RegexEnclosed, RemainingAtomNodes, AccOut). +%% @private +check_atom_quotes(_Regex, _RegexEnclosed, [] = _AtomNodes, Acc) -> + Acc; +check_atom_quotes(Regex, RegexEnclosed, [AtomNode | RemainingAtomNodes], AccIn) -> + AtomName0 = ktn_code:attr(text, AtomNode), + ValueAtomName = ktn_code:attr(value, AtomNode), + {IsEnclosed, AtomName} = string_strip_enclosed(AtomName0), + IsExceptionClass = is_exception_or_non_reversible(ValueAtomName), + RE = re_compile_for_atom_type(IsEnclosed, Regex, RegexEnclosed), + AccOut = + case re:run( + unicode:characters_to_list(AtomName, unicode), RE) + of + _ when IsExceptionClass andalso not IsEnclosed -> + AccIn; + nomatch when not IsEnclosed -> + AccIn; + nomatch when IsEnclosed -> + AccIn; + {match, _Captured} -> + Msg = ?ATOM_PREFERRED_QUOTES_MSG, + {Line, _} = ktn_code:attr(location, AtomNode), + Info = [AtomName0, Line, RegexEnclosed], + Result = elvis_result:new(item, Msg, Info, Line), + AccIn ++ [Result] + end, + check_atom_quotes(Regex, RegexEnclosed, RemainingAtomNodes, AccOut). + %% @private string_strip_enclosed([$' | Rest]) -> [$' | Reversed] = lists:reverse(Rest), diff --git a/test/examples/fail_quoted_atoms.erl b/test/examples/fail_quoted_atoms.erl new file mode 100644 index 00000000..390ca87c --- /dev/null +++ b/test/examples/fail_quoted_atoms.erl @@ -0,0 +1,11 @@ +-module(fail_quoted_atoms). + +-export([test/1, test/2]). + +-define(TEST(), test(1, default)). + +test(_Test) -> {ok, test}. + +test(_A, 'ugly_atom_name') -> 'why_use_quotes_here'; +test(_A, default) -> ?TEST(); +test(_A, _B) -> 'quoted_atom'. diff --git a/test/examples/pass_unquoted_atoms.erl b/test/examples/pass_unquoted_atoms.erl new file mode 100644 index 00000000..eaded94c --- /dev/null +++ b/test/examples/pass_unquoted_atoms.erl @@ -0,0 +1,9 @@ +-module(pass_unquoted_atoms). + +-export([test/1, test/2]). + +test(_Test) -> ok. + +test(_A, nice_atom_name) -> perfect_atomname; +test(_A, _B) -> unquoted_atom. + diff --git a/test/style_SUITE.erl b/test/style_SUITE.erl index c59f94e7..3c272cd3 100644 --- a/test/style_SUITE.erl +++ b/test/style_SUITE.erl @@ -25,7 +25,7 @@ verify_always_shortcircuit/1, verify_consistent_generic_type/1, verify_no_types/1, verify_no_specs/1, verify_export_used_types/1, verify_consistent_variable_casing/1, verify_no_match_in_condition/1, verify_param_pattern_matching/1, - verify_private_data_types/1]). + verify_private_data_types/1, verify_unquoted_atoms/1]). %% -elvis attribute -export([verify_elvis_attr_atom_naming_convention/1, verify_elvis_attr_numeric_format/1, verify_elvis_attr_dont_repeat_yourself/1, verify_elvis_attr_function_naming_convention/1, @@ -82,7 +82,7 @@ groups() -> 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_param_pattern_matching, verify_private_data_types, verify_unquoted_atoms]}]. -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> @@ -1297,6 +1297,27 @@ verify_no_successive_maps(_Config) -> -endif. +-spec verify_unquoted_atoms(config()) -> any(). +verify_unquoted_atoms(Config) -> + BaseRegex = "^'([a-z_0-9)]+)'$", + RuleConfig = #{regex => BaseRegex}, + + PassPath = "pass_unquoted_atoms." ++ "erl", + [] = + elvis_core_apply_rule(Config, + elvis_style, + prefer_unquoted_atoms, + RuleConfig, + PassPath), + + FailPath = "fail_quoted_atoms." ++ "erl", + [_, _, _] = + elvis_core_apply_rule(Config, + elvis_style, + prefer_unquoted_atoms, + RuleConfig, + FailPath). + -spec verify_atom_naming_convention(config()) -> any(). verify_atom_naming_convention(Config) -> Group = proplists:get_value(group, Config, erl_files), From c45581e4c1df6ccff48a637f254390c8ea98886b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Tue, 10 Sep 2024 18:51:59 +0200 Subject: [PATCH 2/8] feat(#297): fix suggestions --- src/elvis_style.erl | 48 +++++++++++++++-------------- test/examples/fail_quoted_atoms.erl | 2 +- test/style_SUITE.erl | 17 ++-------- 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/elvis_style.erl b/src/elvis_style.erl index 882a248c..424095ee 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -12,8 +12,8 @@ atom_naming_convention/3, no_throw/3, no_dollar_space/3, no_author/3, no_import/3, no_catch_expressions/3, no_single_clause_case/3, numeric_format/3, behaviour_spelling/3, always_shortcircuit/3, consistent_generic_type/3, export_used_types/3, - no_match_in_condition/3, param_pattern_matching/3, private_data_types/3, option/3 - , prefer_unquoted_atoms/3]). + no_match_in_condition/3, param_pattern_matching/3, private_data_types/3, option/3, + prefer_unquoted_atoms/3]). -export_type([empty_rule_config/0]). -export_type([ignorable/0]). @@ -109,8 +109,8 @@ "Atom ~p on line ~p does not respect the format " "defined by the regular expression '~p'."). -define(ATOM_PREFERRED_QUOTES_MSG, - "Atom ~p on line ~p has quotes on them " - "but its not needed on it."). + "Atom ~p on line ~p is quoted " + "but quotes are not needed."). -define(NO_THROW_MSG, "Usage of throw/1 on line ~p is not recommended"). -define(NO_DOLLAR_SPACE_MSG, "'$ ' was found on line ~p. It's use is discouraged. " @@ -200,8 +200,6 @@ default(no_common_caveats_call) -> {erlang, size, 1}]}; default(atom_naming_convention) -> #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$", enclosed_atoms => ".*"}; -default(prefer_unquoted_atoms) -> - #{regex => "^'([a-z_0-9)]+)'$", enclosed_atoms => ".*"}; %% Not restrictive. Those who want more restrictions can set it like "^[^_]*$" default(numeric_format) -> #{regex => ".*", @@ -1025,14 +1023,11 @@ atom_naming_convention(Config, Target, RuleConfig) -> elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. -prefer_unquoted_atoms(_Config, Target, RuleConfig) -> - Regex = option(regex, RuleConfig, prefer_unquoted_atoms), - RegexEnclosed = - specific_or_default(option(enclosed_atoms, RuleConfig, atom_naming_convention), Regex), +prefer_unquoted_atoms(_Config, Target, _RuleConfig) -> {Content, #{encoding := _Encoding}} = elvis_file:src(Target), Tree = ktn_code:parse_tree(Content), AtomNodes = elvis_code:find(fun is_atom_node/1, Tree, #{traverse => all, mode => node}), - check_atom_quotes(Regex, RegexEnclosed, AtomNodes, []). + check_atom_quotes(AtomNodes, []). -spec no_throw(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. @@ -1476,32 +1471,39 @@ check_atom_names(Regex, RegexEnclosed, [AtomNode | RemainingAtomNodes], AccIn) - check_atom_names(Regex, RegexEnclosed, RemainingAtomNodes, AccOut). %% @private -check_atom_quotes(_Regex, _RegexEnclosed, [] = _AtomNodes, Acc) -> +check_atom_quotes([] = _AtomNodes, Acc) -> Acc; -check_atom_quotes(Regex, RegexEnclosed, [AtomNode | RemainingAtomNodes], AccIn) -> +check_atom_quotes([AtomNode | RemainingAtomNodes], AccIn) -> + Regex = "^'([a-z_0-9)]+)'$", + RegexEnclosed = ".*", AtomName0 = ktn_code:attr(text, AtomNode), ValueAtomName = ktn_code:attr(value, AtomNode), {IsEnclosed, AtomName} = string_strip_enclosed(AtomName0), - IsExceptionClass = is_exception_or_non_reversible(ValueAtomName), + + IsException = is_exception_prefer_quoted(ValueAtomName), RE = re_compile_for_atom_type(IsEnclosed, Regex, RegexEnclosed), AccOut = case re:run( unicode:characters_to_list(AtomName, unicode), RE) of - _ when IsExceptionClass andalso not IsEnclosed -> - AccIn; - nomatch when not IsEnclosed -> - AccIn; - nomatch when IsEnclosed -> - AccIn; - {match, _Captured} -> + {match, _Captured} when not IsException -> Msg = ?ATOM_PREFERRED_QUOTES_MSG, {Line, _} = ktn_code:attr(location, AtomNode), Info = [AtomName0, Line, RegexEnclosed], Result = elvis_result:new(item, Msg, Info, Line), - AccIn ++ [Result] + AccIn ++ [Result]; + _ -> + AccIn end, - check_atom_quotes(Regex, RegexEnclosed, RemainingAtomNodes, AccOut). + check_atom_quotes(RemainingAtomNodes, AccOut). + +%% @private +is_exception_prefer_quoted(Elem) -> + KeyWords = + ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', + 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', + 'rem', 'try', 'when', 'xor'], + lists:member(Elem, KeyWords). %% @private string_strip_enclosed([$' | Rest]) -> diff --git a/test/examples/fail_quoted_atoms.erl b/test/examples/fail_quoted_atoms.erl index 390ca87c..aa56755a 100644 --- a/test/examples/fail_quoted_atoms.erl +++ b/test/examples/fail_quoted_atoms.erl @@ -8,4 +8,4 @@ test(_Test) -> {ok, test}. test(_A, 'ugly_atom_name') -> 'why_use_quotes_here'; test(_A, default) -> ?TEST(); -test(_A, _B) -> 'quoted_atom'. +test(_A, _B) -> 'and'. diff --git a/test/style_SUITE.erl b/test/style_SUITE.erl index 3c272cd3..d53b0761 100644 --- a/test/style_SUITE.erl +++ b/test/style_SUITE.erl @@ -1299,24 +1299,11 @@ verify_no_successive_maps(_Config) -> -spec verify_unquoted_atoms(config()) -> any(). verify_unquoted_atoms(Config) -> - BaseRegex = "^'([a-z_0-9)]+)'$", - RuleConfig = #{regex => BaseRegex}, - PassPath = "pass_unquoted_atoms." ++ "erl", - [] = - elvis_core_apply_rule(Config, - elvis_style, - prefer_unquoted_atoms, - RuleConfig, - PassPath), + [] = elvis_core_apply_rule(Config, elvis_style, prefer_unquoted_atoms, #{}, PassPath), FailPath = "fail_quoted_atoms." ++ "erl", - [_, _, _] = - elvis_core_apply_rule(Config, - elvis_style, - prefer_unquoted_atoms, - RuleConfig, - FailPath). + [_, _] = elvis_core_apply_rule(Config, elvis_style, prefer_unquoted_atoms, #{}, FailPath). -spec verify_atom_naming_convention(config()) -> any(). verify_atom_naming_convention(Config) -> From 3aef3ee55e9cdcaad17647ae600385457fa4a84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Tue, 10 Sep 2024 19:24:39 +0200 Subject: [PATCH 3/8] feat(#297): add RULES.md doc --- RULES.md | 1 + doc_rules/elvis_style/prefer_unquoted_atoms.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 doc_rules/elvis_style/prefer_unquoted_atoms.md diff --git a/RULES.md b/RULES.md index da5561e7..12f5f620 100644 --- a/RULES.md +++ b/RULES.md @@ -61,6 +61,7 @@ identified with `(since ...)` for convenience purposes. - [State Record and Type](doc_rules/elvis_style/state_record_and_type.md) - [Used Ignored Variable](doc_rules/elvis_style/used_ignored_variable.md) - [Variable Naming Convention](doc_rules/elvis_style/variable_naming_convention.md) +- [Prefer Unquoted Atoms](doc_rules/elvis_style/prefer_unquoted_atoms.md) ## Project rules diff --git a/doc_rules/elvis_style/prefer_unquoted_atoms.md b/doc_rules/elvis_style/prefer_unquoted_atoms.md new file mode 100644 index 00000000..673b41d2 --- /dev/null +++ b/doc_rules/elvis_style/prefer_unquoted_atoms.md @@ -0,0 +1,15 @@ +# Prefer unquoted atoms + +Do not use quotes on atoms that are not need it. + +> Works on `.beam` file? No. + +## Options + +- None. + +## Example + +```erlang +{elvis_style, prefer_unquoted_atoms, #{}} +``` From 198544b7088c7ae27ec57ee77825a05243ca3d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Thu, 12 Sep 2024 17:48:39 +0200 Subject: [PATCH 4/8] feat(#297): fix suggestions --- doc_rules/elvis_style/prefer_unquoted_atoms.md | 2 +- src/elvis_style.erl | 15 +++++---------- test/examples/pass_unquoted_atoms.erl | 4 +++- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/doc_rules/elvis_style/prefer_unquoted_atoms.md b/doc_rules/elvis_style/prefer_unquoted_atoms.md index 673b41d2..1f545c6e 100644 --- a/doc_rules/elvis_style/prefer_unquoted_atoms.md +++ b/doc_rules/elvis_style/prefer_unquoted_atoms.md @@ -1,6 +1,6 @@ # Prefer unquoted atoms -Do not use quotes on atoms that are not need it. +Do not use quotes on atoms that don't need to be quoted. > Works on `.beam` file? No. diff --git a/src/elvis_style.erl b/src/elvis_style.erl index 424095ee..4b037cc8 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -1474,22 +1474,17 @@ check_atom_names(Regex, RegexEnclosed, [AtomNode | RemainingAtomNodes], AccIn) - check_atom_quotes([] = _AtomNodes, Acc) -> Acc; check_atom_quotes([AtomNode | RemainingAtomNodes], AccIn) -> - Regex = "^'([a-z_0-9)]+)'$", - RegexEnclosed = ".*", - AtomName0 = ktn_code:attr(text, AtomNode), + AtomName = ktn_code:attr(text, AtomNode), ValueAtomName = ktn_code:attr(value, AtomNode), - {IsEnclosed, AtomName} = string_strip_enclosed(AtomName0), IsException = is_exception_prefer_quoted(ValueAtomName), - RE = re_compile_for_atom_type(IsEnclosed, Regex, RegexEnclosed), + AccOut = - case re:run( - unicode:characters_to_list(AtomName, unicode), RE) - of - {match, _Captured} when not IsException -> + case unicode:characters_to_list(AtomName, unicode) of + [$' | _] when not IsException -> Msg = ?ATOM_PREFERRED_QUOTES_MSG, {Line, _} = ktn_code:attr(location, AtomNode), - Info = [AtomName0, Line, RegexEnclosed], + Info = [AtomName, Line], Result = elvis_result:new(item, Msg, Info, Line), AccIn ++ [Result]; _ -> diff --git a/test/examples/pass_unquoted_atoms.erl b/test/examples/pass_unquoted_atoms.erl index eaded94c..ffbe676e 100644 --- a/test/examples/pass_unquoted_atoms.erl +++ b/test/examples/pass_unquoted_atoms.erl @@ -5,5 +5,7 @@ test(_Test) -> ok. test(_A, nice_atom_name) -> perfect_atomname; -test(_A, _B) -> unquoted_atom. +test(_Reserved, _Words) -> ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', + 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', + 'rem', 'try', 'when', 'xor']. From 164cc66984b104c61acefddd6e452fe7b7a29e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Mon, 16 Sep 2024 11:10:47 +0200 Subject: [PATCH 5/8] move rule to elvis_text_style --- src/elvis_style.erl | 46 +-------------------------------- src/elvis_text_style.erl | 55 ++++++++++++++++++++++++++++++++++++++-- test/style_SUITE.erl | 6 +++-- 3 files changed, 58 insertions(+), 49 deletions(-) diff --git a/src/elvis_style.erl b/src/elvis_style.erl index 4b037cc8..14311707 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -12,8 +12,7 @@ atom_naming_convention/3, no_throw/3, no_dollar_space/3, no_author/3, no_import/3, no_catch_expressions/3, no_single_clause_case/3, numeric_format/3, behaviour_spelling/3, always_shortcircuit/3, consistent_generic_type/3, export_used_types/3, - no_match_in_condition/3, param_pattern_matching/3, private_data_types/3, option/3, - prefer_unquoted_atoms/3]). + no_match_in_condition/3, param_pattern_matching/3, private_data_types/3, option/3]). -export_type([empty_rule_config/0]). -export_type([ignorable/0]). @@ -108,9 +107,6 @@ -define(ATOM_NAMING_CONVENTION_MSG, "Atom ~p on line ~p does not respect the format " "defined by the regular expression '~p'."). --define(ATOM_PREFERRED_QUOTES_MSG, - "Atom ~p on line ~p is quoted " - "but quotes are not needed."). -define(NO_THROW_MSG, "Usage of throw/1 on line ~p is not recommended"). -define(NO_DOLLAR_SPACE_MSG, "'$ ' was found on line ~p. It's use is discouraged. " @@ -1019,16 +1015,6 @@ atom_naming_convention(Config, Target, RuleConfig) -> AtomNodes = elvis_code:find(fun is_atom_node/1, Root, #{traverse => all, mode => node}), check_atom_names(Regex, RegexEnclosed, AtomNodes, []). --spec prefer_unquoted_atoms(elvis_config:config(), - elvis_file:file(), - empty_rule_config()) -> - [elvis_result:item()]. -prefer_unquoted_atoms(_Config, Target, _RuleConfig) -> - {Content, #{encoding := _Encoding}} = elvis_file:src(Target), - Tree = ktn_code:parse_tree(Content), - AtomNodes = elvis_code:find(fun is_atom_node/1, Tree, #{traverse => all, mode => node}), - check_atom_quotes(AtomNodes, []). - -spec no_throw(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. no_throw(Config, Target, RuleConfig) -> @@ -1470,36 +1456,6 @@ check_atom_names(Regex, RegexEnclosed, [AtomNode | RemainingAtomNodes], AccIn) - end, check_atom_names(Regex, RegexEnclosed, RemainingAtomNodes, AccOut). -%% @private -check_atom_quotes([] = _AtomNodes, Acc) -> - Acc; -check_atom_quotes([AtomNode | RemainingAtomNodes], AccIn) -> - AtomName = ktn_code:attr(text, AtomNode), - ValueAtomName = ktn_code:attr(value, AtomNode), - - IsException = is_exception_prefer_quoted(ValueAtomName), - - AccOut = - case unicode:characters_to_list(AtomName, unicode) of - [$' | _] when not IsException -> - Msg = ?ATOM_PREFERRED_QUOTES_MSG, - {Line, _} = ktn_code:attr(location, AtomNode), - Info = [AtomName, Line], - Result = elvis_result:new(item, Msg, Info, Line), - AccIn ++ [Result]; - _ -> - AccIn - end, - check_atom_quotes(RemainingAtomNodes, AccOut). - -%% @private -is_exception_prefer_quoted(Elem) -> - KeyWords = - ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', - 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', - 'rem', 'try', 'when', 'xor'], - lists:member(Elem, KeyWords). - %% @private string_strip_enclosed([$' | Rest]) -> [$' | Reversed] = lists:reverse(Rest), diff --git a/src/elvis_text_style.erl b/src/elvis_text_style.erl index c560d166..d677a9e7 100644 --- a/src/elvis_text_style.erl +++ b/src/elvis_text_style.erl @@ -1,19 +1,26 @@ -module(elvis_text_style). --export([default/1, line_length/3, no_tabs/3, no_trailing_whitespace/3]). +-export([default/1, line_length/3, no_tabs/3, no_trailing_whitespace/3, + prefer_unquoted_atoms/3]). -export_type([line_length_config/0, no_trailing_whitespace_config/0]). -define(LINE_LENGTH_MSG, "Line ~p is too long. It has ~p characters."). -define(NO_TABS_MSG, "Line ~p has a tab at column ~p."). -define(NO_TRAILING_WHITESPACE_MSG, "Line ~b has ~b trailing whitespace characters."). +-define(ATOM_PREFERRED_QUOTES_MSG, + "Atom ~p on line ~p is quoted " + "but quotes are not needed."). % These are part of a non-declared "behaviour" % The reason why we don't try to handle them with different arity is % that arguments are ignored in different positions (1 and 3) so that'd % probably be messier than to ignore the warning -hank([{unnecessary_function_arguments, - [{no_trailing_whitespace, 3}, {no_tabs, 3}, {line_length, 3}]}]). + [{no_trailing_whitespace, 3}, + {no_tabs, 3}, + {line_length, 3}, + {prefer_unquoted_atoms, 3}]}]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Default values @@ -71,6 +78,38 @@ no_trailing_whitespace(_Config, Target, RuleConfig) -> end, RuleConfig). +-spec prefer_unquoted_atoms(elvis_config:config(), + elvis_file:file(), + elvis_style:empty_rule_config()) -> + [elvis_result:item()]. +prefer_unquoted_atoms(_Config, Target, _RuleConfig) -> + {Content, #{encoding := _Encoding}} = elvis_file:src(Target), + Tree = ktn_code:parse_tree(Content), + AtomNodes = elvis_code:find(fun is_atom_node/1, Tree, #{traverse => all, mode => node}), + check_atom_quotes(AtomNodes, []). + +%% @private +check_atom_quotes([] = _AtomNodes, Acc) -> + Acc; +check_atom_quotes([AtomNode | RemainingAtomNodes], AccIn) -> + AtomName = ktn_code:attr(text, AtomNode), + ValueAtomName = ktn_code:attr(value, AtomNode), + + IsException = is_exception_prefer_quoted(ValueAtomName), + + AccOut = + case unicode:characters_to_list(AtomName, unicode) of + [$' | _] when not IsException -> + Msg = ?ATOM_PREFERRED_QUOTES_MSG, + {Line, _} = ktn_code:attr(location, AtomNode), + Info = [AtomName, Line], + Result = elvis_result:new(item, Msg, Info, Line), + AccIn ++ [Result]; + _ -> + AccIn + end, + check_atom_quotes(RemainingAtomNodes, AccOut). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Private %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -164,6 +203,18 @@ check_no_trailing_whitespace(Line, Num, IgnoreEmptyLines) -> {ok, Result} end. +%% @private +is_atom_node(MaybeAtom) -> + ktn_code:type(MaybeAtom) =:= atom. + +%% @private +is_exception_prefer_quoted(Elem) -> + KeyWords = + ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', + 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', + 'rem', 'try', 'when', 'xor'], + lists:member(Elem, KeyWords). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Internal Function Definitions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/test/style_SUITE.erl b/test/style_SUITE.erl index d53b0761..8ebfaea0 100644 --- a/test/style_SUITE.erl +++ b/test/style_SUITE.erl @@ -1300,10 +1300,12 @@ verify_no_successive_maps(_Config) -> -spec verify_unquoted_atoms(config()) -> any(). verify_unquoted_atoms(Config) -> PassPath = "pass_unquoted_atoms." ++ "erl", - [] = elvis_core_apply_rule(Config, elvis_style, prefer_unquoted_atoms, #{}, PassPath), + [] = + elvis_core_apply_rule(Config, elvis_text_style, prefer_unquoted_atoms, #{}, PassPath), FailPath = "fail_quoted_atoms." ++ "erl", - [_, _] = elvis_core_apply_rule(Config, elvis_style, prefer_unquoted_atoms, #{}, FailPath). + [_, _] = + elvis_core_apply_rule(Config, elvis_text_style, prefer_unquoted_atoms, #{}, FailPath). -spec verify_atom_naming_convention(config()) -> any(). verify_atom_naming_convention(Config) -> From feddb405febfeaa1d0bdb52597f494ffa7e1e389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Mon, 16 Sep 2024 12:00:35 +0200 Subject: [PATCH 6/8] add maybe --- src/elvis_text_style.erl | 2 +- test/examples/pass_unquoted_atoms.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/elvis_text_style.erl b/src/elvis_text_style.erl index d677a9e7..ed88fea8 100644 --- a/src/elvis_text_style.erl +++ b/src/elvis_text_style.erl @@ -212,7 +212,7 @@ is_exception_prefer_quoted(Elem) -> KeyWords = ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', - 'rem', 'try', 'when', 'xor'], + 'rem', 'try', 'when', 'xor', maybe], lists:member(Elem, KeyWords). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/test/examples/pass_unquoted_atoms.erl b/test/examples/pass_unquoted_atoms.erl index ffbe676e..48ddb761 100644 --- a/test/examples/pass_unquoted_atoms.erl +++ b/test/examples/pass_unquoted_atoms.erl @@ -7,5 +7,5 @@ test(_Test) -> ok. test(_A, nice_atom_name) -> perfect_atomname; test(_Reserved, _Words) -> ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', - 'rem', 'try', 'when', 'xor']. + 'rem', 'try', 'when', 'xor', 'maybe']. From 9827d6851043abb9a232436dea5c08b4ca775aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Mon, 16 Sep 2024 12:02:24 +0200 Subject: [PATCH 7/8] move rules --- RULES.md | 2 +- .../{elvis_style => elvis_text_style}/prefer_unquoted_atoms.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename doc_rules/{elvis_style => elvis_text_style}/prefer_unquoted_atoms.md (100%) diff --git a/RULES.md b/RULES.md index 12f5f620..34ce8925 100644 --- a/RULES.md +++ b/RULES.md @@ -61,7 +61,7 @@ identified with `(since ...)` for convenience purposes. - [State Record and Type](doc_rules/elvis_style/state_record_and_type.md) - [Used Ignored Variable](doc_rules/elvis_style/used_ignored_variable.md) - [Variable Naming Convention](doc_rules/elvis_style/variable_naming_convention.md) -- [Prefer Unquoted Atoms](doc_rules/elvis_style/prefer_unquoted_atoms.md) +- [Prefer Unquoted Atoms](doc_rules/elvis_text_style/prefer_unquoted_atoms.md) ## Project rules diff --git a/doc_rules/elvis_style/prefer_unquoted_atoms.md b/doc_rules/elvis_text_style/prefer_unquoted_atoms.md similarity index 100% rename from doc_rules/elvis_style/prefer_unquoted_atoms.md rename to doc_rules/elvis_text_style/prefer_unquoted_atoms.md From 0b16f2dc2718f5fd91735479b58a98ce39e654f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mil=C3=A1n=20B=C3=B3r?= Date: Mon, 16 Sep 2024 13:20:13 +0200 Subject: [PATCH 8/8] fix exceptions --- src/elvis_text_style.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/elvis_text_style.erl b/src/elvis_text_style.erl index ed88fea8..eaeb44b1 100644 --- a/src/elvis_text_style.erl +++ b/src/elvis_text_style.erl @@ -93,9 +93,8 @@ check_atom_quotes([] = _AtomNodes, Acc) -> Acc; check_atom_quotes([AtomNode | RemainingAtomNodes], AccIn) -> AtomName = ktn_code:attr(text, AtomNode), - ValueAtomName = ktn_code:attr(value, AtomNode), - IsException = is_exception_prefer_quoted(ValueAtomName), + IsException = is_exception_prefer_quoted(AtomName), AccOut = case unicode:characters_to_list(AtomName, unicode) of @@ -210,9 +209,10 @@ is_atom_node(MaybeAtom) -> %% @private is_exception_prefer_quoted(Elem) -> KeyWords = - ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', - 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', - 'rem', 'try', 'when', 'xor', maybe], + ["'after'", "'and'", "'andalso'", "'band'", "'begin'", "'bnot'", "'bor'", "'bsl'", + "'bsr'", "'bxor'", "'case'", "'catch'", "'cond'", "'div'", "'end'", "'fun'", "'if'", + "'let'", "'not'", "'of'", "'or'", "'orelse'", "'receive'", "'rem'", "'try'", "'when'", + "'xor'", "'maybe'"], lists:member(Elem, KeyWords). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%