diff --git a/src/elvis_style.erl b/src/elvis_style.erl index 1431170..882a248 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 0000000..390ca87 --- /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 0000000..eaded94 --- /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 c59f94e..3c272cd 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),