Skip to content

Commit

Permalink
feat(inaka#297): New rule: Prefer Unquoted Atoms
Browse files Browse the repository at this point in the history
  • Loading branch information
Milán Bór authored and Milán Bór committed Sep 2, 2024
1 parent 7d7ac44 commit 4e94c15
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 3 deletions.
49 changes: 48 additions & 1 deletion src/elvis_style.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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]).
Expand Down Expand Up @@ -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. "
Expand Down Expand Up @@ -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 => ".*",
Expand Down Expand Up @@ -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) ->
Expand Down Expand Up @@ -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),
Expand Down
11 changes: 11 additions & 0 deletions test/examples/fail_quoted_atoms.erl
Original file line number Diff line number Diff line change
@@ -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'.
9 changes: 9 additions & 0 deletions test/examples/pass_unquoted_atoms.erl
Original file line number Diff line number Diff line change
@@ -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.

25 changes: 23 additions & 2 deletions test/style_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) ->
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit 4e94c15

Please sign in to comment.