Current section

Files

Jump to
elvis_core src elvis_style.erl
Raw

src/elvis_style.erl

-module(elvis_style).
-export([default/1, function_naming_convention/3, variable_naming_convention/3,
consistent_variable_casing/3, macro_names/3, macro_module_names/3, no_macros/3,
no_specs/3, no_types/3, no_block_expressions/3, operator_spaces/3, no_space/3,
no_space_after_pound/3, nesting_level/3, god_modules/3, no_if_expression/3,
invalid_dynamic_call/3, used_ignored_variable/3, no_behavior_info/3,
module_naming_convention/3, state_record_and_type/3, no_spec_with_records/3,
dont_repeat_yourself/3, max_module_length/3, max_anonymous_function_arity/3,
max_function_arity/3, max_function_length/3, no_call/3, no_debug_call/3,
no_common_caveats_call/3, no_nested_try_catch/3, no_successive_maps/3,
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]).
-export_type([empty_rule_config/0]).
-export_type([ignorable/0]).
-export_type([max_anonymous_function_arity_config/0, max_function_arity_config/0,
max_function_length_config/0, max_module_length_config/0,
function_naming_convention_config/0, variable_naming_convention_config/0,
macro_names_config/0, no_macros_config/0, no_types_config/0, no_specs_config/0,
no_block_expressions_config/0, no_space_after_pound_config/0,
operator_spaces_config/0, no_space_config/0, nesting_level_config/0,
god_modules_config/0, module_naming_convention_config/0,
dont_repeat_yourself_config/0, no_call_config/0, no_debug_call_config/0,
no_common_caveats_call_config/0, atom_naming_convention_config/0, no_author_config/0,
no_import_config/0, no_catch_expressions_config/0, numeric_format_config/0,
no_single_clause_case_config/0, consistent_variable_casing_config/0,
no_match_in_condition_config/0, behaviour_spelling_config/0,
param_pattern_matching_config/0, private_data_type_config/0]).
-define(INVALID_MACRO_NAME_REGEX_MSG,
"The macro named ~p on line ~p does not respect the format "
"defined by the regular expression '~p'.").
-define(MACRO_AS_MODULE_NAME_MSG,
"Don't use macros (like ~s on line ~p) as module names.").
-define(MACRO_MODULE_NAMES_EXCEPTIONS, ["MODULE"]).
-define(MACRO_AS_FUNCTION_NAME_MSG,
"Don't use macros (like ~s on line ~p) as function names.").
-define(NO_MACROS_MSG, "Unexpected macro (~p) used on line ~p.").
-define(NO_SPECS_MSG, "Unexpected spec for function ~p defined on line ~p.").
-define(NO_TYPES_MSG, "Unexpected type (~p) defined on line ~p.").
-define(NO_BLOCK_EXPRESSIONS_MSG,
"Unexpected block expression (begin-end) used on line ~p.").
-define(MISSING_SPACE_MSG, "Missing space to the ~s of ~p on line ~p").
-define(UNEXPECTED_SPACE_MSG, "Unexpected space to the ~s of ~p on line ~p").
-define(NESTING_LEVEL_MSG,
"The expression on line ~p and column ~p is nested "
"beyond the maximum level of ~p.").
-define(GOD_MODULES_MSG,
"This module has too many functions (~p). "
"Consider breaking it into a number of modules.").
-define(NO_IF_EXPRESSION_MSG,
"Replace the 'if' expression on line ~p with a 'case' "
"expression or function clauses.").
-define(INVALID_DYNAMIC_CALL_MSG,
"Remove the dynamic function call on line ~p. "
"Only modules that define callbacks should make dynamic calls.").
-define(USED_IGNORED_VAR_MSG,
"Ignored variable is being used on line ~p and "
"column ~p.").
-define(NO_BEHAVIOR_INFO,
"Use the '-callback' attribute instead of 'behavior_info/1' "
"on line ~p.").
-define(FUNCTION_NAMING_CONVENTION_MSG,
"The function ~p does not respect the format defined by the "
"regular expression '~p'.").
-define(VARIABLE_NAMING_CONVENTION_MSG,
"The variable ~p on line ~p does not respect the format "
"defined by the regular expression '~p'.").
-define(CONSISTENT_VARIABLE_CASING_MSG,
"Variable ~ts (first used in line ~p) is written in different ways within the module: ~p.").
-define(MODULE_NAMING_CONVENTION_MSG,
"The module ~p does not respect the format defined by the "
"regular expression '~p'.").
-define(STATE_RECORD_MISSING_MSG,
"This module implements an OTP behavior but is missing "
"a 'state' record.").
-define(STATE_TYPE_MISSING_MSG,
"This module implements an OTP behavior and has a 'state' record "
"but is missing a 'state()' type.").
-define(NO_SPEC_WITH_RECORDS,
"The spec in line ~p uses a record, please define a type for the "
"record and use that instead.").
-define(DONT_REPEAT_YOURSELF,
"The code in the following (LINE, COL) locations has "
"the same structure: ~s.").
-define(MAX_MODULE_LENGTH,
"The code for module ~p has ~p lines which exceeds the "
"maximum of ~p.").
-define(MAX_ANONYMOUS_FUNCTION_ARITY_MSG,
"The arity of the anonymous function defined in line ~p (~w arguments) exceeds the "
"maximum of ~p.").
-define(MAX_FUNCTION_ARITY_MSG, "The arity of function ~p/~w exceeds the maximum of ~p.").
-define(MAX_FUNCTION_LENGTH,
"The code for function ~p/~w has ~p lines which exceeds the "
"maximum of ~p.").
-define(NO_CALL_MSG, "The call to ~p:~p/~p on line ~p is in the no_call list.").
-define(NO_DEBUG_CALL_MSG, "Remove the debug call to ~p:~p/~p on line ~p.").
-define(NO_COMMON_CAVEATS_CALL_MSG,
"The call to ~p:~p/~p on line ~p is in the list of "
"Erlang Efficiency Guide common caveats.").
-define(NO_NESTED_TRY_CATCH, "Nested try...catch block starting at line ~p.").
-define(NO_SUCCESSIVE_MAPS_MSG,
"Found map update after map construction/update at line ~p.").
-define(ATOM_NAMING_CONVENTION_MSG,
"Atom ~p on line ~p does not respect the format "
"defined by the regular expression '~p'.").
-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. "
"Use $\\s, instead.").
-define(NO_AUTHOR_MSG, "Unnecessary author attribute on line ~p").
-define(NO_IMPORT_MSG, "Usage of the import attribute, on line ~p, is discouraged").
-define(NO_CATCH_EXPRESSIONS_MSG,
"Usage of catch expression on line ~p is not recommended").
-define(NO_SINGLE_CLAUSE_CASE_MSG,
"Case statement with a single clause found on line ~p.").
-define(NO_MATCH_IN_CONDITION_MSG,
"Case statement with a match in its condition found on line ~p.").
-define(NUMERIC_FORMAT_MSG,
"Number ~p on line ~p does not respect the format "
"defined by the regular expression '~p'.").
-define(BEHAVIOUR_SPELLING_MSG,
"The behavior/behaviour in line ~p is misspelt, please use the "
"~p spelling.").
-define(PARAM_PATTERN_MATCHING_MSG,
"Variable ~ts, used to match a parameter in line ~p, is placed on "
"the wrong side of the match. It was expected on the ~p side.").
-define(ALWAYS_SHORTCIRCUIT_MSG,
"Non-shortcircuiting operator (~p) found in line ~p. "
"It's recommended to use ~p, instead.").
-define(CONSISTENT_GENERIC_TYPE,
"Found usage of type ~p/0 on line ~p. Please use ~p/0, instead.").
-define(EXPORT_USED_TYPES_MSG,
"Type ~p/~p, defined on line ~p, is used by an exported function but not exported itself").
-define(PRIVATE_DATA_TYPES_MSG,
"Private data type ~p/~p, defined on line ~p, is exported. Either don't export it or make "
"it an opaque type.").
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Default values
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec default(Rule :: atom()) -> DefaultRuleConfig :: term().
default(macro_names) ->
#{regex => "^([A-Z][A-Z_0-9]+)$"};
default(operator_spaces) ->
#{rules =>
[{right, "++"}, {left, "++"}, {right, "="}, {left, "="}, {right, "+"}, {left, "+"},
{right, "-"}, {left, "-"}, {right, "*"}, {left, "*"}, {right, "/"}, {left, "/"},
{right, "=<"}, {left, "=<"}, {right, "<"}, {left, "<"}, {right, ">"}, {left, ">"},
{right, ">="}, {left, ">="}, {right, "=="}, {left, "=="}, {right, "=:="}, {left, "=:="},
{right, "/="}, {left, "/="}, {right, "=/="}, {left, "=/="}, {right, "--"}, {left, "--"},
{right, "=>"}, {left, "=>"}, {right, ":="}, {left, ":="}, {right, "<-"}, {left, "<-"},
{right, "<="}, {left, "<="}, {right, "||"}, {left, "||"}, {right, "|"}, {left, "|"},
{right, "::"}, {left, "::"}, {right, "->"}, {left, "->"}, {right, ","}]};
default(no_space) ->
#{rules => [{right, "("}, {left, ")"}, {left, ","}]};
default(nesting_level) ->
#{level => 4};
default(god_modules) ->
#{limit => 25};
default(function_naming_convention) ->
#{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"};
default(variable_naming_convention) ->
#{regex => "^_?([A-Z][0-9a-zA-Z]*)$"};
default(module_naming_convention) ->
#{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"};
default(dont_repeat_yourself) ->
#{min_complexity => 10};
default(max_module_length) ->
#{max_length => 500,
count_comments => false,
count_whitespace => false};
default(max_anonymous_function_arity) ->
#{max_arity => 5};
default(max_function_arity) ->
#{max_arity => 8};
default(max_function_length) ->
#{max_length => 30,
count_comments => false,
count_whitespace => false};
default(no_call) ->
#{no_call_functions => []};
default(no_debug_call) ->
#{debug_functions =>
[{ct, pal}, {ct, print}, {erlang, display, 1}, {io, format, 1}, {io, format, 2}]};
default(no_common_caveats_call) ->
#{caveat_functions =>
[{timer, send_after, 2},
{timer, send_after, 3},
{timer, send_interval, 2},
{timer, send_interval, 3},
{erlang, size, 1}]};
default(atom_naming_convention) ->
#{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$", enclosed_atoms => ".*"};
%% Not restrictive. Those who want more restrictions can set it like "^[^_]*$"
default(numeric_format) ->
#{regex => ".*",
int_regex => same,
float_regex => same};
default(behaviour_spelling) ->
#{spelling => behaviour};
default(param_pattern_matching) ->
#{side => right};
default(consistent_generic_type) ->
#{preferred_type => term};
default(private_data_types) ->
#{apply_to => [record]};
default(RuleWithEmptyDefault)
when RuleWithEmptyDefault == macro_module_names;
RuleWithEmptyDefault == no_macros;
RuleWithEmptyDefault == no_specs;
RuleWithEmptyDefault == no_types;
RuleWithEmptyDefault == no_block_expressions;
RuleWithEmptyDefault == no_if_expression;
RuleWithEmptyDefault == no_nested_try_catch;
RuleWithEmptyDefault == no_successive_maps;
RuleWithEmptyDefault == invalid_dynamic_call;
RuleWithEmptyDefault == used_ignored_variable;
RuleWithEmptyDefault == no_behavior_info;
RuleWithEmptyDefault == state_record_and_type;
RuleWithEmptyDefault == no_spec_with_records;
RuleWithEmptyDefault == no_throw;
RuleWithEmptyDefault == no_dollar_space;
RuleWithEmptyDefault == no_author;
RuleWithEmptyDefault == no_import;
RuleWithEmptyDefault == no_catch_expressions;
RuleWithEmptyDefault == no_single_clause_case;
RuleWithEmptyDefault == no_match_in_condition;
RuleWithEmptyDefault == always_shortcircuit;
RuleWithEmptyDefault == no_space_after_pound;
RuleWithEmptyDefault == export_used_types;
RuleWithEmptyDefault == consistent_variable_casing ->
#{}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Rules
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-type empty_rule_config() :: #{ignore => [ignorable()]}.
-type ignorable() :: module() | {module(), atom()} | {module(), atom(), arity()}.
-type max_function_length_config() ::
#{ignore => [ignorable()],
max_length => non_neg_integer(),
count_comments => boolean(),
count_whitespace => boolean()}.
-type max_module_length_config() ::
#{ignore => [ignorable()],
count_comments => boolean(),
count_whitespace => boolean(),
max_length => integer()}.
-type function_naming_convention_config() ::
#{ignore => [ignorable()], regex => string()}.
-spec function_naming_convention(elvis_config:config(),
elvis_file:file(),
function_naming_convention_config()) ->
[elvis_result:item()].
function_naming_convention(Config, Target, RuleConfig) ->
Regex = option(regex, RuleConfig, function_naming_convention),
Root = get_root(Config, Target, RuleConfig),
FunctionNames0 = elvis_code:function_names(Root),
errors_for_function_names(Regex, FunctionNames0).
errors_for_function_names(_Regex, []) ->
[];
errors_for_function_names(Regex, [FunctionName | RemainingFuncNames]) ->
FunctionNameStr = unicode:characters_to_list(atom_to_list(FunctionName), unicode),
case re:run(FunctionNameStr, Regex, [unicode]) of
nomatch ->
Msg = ?FUNCTION_NAMING_CONVENTION_MSG,
Info = [FunctionNameStr, Regex],
Result = elvis_result:new(item, Msg, Info, 1),
[Result | errors_for_function_names(Regex, RemainingFuncNames)];
{match, _} ->
errors_for_function_names(Regex, RemainingFuncNames)
end.
-type consistent_variable_casing_config() :: #{ignore => [ignorable()]}.
-spec consistent_variable_casing(elvis_config:config(),
elvis_file:file(),
consistent_variable_casing_config()) ->
[elvis_result:item()].
%% @todo Use maps:groups_from_list/2 when we specify OTP25 as the minimum OTP version
consistent_variable_casing(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Vars = elvis_code:find(fun is_var/1, Root, #{traverse => all, mode => zipper}),
Grouped =
maps:to_list(
lists:foldr(fun(Var, Acc) ->
VarName = canonical_variable_name(Var),
maps:update_with(
string:uppercase(VarName),
fun(Locations) -> [#{name => VarName, var => Var} | Locations] end,
[#{name => VarName, var => Var}],
Acc)
end,
#{},
Vars)),
lists:flatmap(fun check_variable_casing_consistency/1, Grouped).
canonical_variable_name(Var) ->
case atom_to_list(ktn_code:attr(name, Var)) of
[$_ | Rest] ->
Rest;
VarNameStr ->
VarNameStr
end.
check_variable_casing_consistency({_,
[#{name := FirstName, var := FirstVar} | Others]}) ->
case lists:usort([OtherName || #{name := OtherName} <- Others, OtherName /= FirstName]) of
[] ->
[];
OtherNames ->
{Line, _} = ktn_code:attr(location, FirstVar),
Info = [FirstName, Line, OtherNames],
[elvis_result:new(item, ?CONSISTENT_VARIABLE_CASING_MSG, Info, Line)]
end.
-type variable_naming_convention_config() ::
#{ignore => [ignorable()], regex => string()}.
-spec variable_naming_convention(elvis_config:config(),
elvis_file:file(),
variable_naming_convention_config()) ->
[elvis_result:item()].
variable_naming_convention(Config, Target, RuleConfig) ->
Regex = option(regex, RuleConfig, variable_naming_convention),
Root = get_root(Config, Target, RuleConfig),
Vars = elvis_code:find(fun is_var/1, Root, #{traverse => all, mode => zipper}),
check_variables_name(Regex, Vars).
-type macro_names_config() :: #{ignore => [ignorable()], regex => string()}.
-spec macro_names(elvis_config:config(), elvis_file:file(), macro_names_config()) ->
[elvis_result:item()].
macro_names(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Regexp = option(regex, RuleConfig, macro_names),
MacroNodes =
elvis_code:find(fun is_macro_define_node/1, Root, #{traverse => all, mode => node}),
check_macro_names(Regexp, MacroNodes, _ResultsIn = []).
-spec macro_module_names(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
[elvis_result:item()].
macro_module_names(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Calls = elvis_code:find(fun is_call/1, Root),
check_no_macro_calls(Calls).
-spec check_no_macro_calls([ktn_code:tree_node()]) -> [elvis_result:item()].
check_no_macro_calls(Calls) ->
TypeFun =
fun(Call) ->
FunctionSpec = ktn_code:node_attr(function, Call),
ModuleAttr = ktn_code:node_attr(module, FunctionSpec),
FuncAttr = ktn_code:node_attr(function, FunctionSpec),
M = ktn_code:type(ModuleAttr),
MN = ktn_code:attr(name, ModuleAttr),
F = ktn_code:type(FuncAttr),
FN = ktn_code:attr(name, FuncAttr),
#{call => Call,
module_type => M,
func_type => F,
module_name => MN,
func_name => FN}
end,
CallsWithTypes = lists:map(TypeFun, Calls),
MacroInM =
[{?MACRO_AS_MODULE_NAME_MSG, MN, ktn_code:attr(location, Call)}
|| #{module_type := macro,
call := Call,
module_name := MN}
<- CallsWithTypes,
not lists:member(MN, ?MACRO_MODULE_NAMES_EXCEPTIONS)],
MacroInF =
[{?MACRO_AS_FUNCTION_NAME_MSG, FN, ktn_code:attr(location, Call)}
|| #{func_type := macro,
call := Call,
func_name := FN}
<- CallsWithTypes],
ResultFun =
fun({Msg, Subject, {Line, _}}) -> elvis_result:new(item, Msg, [Subject, Line], Line) end,
lists:map(ResultFun, MacroInM ++ MacroInF).
-type no_macros_config() :: #{allow => [atom()], ignore => [ignorable()]}.
-spec no_macros(elvis_config:config(), elvis_file:file(), no_macros_config()) ->
[elvis_result:item()].
no_macros(ElvisConfig, RuleTarget, RuleConfig) ->
TreeRootNode = get_root(ElvisConfig, RuleTarget, RuleConfig),
AllowedMacros = maps:get(allow, RuleConfig, []) ++ eep_predef_macros() ++ logger_macros(),
MacroNodes =
elvis_code:find(fun is_macro_node/1, TreeRootNode, #{traverse => all, mode => node}),
lists:foldl(fun(MacroNode, Acc) ->
Macro = list_to_atom(ktn_code:attr(name, MacroNode)),
case lists:member(Macro, AllowedMacros) of
true ->
Acc;
false ->
{Line, _Col} = ktn_code:attr(location, MacroNode),
[elvis_result:new(item, ?NO_MACROS_MSG, [Macro, Line], Line) | Acc]
end
end,
[],
MacroNodes).
is_macro_node(Node) ->
ktn_code:type(Node) =:= macro.
-type no_types_config() :: #{allow => [atom()], ignore => [ignorable()]}.
-spec no_types(elvis_config:config(), elvis_file:file(), no_types_config()) ->
[elvis_result:item()].
no_types(ElvisConfig, RuleTarget, RuleConfig) ->
TreeRootNode = get_root(ElvisConfig, RuleTarget, RuleConfig),
TypeNodes =
elvis_code:find(fun is_type_attribute/1, TreeRootNode, #{traverse => all, mode => node}),
lists:foldl(fun(TypeNode, Acc) ->
Type = ktn_code:attr(name, TypeNode),
{Line, _Col} = ktn_code:attr(location, TypeNode),
[elvis_result:new(item, ?NO_TYPES_MSG, [Type, Line], Line) | Acc]
end,
[],
TypeNodes).
is_type_attribute(Node) ->
ktn_code:type(Node) =:= type_attr.
-type no_specs_config() :: #{allow => [atom()], ignore => [ignorable()]}.
-spec no_specs(elvis_config:config(), elvis_file:file(), no_specs_config()) ->
[elvis_result:item()].
no_specs(ElvisConfig, RuleTarget, RuleConfig) ->
TreeRootNode = get_root(ElvisConfig, RuleTarget, RuleConfig),
SpecNodes =
elvis_code:find(fun is_spec_attribute/1, TreeRootNode, #{traverse => all, mode => node}),
lists:foldl(fun(SpecNode, Acc) ->
FunctionName = ktn_code:attr(name, SpecNode),
{Line, _Col} = ktn_code:attr(location, SpecNode),
[elvis_result:new(item, ?NO_SPECS_MSG, [FunctionName, Line], Line) | Acc]
end,
[],
SpecNodes).
is_spec_attribute(Node) ->
ktn_code:type(Node) =:= spec.
-type no_block_expressions_config() :: #{ignore => [ignorable()]}.
-spec no_block_expressions(elvis_config:config(),
elvis_file:file(),
no_block_expressions_config()) ->
[elvis_result:item()].
no_block_expressions(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Tokens = ktn_code:attr(tokens, Root),
BeginNodes = lists:filter(fun is_begin_node/1, Tokens),
lists:foldl(fun(BeginNode, Acc) ->
{Line, _Col} = ktn_code:attr(location, BeginNode),
[elvis_result:new(item, ?NO_BLOCK_EXPRESSIONS_MSG, [Line], Line) | Acc]
end,
[],
BeginNodes).
is_begin_node(Node) ->
ktn_code:type(Node) =:= 'begin'.
eep_predef_macros() ->
% From unexported epp:predef_macros/1
['BASE_MODULE',
'BASE_MODULE_STRING',
'BEAM',
'FEATURE_AVAILABLE',
'FEATURE_ENABLED',
'FILE',
'FUNCTION_ARITY',
'FUNCTION_NAME',
'LINE',
'MACHINE',
'MODULE',
'MODULE_STRING',
'OTP_RELEASE'].
logger_macros() ->
% From logger.hrl
['LOG',
'LOG_ALERT',
'LOG_CRITICAL',
'LOG_DEBUG',
'LOG_EMERGENCY',
'LOG_ERROR',
'LOG_INFO',
'LOG_NOTICE',
'LOG_WARNING'].
-type no_space_after_pound_config() :: #{ignore => [ignorable()]}.
-spec no_space_after_pound(elvis_config:config(),
elvis_file:file(),
no_space_after_pound_config()) ->
[elvis_result:item()].
no_space_after_pound(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Tokens = ktn_code:attr(tokens, Root),
TextNodes = lists:filter(fun is_text_node/1, Tokens),
{Src, #{encoding := Encoding}} = elvis_file:src(Target),
Lines = elvis_utils:split_all_lines(Src),
check_spaces(Lines, TextNodes, {right, "#"}, Encoding, {should_not_have, []}).
-type operator_spaces_config() ::
#{ignore => [ignorable()], rules => [{right | left, string()}]}.
-define(PUNCTUATION_SYMBOLS, [',', ';', dot, '->', ':', '::', '|', '||']).
-spec operator_spaces(elvis_config:config(),
elvis_file:file(),
operator_spaces_config()) ->
[elvis_result:item()].
operator_spaces(Config, Target, RuleConfig) ->
Rules = option(rules, RuleConfig, operator_spaces),
{Src, #{encoding := Encoding}} = elvis_file:src(Target),
Root = get_root(Config, Target, RuleConfig),
Zipper = elvis_code:code_zipper(Root),
OpNodes = zipper:filter(fun is_operator_node/1, Zipper),
Tokens = ktn_code:attr(tokens, Root),
PunctuationTokens = lists:filter(fun is_punctuation_token/1, Tokens),
Lines = elvis_utils:split_all_lines(Src),
AllNodes = OpNodes ++ PunctuationTokens,
FlatMap =
fun(Rule) -> check_spaces(Lines, AllNodes, Rule, Encoding, {should_have, []}) end,
lists:flatmap(FlatMap, Rules).
%% @doc Returns true when the node is an operator with more than one operand
-spec is_operator_node(ktn_code:tree_node()) -> boolean().
is_operator_node(Node) ->
ktn_code:type(Node) =:= op andalso length(ktn_code:content(Node)) > 1.
-type no_space_config() ::
#{ignore => [ignorable()], rules => [{right | left, string()}]}.
-spec no_space(elvis_config:config(), elvis_file:file(), no_space_config()) ->
[elvis_result:item()].
no_space(Config, Target, RuleConfig) ->
Rules = option(rules, RuleConfig, no_space),
Root = get_root(Config, Target, RuleConfig),
Tokens = ktn_code:attr(tokens, Root),
TextNodes = lists:filter(fun is_text_node/1, Tokens),
{Src, #{encoding := Encoding}} = elvis_file:src(Target),
Lines = elvis_utils:split_all_lines(Src),
AllSpaceUntilText =
[{Text,
re:compile("^[ ]+"
++ re:replace(Text,
"(\\.|\\[|\\]|\\^|\\$|\\+|\\*|\\?|\\{|\\}|\\(|\\)|\\||\\\\)",
"\\\\\\1",
[{return, list}, global]))}
|| {left, Text} <- Rules],
FlatMap =
fun(Rule) ->
check_spaces(Lines, TextNodes, Rule, Encoding, {should_not_have, AllSpaceUntilText})
end,
lists:flatmap(FlatMap, Rules).
is_text_node(Node) ->
ktn_code:attr(text, Node) =/= "".
%% @doc Returns true when the token is one of the ?PUNCTUATION_SYMBOLS
-spec is_punctuation_token(ktn_code:tree_node()) -> boolean().
is_punctuation_token(Node) ->
Type = ktn_code:type(Node),
lists:member(Type, ?PUNCTUATION_SYMBOLS).
-type nesting_level_config() :: #{ignore => [ignorable()], level => integer()}.
-spec nesting_level(elvis_config:config(), elvis_file:file(), nesting_level_config()) ->
[elvis_result:item()].
nesting_level(Config, Target, RuleConfig) ->
Level = option(level, RuleConfig, nesting_level),
Root = get_root(Config, Target, RuleConfig),
elvis_utils:check_nodes(Root, fun check_nesting_level/2, [Level]).
-type god_modules_config() :: #{ignore => [ignorable()], limit => integer()}.
-spec god_modules(elvis_config:config(), elvis_file:file(), god_modules_config()) ->
[elvis_result:item()].
god_modules(Config, Target, RuleConfig) ->
Limit = option(limit, RuleConfig, god_modules),
Root = get_root(Config, Target, RuleConfig),
Exported = elvis_code:exported_functions(Root),
case length(Exported) of
Count when Count > Limit ->
Msg = ?GOD_MODULES_MSG,
Result = elvis_result:new(item, Msg, [Count], 1),
[Result];
_ ->
[]
end.
-spec no_if_expression(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
[elvis_result:item()].
no_if_expression(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Predicate = fun(Node) -> ktn_code:type(Node) == 'if' end,
ResultFun = result_node_line_fun(?NO_IF_EXPRESSION_MSG),
case elvis_code:find(Predicate, Root) of
[] ->
[];
IfExprs ->
lists:map(ResultFun, IfExprs)
end.
-spec invalid_dynamic_call(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
[elvis_result:item()].
invalid_dynamic_call(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Predicate = fun(Node) -> ktn_code:type(Node) == callback end,
case elvis_code:find(Predicate, Root) of
[] ->
check_invalid_dynamic_calls(Root);
_Callbacks ->
[]
end.
-spec used_ignored_variable(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
[elvis_result:item()].
used_ignored_variable(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
ResultFun = result_node_line_col_fun(?USED_IGNORED_VAR_MSG),
case elvis_code:find(fun is_ignored_var/1, Root, #{mode => zipper}) of
[] ->
[];
UsedIgnoredVars ->
lists:map(ResultFun, UsedIgnoredVars)
end.
-spec no_behavior_info(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
[elvis_result:item()].
no_behavior_info(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Children = ktn_code:content(Root),
FilterFun =
fun(Node) ->
case ktn_code:type(Node) of
function ->
Name = ktn_code:attr(name, Node),
lists:member(Name, [behavior_info, behaviour_info]);
_ ->
false
end
end,
ResultFun = result_node_line_fun(?NO_BEHAVIOR_INFO),
case lists:filter(FilterFun, Children) of
[] ->
[];
BehaviorInfos ->
lists:map(ResultFun, BehaviorInfos)
end.
-type module_naming_convention_config() :: #{ignore => [ignorable()], regex => string()}.
-spec module_naming_convention(elvis_config:config(),
elvis_file:file(),
module_naming_convention_config()) ->
[elvis_result:item()].
module_naming_convention(Config, Target, RuleConfig) ->
Regex = option(regex, RuleConfig, module_naming_convention),
IgnoreModules = option(ignore, RuleConfig, module_naming_convention),
Root = get_root(Config, Target, RuleConfig),
ModuleName = elvis_code:module_name(Root),
case lists:member(ModuleName, IgnoreModules) of
false ->
ModuleNameStr = atom_to_list(ModuleName),
case re:run(ModuleNameStr, Regex) of
nomatch ->
Msg = ?MODULE_NAMING_CONVENTION_MSG,
Info = [ModuleNameStr, Regex],
Result = elvis_result:new(item, Msg, Info, 1),
[Result];
{match, _} ->
[]
end;
true ->
[]
end.
-spec state_record_and_type(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
[elvis_result:item()].
state_record_and_type(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
case is_otp_module(Root) of
true ->
case {has_state_record(Root), has_state_type(Root)} of
{true, true} ->
[];
{false, _} ->
Msg = ?STATE_RECORD_MISSING_MSG,
Result = elvis_result:new(item, Msg, [], 1),
[Result];
{true, false} ->
Msg = ?STATE_TYPE_MISSING_MSG,
Result = elvis_result:new(item, Msg, [], 1),
[Result]
end;
false ->
[]
end.
-spec no_spec_with_records(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
[elvis_result:item()].
no_spec_with_records(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
case elvis_code:find(fun spec_includes_record/1, Root) of
[] ->
[];
SpecNodes ->
ResultFun = result_node_line_fun(?NO_SPEC_WITH_RECORDS),
lists:map(ResultFun, SpecNodes)
end.
-type dont_repeat_yourself_config() ::
#{ignore => [ignorable()], min_complexity => non_neg_integer()}.
-spec dont_repeat_yourself(elvis_config:config(),
elvis_file:file(),
dont_repeat_yourself_config()) ->
[elvis_result:item()].
dont_repeat_yourself(Config, Target, RuleConfig) ->
MinComplexity = option(min_complexity, RuleConfig, dont_repeat_yourself),
Root = get_root(Config, Target, RuleConfig),
Nodes = find_repeated_nodes(Root, MinComplexity),
LocationCat =
fun ({Line, Col}, "") ->
io_lib:format("(~p, ~p)", [Line, Col]);
({Line, Col}, Str) ->
io_lib:format("~s, (~p, ~p)", [Str, Line, Col])
end,
ResultFun =
fun([{Line, _} | _] = Locations) ->
LocationsStr = lists:foldl(LocationCat, "", Locations),
Info = [LocationsStr],
Msg = ?DONT_REPEAT_YOURSELF,
elvis_result:new(item, Msg, Info, Line)
end,
lists:map(ResultFun, Nodes).
-spec max_module_length(elvis_config:config(),
elvis_file:file(),
max_module_length_config()) ->
[elvis_result:item()].
max_module_length(Config, Target, RuleConfig) ->
MaxLength = option(max_length, RuleConfig, max_module_length),
CountComments = option(count_comments, RuleConfig, max_module_length),
CountWhitespace = option(count_whitespace, RuleConfig, max_module_length),
Root = get_root(Config, Target, RuleConfig),
{Src, _} = elvis_file:src(Target),
ModuleName = elvis_code:module_name(Root),
FilterFun =
fun(Line) ->
(CountComments orelse not line_is_comment(Line))
andalso (CountWhitespace orelse not line_is_whitespace(Line))
end,
Lines =
case elvis_utils:split_all_lines(Src, [trim]) of
Ls when CountComments andalso CountWhitespace ->
Ls;
Ls ->
lists:filter(FilterFun, Ls)
end,
case length(Lines) of
L when L > MaxLength ->
Info = [ModuleName, L, MaxLength],
Msg = ?MAX_MODULE_LENGTH,
Result = elvis_result:new(item, Msg, Info, 1),
[Result];
_ ->
[]
end.
-type max_anonymous_function_arity_config() :: #{max_arity => non_neg_integer()}.
-spec max_anonymous_function_arity(elvis_config:config(),
elvis_file:file(),
max_anonymous_function_arity_config()) ->
[elvis_result:item()].
max_anonymous_function_arity(Config, Target, RuleConfig) ->
MaxArity = option(max_arity, RuleConfig, max_anonymous_function_arity),
Root = get_root(Config, Target, RuleConfig),
IsClause = fun(Node) -> ktn_code:type(Node) == clause end,
IsFun =
fun(Node) ->
%% Not having clauses means it's something like fun mod:f/10 and we don't want
%% this rule to raise warnings for those. max_function_arity should take care of them.
ktn_code:type(Node) == 'fun' andalso [] /= elvis_code:find(IsClause, Node)
end,
Funs = elvis_code:find(IsFun, Root),
lists:filtermap(fun(Fun) ->
[FirstClause | _] = elvis_code:find(IsClause, Fun),
case length(ktn_code:node_attr(pattern, FirstClause)) of
Arity when Arity =< MaxArity ->
false;
Arity ->
{Line, _} = ktn_code:attr(location, Fun),
Info = [Line, Arity, MaxArity],
{true,
elvis_result:new(item,
?MAX_ANONYMOUS_FUNCTION_ARITY_MSG,
Info,
Line)}
end
end,
Funs).
-type max_function_arity_config() :: #{max_arity => non_neg_integer()}.
-spec max_function_arity(elvis_config:config(),
elvis_file:file(),
max_function_arity_config()) ->
[elvis_result:item()].
max_function_arity(Config, Target, RuleConfig) ->
MaxArity = option(max_arity, RuleConfig, max_function_arity),
Root = get_root(Config, Target, RuleConfig),
IsFunction = fun(Node) -> ktn_code:type(Node) == function end,
Functions = elvis_code:find(IsFunction, Root),
lists:filtermap(fun(Function) ->
case ktn_code:attr(arity, Function) of
Arity when Arity =< MaxArity ->
false;
Arity ->
Name = ktn_code:attr(name, Function),
{Line, _} = ktn_code:attr(location, Function),
Info = [Name, Arity, MaxArity],
{true, elvis_result:new(item, ?MAX_FUNCTION_ARITY_MSG, Info, Line)}
end
end,
Functions).
-spec max_function_length(elvis_config:config(),
elvis_file:file(),
max_function_length_config()) ->
[elvis_result:item()].
max_function_length(Config, Target, RuleConfig) ->
MaxLength = option(max_length, RuleConfig, max_function_length),
CountComments = option(count_comments, RuleConfig, max_function_length),
CountWhitespace = option(count_whitespace, RuleConfig, max_function_length),
Root = get_root(Config, Target, RuleConfig),
{Src, _} = elvis_file:src(Target),
Lines = elvis_utils:split_all_lines(Src, [trim]),
IsFunction = fun(Node) -> ktn_code:type(Node) == function end,
Functions0 = elvis_code:find(IsFunction, Root),
FilterFun =
fun(Line) ->
(CountComments orelse not line_is_comment(Line))
andalso (CountWhitespace orelse not line_is_whitespace(Line))
end,
PairFun =
fun(FunctionNode) ->
Name = ktn_code:attr(name, FunctionNode),
Arity = ktn_code:attr(arity, FunctionNode),
{Min, Max} = node_line_limits(FunctionNode),
FunLines = lists:sublist(Lines, Min, Max - Min + 1),
FilteredLines = lists:filter(FilterFun, FunLines),
L = length(FilteredLines),
{Name, Arity, Min, L}
end,
FunLenInfos = lists:map(PairFun, Functions0),
MaxLengthPred = fun({_, _, _, L}) -> L > MaxLength end,
FunLenMaxPairs = lists:filter(MaxLengthPred, FunLenInfos),
ResultFun =
fun({Name, Arity, StartPos, L}) ->
Info = [Name, Arity, L, MaxLength],
Msg = ?MAX_FUNCTION_LENGTH,
elvis_result:new(item, Msg, Info, StartPos)
end,
lists:map(ResultFun, FunLenMaxPairs).
-type function_spec() :: {module(), atom(), arity()} | {module(), atom()}.
-type no_call_config() ::
#{ignore => [ignorable()], no_call_functions => [function_spec()]}.
-spec no_call(elvis_config:config(), elvis_file:file(), no_call_config()) ->
[elvis_result:item()].
no_call(Config, Target, RuleConfig) ->
DefaultFns = option(no_call_functions, RuleConfig, no_call),
no_call_common(Config, Target, DefaultFns, ?NO_CALL_MSG, RuleConfig).
-type no_debug_call_config() ::
#{ignore => [ignorable()], debug_functions => [function_spec()]}.
-spec no_debug_call(elvis_config:config(), elvis_file:file(), no_debug_call_config()) ->
[elvis_result:item()].
no_debug_call(Config, Target, RuleConfig) ->
DefaultFns = option(debug_functions, RuleConfig, no_debug_call),
no_call_common(Config, Target, DefaultFns, ?NO_DEBUG_CALL_MSG, RuleConfig).
-type no_common_caveats_call_config() ::
#{ignore => [ignorable()], caveat_functions => [function_spec()]}.
-spec no_common_caveats_call(elvis_config:config(),
elvis_file:file(),
no_common_caveats_call_config()) ->
[elvis_result:item()].
no_common_caveats_call(Config, Target, RuleConfig) ->
DefaultFns = option(caveat_functions, RuleConfig, no_common_caveats_call),
no_call_common(Config, Target, DefaultFns, ?NO_COMMON_CAVEATS_CALL_MSG, RuleConfig).
-spec node_line_limits(ktn_code:tree_node()) -> {Min :: integer(), Max :: integer()}.
node_line_limits(FunctionNode) ->
Zipper = elvis_code:code_zipper(FunctionNode),
LineFun =
fun(N) ->
{L, _} = ktn_code:attr(location, N),
L
end,
% The first number in `lineNums' list is the location of the first
% line of the function. That's why we use it for the `Min' value.
LineNums = zipper:map(LineFun, Zipper),
% Last function's line
Max = lists:max(LineNums),
% If you use `lists:min/1' here, you will get weird results when using
% macros because most of the time macros are defined at the beginning of
% the module, but your function's first line could be in the middle or
% even at the end of the module.
[Min | _] = LineNums, % Min = first function's line
{Min, Max}.
-spec no_nested_try_catch(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
[elvis_result:item()].
no_nested_try_catch(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Predicate = fun(Node) -> ktn_code:type(Node) == 'try' end,
ResultFun = result_node_line_fun(?NO_NESTED_TRY_CATCH),
case elvis_code:find(Predicate, Root) of
[] ->
[];
TryExprs ->
lists:flatmap(fun(TryExp) -> check_nested_try_catchs(ResultFun, TryExp) end, TryExprs)
end.
-spec no_successive_maps(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
[elvis_result:item()].
no_successive_maps(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Predicate = fun(Node) -> ktn_code:type(Node) == map end,
ResultFun = result_node_line_fun(?NO_SUCCESSIVE_MAPS_MSG),
case elvis_code:find(Predicate, Root) of
[] ->
[];
MapExprs ->
lists:flatmap(fun(MapExp) -> check_successive_maps(ResultFun, MapExp) end, MapExprs)
end.
-type atom_naming_convention_config() ::
#{ignore => [ignorable()],
regex => string(),
enclosed_atoms => same | string()}.
-spec atom_naming_convention(elvis_config:config(),
elvis_file:file(),
atom_naming_convention_config()) ->
[elvis_result:item()].
atom_naming_convention(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Regex = option(regex, RuleConfig, atom_naming_convention),
RegexEnclosed =
specific_or_default(option(enclosed_atoms, RuleConfig, atom_naming_convention), Regex),
AtomNodes = elvis_code:find(fun is_atom_node/1, Root, #{traverse => all, mode => node}),
check_atom_names(Regex, RegexEnclosed, AtomNodes, []).
-spec no_throw(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
[elvis_result:item()].
no_throw(Config, Target, RuleConfig) ->
Zipper =
fun(Node) -> lists:any(fun(T) -> is_call(Node, T) end, [{throw, 1}, {erlang, throw, 1}])
end,
Root = get_root(Config, Target, RuleConfig),
ThrowNodes = elvis_code:find(Zipper, Root),
lists:foldl(fun(ThrowNode, AccIn) ->
{Line, _} = ktn_code:attr(location, ThrowNode),
[elvis_result:new(item, ?NO_THROW_MSG, [Line]) | AccIn]
end,
[],
ThrowNodes).
-spec no_dollar_space(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
[elvis_result:item()].
no_dollar_space(Config, Target, RuleConfig) ->
IsDollarSpace =
fun(Node) -> ktn_code:type(Node) == char andalso ktn_code:attr(text, Node) == "$ " end,
Root = get_root(Config, Target, RuleConfig),
Opts = #{mode => node, traverse => all},
DollarSpaceNodes = elvis_code:find(IsDollarSpace, Root, Opts),
lists:map(fun(ThrowNode) ->
{Line, _} = ktn_code:attr(location, ThrowNode),
elvis_result:new(item, ?NO_DOLLAR_SPACE_MSG, [Line])
end,
DollarSpaceNodes).
-type no_author_config() :: #{ignore => [ignorable()]}.
-spec no_author(elvis_config:config(), elvis_file:file(), no_author_config()) ->
[elvis_result:item()].
no_author(Config, Target, RuleConfig) ->
no_attribute(author, ?NO_AUTHOR_MSG, Config, Target, RuleConfig).
-type no_import_config() :: #{ignore => [ignorable()]}.
-spec no_import(elvis_config:config(), elvis_file:file(), no_import_config()) ->
[elvis_result:item()].
no_import(Config, Target, RuleConfig) ->
no_attribute(import, ?NO_IMPORT_MSG, Config, Target, RuleConfig).
no_attribute(Attribute, Msg, Config, Target, RuleConfig) ->
Zipper = fun(Node) -> ktn_code:type(Node) =:= Attribute end,
Root = get_root(Config, Target, RuleConfig),
Nodes = elvis_code:find(Zipper, Root),
lists:map(fun(Node) ->
{Line, _} = ktn_code:attr(location, Node),
elvis_result:new(item, Msg, [Line], Line)
end,
Nodes).
-type no_catch_expressions_config() :: #{ignore => [ignorable()]}.
-spec no_catch_expressions(elvis_config:config(),
elvis_file:file(),
no_catch_expressions_config()) ->
[elvis_result:item()].
no_catch_expressions(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
CatchNodes = elvis_code:find(fun is_catch_node/1, Root),
lists:foldl(fun(CatchNode, Acc) ->
{Line, _Col} = ktn_code:attr(location, CatchNode),
[elvis_result:new(item, ?NO_CATCH_EXPRESSIONS_MSG, [Line]) | Acc]
end,
[],
CatchNodes).
is_catch_node(Node) ->
ktn_code:type(Node) =:= 'catch'.
-type no_single_clause_case_config() :: #{ignore => [ignorable()]}.
-spec no_single_clause_case(elvis_config:config(),
elvis_file:file(),
no_single_clause_case_config()) ->
[elvis_result:item()].
no_single_clause_case(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
CaseNodes = elvis_code:find(fun is_single_clause_case_statement/1, Root),
lists:map(fun(CaseNode) ->
{Line, _Col} = ktn_code:attr(location, CaseNode),
elvis_result:new(item, ?NO_SINGLE_CLAUSE_CASE_MSG, [Line], Line)
end,
CaseNodes).
is_single_clause_case_statement(Node) ->
ktn_code:type(Node) == 'case'
andalso length([Clause
|| SubNode <- ktn_code:content(Node),
ktn_code:type(SubNode) == case_clauses,
Clause <- ktn_code:content(SubNode)])
== 1.
-type no_match_in_condition_config() :: #{ignore => [ignorable()]}.
-spec no_match_in_condition(elvis_config:config(),
elvis_file:file(),
no_match_in_condition_config()) ->
[elvis_result:item()].
no_match_in_condition(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
CaseNodes = elvis_code:find(fun is_match_in_condition/1, Root),
lists:map(fun(CaseNode) ->
{Line, _Col} = ktn_code:attr(location, CaseNode),
elvis_result:new(item, ?NO_MATCH_IN_CONDITION_MSG, [Line], Line)
end,
CaseNodes).
is_match_in_condition(Node) ->
ktn_code:type(Node) == case_expr andalso [] =/= elvis_code:find(fun is_match/1, Node).
is_match(Node) ->
ktn_code:type(Node) == match orelse ktn_code:type(Node) == maybe_match.
-type numeric_format_config() ::
#{ignore => [ignorable()],
regex => string(),
int_regex => same | string(),
float_regex => same | string()}.
-spec numeric_format(elvis_config:config(), elvis_file:file(), numeric_format_config()) ->
[elvis_result:item()].
numeric_format(Config, Target, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Regex = option(regex, RuleConfig, numeric_format),
IntRegex = specific_or_default(option(int_regex, RuleConfig, numeric_format), Regex),
FloatRegex = specific_or_default(option(float_regex, RuleConfig, numeric_format), Regex),
IntNodes = elvis_code:find(fun is_integer_node/1, Root, #{traverse => all, mode => node}),
FloatNodes = elvis_code:find(fun is_float_node/1, Root, #{traverse => all, mode => node}),
check_numeric_format(IntRegex,
IntNodes,
check_numeric_format(FloatRegex, FloatNodes, [])).
-type behaviour_spelling_config() ::
#{ignore => [ignorable()], spelling => behaviour | behavior}.
-spec behaviour_spelling(elvis_config:config(),
elvis_file:file(),
behaviour_spelling_config()) ->
[elvis_result:item()].
behaviour_spelling(Config, Target, RuleConfig) ->
Spelling = option(spelling, RuleConfig, behaviour_spelling),
Root = get_root(Config, Target, RuleConfig),
Predicate =
fun(Node) ->
NodeType = ktn_code:type(Node),
lists:member(NodeType, [behaviour, behavior]) andalso NodeType /= Spelling
end,
case elvis_code:find(Predicate, Root) of
[] ->
[];
InconsistentBehaviorNodes ->
ResultFun =
fun(Node) ->
{Line, _} = ktn_code:attr(location, Node),
Info = [Line, Spelling],
elvis_result:new(item, ?BEHAVIOUR_SPELLING_MSG, Info, Line)
end,
lists:map(ResultFun, InconsistentBehaviorNodes)
end.
-type param_pattern_matching_config() :: #{ignore => [ignorable()], side => left | right}.
-spec param_pattern_matching(elvis_config:config(),
elvis_file:file(),
param_pattern_matching_config()) ->
[elvis_result:item()].
param_pattern_matching(Config, Target, RuleConfig) ->
Side = option(side, RuleConfig, param_pattern_matching),
Root = get_root(Config, Target, RuleConfig),
FunctionClausePatterns =
lists:flatmap(fun(Clause) -> ktn_code:node_attr(pattern, Clause) end,
elvis_code:find(fun is_function_clause/1,
Root,
#{mode => zipper, traverse => all})),
MatchesInFunctionClauses =
lists:filter(fun(Pattern) -> ktn_code:type(Pattern) == match end, FunctionClausePatterns),
lists:filtermap(fun(Match) ->
case lists:map(fun ktn_code:type/1, ktn_code:content(Match)) of
[var, var] ->
false;
[var, _] when Side == right ->
{Line, _} = ktn_code:attr(location, Match),
[Var, _] = ktn_code:content(Match),
VarName = ktn_code:attr(name, Var),
Info = [VarName, Line, Side],
{true,
elvis_result:new(item, ?PARAM_PATTERN_MATCHING_MSG, Info, Line)};
[_, var] when Side == left ->
{Line, _} = ktn_code:attr(location, Match),
[_, Var] = ktn_code:content(Match),
VarName = ktn_code:attr(name, Var),
Info = [VarName, Line, Side],
{true,
elvis_result:new(item, ?PARAM_PATTERN_MATCHING_MSG, Info, Line)};
_ ->
false
end
end,
MatchesInFunctionClauses).
is_function_clause(Zipper) ->
is_clause(Zipper) andalso is_function_or_fun(zipper:up(Zipper)).
is_clause(Zipper) ->
ktn_code:type(
zipper:node(Zipper))
== clause.
is_function_or_fun(Zipper) ->
lists:member(
ktn_code:type(
zipper:node(Zipper)),
[function, 'fun']).
-spec consistent_generic_type(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
[elvis_result:item()].
consistent_generic_type(Config, Target, RuleConfig) ->
TypePreference = option(preferred_type, RuleConfig, consistent_generic_type),
Root = get_root(Config, Target, RuleConfig),
Predicate = consistent_generic_type_predicate(TypePreference),
case elvis_code:find(Predicate, Root, #{traverse => all, mode => node}) of
[] ->
[];
InconsistentTypeNodes ->
ResultFun = consistent_generic_type_result(TypePreference),
lists:map(ResultFun, InconsistentTypeNodes)
end.
-spec always_shortcircuit(elvis_config:config(),
elvis_file:file(),
empty_rule_config()) ->
[elvis_result:item()].
always_shortcircuit(Config, Target, RuleConfig) ->
Operators = #{'and' => 'andalso', 'or' => 'orelse'},
Root = get_root(Config, Target, RuleConfig),
Predicate =
fun(Node) ->
is_operator_node(Node)
andalso lists:member(
ktn_code:attr(operation, Node), maps:keys(Operators))
end,
case elvis_code:find(Predicate, Root, #{traverse => all}) of
[] ->
[];
BadOperators ->
ResultFun =
fun(Node) ->
{Line, _} = ktn_code:attr(location, Node),
BadOperator = ktn_code:attr(operation, Node),
GoodOperator = maps:get(BadOperator, Operators),
Info = [BadOperator, Line, GoodOperator],
elvis_result:new(item, ?ALWAYS_SHORTCIRCUIT_MSG, Info, Line)
end,
lists:map(ResultFun, BadOperators)
end.
-spec export_used_types(elvis_config:config(), elvis_file:file(), empty_rule_config()) ->
[elvis_result:item()].
export_used_types(Config, Target, RuleConfig) ->
TreeRootNode = get_root(Config, Target, RuleConfig),
ExportedFunctions = elvis_code:exported_functions(TreeRootNode),
ExportedTypes = elvis_code:exported_types(TreeRootNode),
SpecNodes =
elvis_code:find(fun is_spec_attribute/1, TreeRootNode, #{traverse => all, mode => node}),
ExportedSpecs =
lists:filter(fun(#{attrs := #{arity := Arity, name := Name}}) ->
lists:member({Name, Arity}, ExportedFunctions)
end,
SpecNodes),
UsedTypes =
lists:usort(
lists:flatmap(fun(Spec) ->
Types =
elvis_code:find(fun(Node) -> ktn_code:type(Node) =:= user_type end,
Spec,
#{mode => node, traverse => all}),
% yes, on a -type line, the arity is based on `args`, but on
% a -spec line, it's based on `content`
[{Name, length(Vars)}
|| #{attrs := #{name := Name}, content := Vars} <- Types]
end,
ExportedSpecs)),
UnexportedUsedTypes = lists:subtract(UsedTypes, ExportedTypes),
LineNumbers = map_type_declarations_to_line_numbers(TreeRootNode),
% Report
lists:map(fun({Name, Arity} = Info) ->
Line = maps:get(Info, LineNumbers, unknown),
elvis_result:new(item, ?EXPORT_USED_TYPES_MSG, [Name, Arity, Line], Line)
end,
UnexportedUsedTypes).
get_type_of_type(#{type := type_attr,
node_attrs := #{type := #{attrs := #{name := TypeOfType}}}}) ->
TypeOfType;
get_type_of_type(_) ->
undefined.
-type data_type() :: record | map | tuple.
-type private_data_type_config() :: #{apply_to => [data_type()]}.
-spec private_data_types(elvis_config:config(),
elvis_file:file(),
private_data_type_config()) ->
[elvis_result:item()].
private_data_types(Config, Target, RuleConfig) ->
TypesToCheck = option(apply_to, RuleConfig, private_data_types),
TreeRootNode = get_root(Config, Target, RuleConfig),
ExportedTypes = elvis_code:exported_types(TreeRootNode),
LineNumbers = map_type_declarations_to_line_numbers(TreeRootNode),
PublicDataTypes = public_data_types(TypesToCheck, TreeRootNode, ExportedTypes),
lists:map(fun({Name, Arity} = Info) ->
Line = maps:get(Info, LineNumbers, unknown),
elvis_result:new(item, ?PRIVATE_DATA_TYPES_MSG, [Name, Arity, Line], Line)
end,
PublicDataTypes).
public_data_types(TypesToCheck, TreeRootNode, ExportedTypes) ->
Fun = fun(Node) -> lists:member(get_type_of_type(Node), TypesToCheck) end,
Types =
[name_arity_from_type_line(Node)
|| Node <- elvis_code:find(Fun, TreeRootNode, #{traverse => all, mode => node})],
lists:filter(fun({Name, Arity}) -> lists:member({Name, Arity}, ExportedTypes) end, Types).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @private
-spec name_arity_from_type_line(ktn_code:tree_node()) -> {atom(), integer()}.
name_arity_from_type_line(#{attrs := #{name := Name}, node_attrs := #{args := Args}}) ->
{Name, length(Args)}.
%% @private
-spec map_type_declarations_to_line_numbers(ktn_code:tree_node()) ->
#{{atom(), number()} => number()}.
map_type_declarations_to_line_numbers(TreeRootNode) ->
AllTypes =
elvis_code:find(fun is_type_attribute/1, TreeRootNode, #{traverse => all, mode => node}),
lists:foldl(fun (#{attrs := #{location := {Line, _}, name := Name},
node_attrs := #{args := Args}},
Acc) ->
maps:put({Name, length(Args)}, Line, Acc);
(_, Acc) ->
Acc
end,
#{},
AllTypes).
%% @private
specific_or_default(same, Regex) ->
Regex;
specific_or_default(RegexEnclosed, _Regex) ->
RegexEnclosed.
%% @private
check_numeric_format(_Regex, [], Acc) ->
lists:reverse(Acc);
check_numeric_format(Regex, [NumNode | RemainingNumNodes], AccIn) ->
AccOut =
case ktn_code:attr(text, NumNode) of
undefined ->
AccIn;
Number ->
case re:run(Number, Regex) of
nomatch ->
{Line, _} = ktn_code:attr(location, NumNode),
Result =
elvis_result:new(item,
?NUMERIC_FORMAT_MSG,
[Number, Line, Regex],
Line),
[Result | AccIn];
{match, _} ->
AccIn
end
end,
check_numeric_format(Regex, RemainingNumNodes, AccOut).
%% @private
is_integer_node(Node) ->
ktn_code:type(Node) =:= integer.
%% @private
is_float_node(Node) ->
ktn_code:type(Node) =:= float.
%% @private
is_exception_class(error) ->
true;
is_exception_class(exit) ->
true;
is_exception_class(throw) ->
true;
is_exception_class(_) ->
false.
%% @private
check_atom_names(_Regex, _RegexEnclosed, [] = _AtomNodes, Acc) ->
Acc;
check_atom_names(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_class(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 ->
Msg = ?ATOM_NAMING_CONVENTION_MSG,
{Line, _} = ktn_code:attr(location, AtomNode),
Info = [AtomName0, Line, Regex],
Result = elvis_result:new(item, Msg, Info, Line),
AccIn ++ [Result];
nomatch when IsEnclosed ->
Msg = ?ATOM_NAMING_CONVENTION_MSG,
{Line, _} = ktn_code:attr(location, AtomNode),
Info = [AtomName0, Line, RegexEnclosed],
Result = elvis_result:new(item, Msg, Info, Line),
AccIn ++ [Result];
{match, _Captured} ->
AccIn
end,
check_atom_names(Regex, RegexEnclosed, RemainingAtomNodes, AccOut).
%% @private
string_strip_enclosed([$' | Rest]) ->
[$' | Reversed] = lists:reverse(Rest),
IsEnclosed = true,
EnclosedAtomName = lists:reverse(Reversed),
{IsEnclosed, EnclosedAtomName};
string_strip_enclosed(NonEnclosedAtomName) ->
IsEnclosed = false,
{IsEnclosed, NonEnclosedAtomName}.
%% @private
re_compile_for_atom_type(false = _IsEnclosed, Regex, _RegexEnclosed) ->
{ok, RE} = re:compile(Regex, [unicode]),
RE;
re_compile_for_atom_type(true = _IsEnclosed, _Regex, RegexEnclosed) ->
{ok, RE} = re:compile(RegexEnclosed, [unicode]),
RE.
%% @private
is_atom_node(MaybeAtom) ->
ktn_code:type(MaybeAtom) =:= atom.
%% Variables name
%% @private
check_variables_name(_Regex, []) ->
[];
check_variables_name(Regex, [Variable | RemainingVars]) ->
VariableNameStr = atom_to_list(ktn_code:attr(name, Variable)),
case re:run(VariableNameStr, Regex) of
nomatch when VariableNameStr == "_" ->
check_variables_name(Regex, RemainingVars);
nomatch ->
Msg = ?VARIABLE_NAMING_CONVENTION_MSG,
{Line, _} = ktn_code:attr(location, Variable),
Info = [VariableNameStr, Line, Regex],
Result = elvis_result:new(item, Msg, Info, Line),
[Result | check_variables_name(Regex, RemainingVars)];
{match, _} ->
check_variables_name(Regex, RemainingVars)
end.
%% Result building
%% @private
result_node_line_fun(Msg) ->
fun(Node) ->
{Line, _} = ktn_code:attr(location, Node),
Info = [Line],
elvis_result:new(item, Msg, Info, Line)
end.
%% @private
result_node_line_col_fun(Msg) ->
fun(Node) ->
{Line, Col} = ktn_code:attr(location, Node),
Info = [Line, Col],
elvis_result:new(item, Msg, Info, Line)
end.
%%% Rule checking
%% Line Length
%% @private
-spec line_is_comment(binary()) -> boolean().
line_is_comment(Line) ->
case re:run(Line, "^[ \t]*%") of
nomatch ->
false;
{match, _} ->
true
end.
%% @private
-spec line_is_whitespace(binary()) -> boolean().
line_is_whitespace(Line) ->
case re:run(Line, "^[ \t]*$") of
nomatch ->
false;
{match, _} ->
true
end.
%% Macro Names
%% @private
check_macro_names(_Regexp, [] = _MacroNodes, ResultsIn) ->
ResultsIn;
check_macro_names(Regexp, [MacroNode | RemainingMacroNodes], ResultsIn) ->
{ok, RE} = re:compile(Regexp, [unicode]),
{MacroNameStripped0, MacroNameOriginal} = macro_name_from_node(MacroNode),
MacroNameStripped = unicode:characters_to_list(MacroNameStripped0, unicode),
ResultsOut =
case re:run(MacroNameStripped, RE) of
nomatch ->
Msg = ?INVALID_MACRO_NAME_REGEX_MSG,
{Line, _} = ktn_code:attr(location, MacroNode),
Info = [MacroNameOriginal, Line, Regexp],
Result = elvis_result:new(item, Msg, Info, Line),
ResultsIn ++ [Result];
{match, _Captured} ->
ResultsIn
end,
check_macro_names(Regexp, RemainingMacroNodes, ResultsOut).
-dialyzer({no_match, is_macro_define_node/1}).
%% @private
is_macro_define_node(MaybeMacro) ->
case ktn_code:type(MaybeMacro) of
{atom, [_, _], define} ->
true;
_ ->
false
end.
%% @private
macro_name_from_node(MacroNode) ->
MacroNodeValue = ktn_code:attr(value, MacroNode),
MacroAsAtom = macro_as_atom(false, [call, var, atom], MacroNodeValue),
MacroNameOriginal = atom_to_list(MacroAsAtom),
MacroNameStripped = string:strip(MacroNameOriginal, both, $'),
{MacroNameStripped, MacroNameOriginal}.
%% @private
macro_as_atom({var, _Text, MacroAsAtom}, _Types, _MacroNodeValue) ->
MacroAsAtom;
macro_as_atom({atom, _Text, MacroAsAtom}, _Types, _MacroNodeValue) ->
MacroAsAtom;
macro_as_atom({call, _CallText, {Type, _AtomText, MacroAsAtom}, _VarArg},
_Types,
_MacroNodeValue)
when Type =:= var orelse Type =:= atom ->
MacroAsAtom;
macro_as_atom(false, [Type | OtherTypes], MacroNodeValue) ->
macro_as_atom(lists:keyfind(Type, _N = 1, MacroNodeValue), OtherTypes, MacroNodeValue).
%% Operator (and Text) Spaces
%% @private
-spec check_spaces(Lines :: [binary()],
Nodes :: [ktn_code:tree_node()],
Rule :: {right | left, string()},
Encoding :: latin1 | utf8,
How :: {should_have, []} | {should_not_have, [{string(), {ok, _}}]}) ->
[elvis_result:item()].
% _ is re:mp()
check_spaces(Lines, UnfilteredNodes, {Position, Text}, Encoding, {How0, _} = How) ->
FilterFun = fun(Node) -> ktn_code:attr(text, Node) =:= Text end,
Nodes = lists:filter(FilterFun, UnfilteredNodes),
SpaceChar = $\s,
FlatFun =
fun(Node) ->
Location = ktn_code:attr(location, Node),
case character_at_location(Position, Lines, Text, Location, Encoding, How) of
Char when Char =:= SpaceChar andalso How0 =:= should_have ->
[];
Char when Char =/= SpaceChar andalso How0 =:= should_not_have ->
[];
_ when How0 =:= should_have ->
Msg = ?MISSING_SPACE_MSG,
{Line, _Col} = Location,
Info = [Position, Text, Line],
Result = elvis_result:new(item, Msg, Info, Line),
[Result];
_ when How0 =:= should_not_have ->
Msg = ?UNEXPECTED_SPACE_MSG,
{Line, _Col} = Location,
Info = [Position, Text, Line],
Result = elvis_result:new(item, Msg, Info, Line),
[Result]
end
end,
lists:flatmap(FlatFun, Nodes).
%% @private
maybe_run_regex(undefined = _Regex, _Line) ->
false;
maybe_run_regex({ok, Regex}, Line) ->
re:run(Line, Regex).
%% @private
-spec character_at_location(Position :: atom(),
Lines :: [binary()],
Text :: string(),
Location :: {integer(), integer()},
Encoding :: latin1 | utf8,
How :: {should_have, []} | {should_not_have, [{string(), {ok, _}}]}) ->
char().
% _ is re:mp()
character_at_location(Position,
Lines,
Text,
{LineNo, Col},
Encoding,
{How, TextRegexes}) ->
Line = lists:nth(LineNo, Lines),
TextRegex =
case TextRegexes of
[] ->
false;
_ ->
maybe_run_regex(proplists:get_value(Text, TextRegexes), Line)
end,
TextLineStr = unicode:characters_to_list(Line, Encoding),
ColToCheck =
case Position of
left ->
Col - 1;
right ->
Col + length(Text)
end,
% If ColToCheck is greater than the length of TextLineStr variable, it
% means the end of line was reached so return " " (or "") to make the check pass,
% otherwise return the character at the given column.
% NOTE: text below only applies when the given Position is equal to `right`,
% or Position is equal to `left` and Col is 1.
SpaceChar = $\s,
case ColToCheck =:= 0 orelse {Position, ColToCheck > length(TextLineStr)} of
true when How =:= should_have ->
SpaceChar;
true when How =:= should_not_have ->
"";
{right, true} when How =:= should_have ->
SpaceChar;
{right, true} when How =:= should_not_have ->
"";
_ ->
case How of
should_have ->
lists:nth(ColToCheck, TextLineStr);
should_not_have when TextRegex =:= false orelse TextRegex =:= nomatch ->
lists:nth(ColToCheck, TextLineStr);
_ ->
""
end
end.
%% Nesting Level
%% @private
-spec check_nesting_level(ktn_code:tree_node(), [integer()]) -> [elvis_result:item()].
check_nesting_level(ParentNode, [MaxLevel]) ->
case elvis_code:past_nesting_limit(ParentNode, MaxLevel) of
[] ->
[];
NestedNodes ->
Msg = ?NESTING_LEVEL_MSG,
Fun = fun(Node) ->
{Line, Col} = ktn_code:attr(location, Node),
Info = [Line, Col, MaxLevel],
elvis_result:new(item, Msg, Info, Line)
end,
lists:map(Fun, NestedNodes)
end.
%% Invalid Dynamic Calls
%% @private
-spec check_invalid_dynamic_calls(ktn_code:tree_node()) -> [elvis_result:item()].
check_invalid_dynamic_calls(Root) ->
case elvis_code:find(fun is_dynamic_call/1, Root, #{traverse => all}) of
[] ->
[];
InvalidCalls ->
ResultFun = result_node_line_fun(?INVALID_DYNAMIC_CALL_MSG),
lists:map(ResultFun, InvalidCalls)
end.
%% @private
-spec is_dynamic_call(ktn_code:tree_node()) -> boolean().
is_dynamic_call(Node) ->
case ktn_code:type(Node) of
call ->
FunctionSpec = ktn_code:node_attr(function, Node),
case ktn_code:type(FunctionSpec) of
remote ->
ModuleName = ktn_code:node_attr(module, FunctionSpec),
var == ktn_code:type(ModuleName);
_Other ->
false
end;
_ ->
false
end.
%% Plain Variable
%% @private
-spec is_var(zipper:zipper(_)) -> boolean().
is_var(Zipper) ->
case ktn_code:type(
zipper:node(Zipper))
of
var ->
PrevLocation =
case ktn_code:attr(location, zipper:node(Zipper)) of
{L, 1} ->
{L - 1, 9999};
{L, C} ->
{L, C - 1}
end,
case elvis_code:find_token(
zipper:root(Zipper), PrevLocation)
of
not_found ->
true;
{ok, PrevToken} ->
ktn_code:type(PrevToken) /= '?'
end;
_NotVar ->
false
end.
%% Ignored Variable
%% @private
-spec is_ignored_var(zipper:zipper(_)) -> boolean().
is_ignored_var(Zipper) ->
Node = zipper:node(Zipper),
case ktn_code:type(Node) of
var ->
Name = ktn_code:attr(name, Node),
[FirstChar | _] = atom_to_list(Name),
(FirstChar == $_) and (Name =/= '_') and not check_parent_match(Zipper);
_OtherType ->
false
end.
%% @private
check_parent_match(Zipper) ->
case zipper:up(Zipper) of
undefined ->
false;
ParentZipper ->
Parent = zipper:node(ParentZipper),
case ktn_code:type(Parent) of
match ->
zipper:down(ParentZipper) == Zipper;
maybe_match ->
zipper:down(ParentZipper) == Zipper;
_ ->
check_parent_match(ParentZipper)
end
end.
%% State record in OTP module
%% @private
-spec is_otp_module(ktn_code:tree_node()) -> boolean().
is_otp_module(Root) ->
OtpSet = sets:from_list([gen_server, gen_event, gen_fsm, gen_statem, supervisor_bridge]),
IsBehaviorAttr =
fun(Node) -> behavior == ktn_code:type(Node) orelse behaviour == ktn_code:type(Node) end,
case elvis_code:find(IsBehaviorAttr, Root) of
[] ->
false;
Behaviors ->
ValueFun = fun(Node) -> ktn_code:attr(value, Node) end,
Names = lists:map(ValueFun, Behaviors),
BehaviorsSet = sets:from_list(Names),
not
sets:is_empty(
sets:intersection(OtpSet, BehaviorsSet))
end.
%% @private
-spec has_state_record(ktn_code:tree_node()) -> boolean().
has_state_record(Root) ->
IsStateRecord =
fun(Node) -> (record_attr == ktn_code:type(Node)) and (state == ktn_code:attr(name, Node))
end,
[] /= elvis_code:find(IsStateRecord, Root).
%% @private
-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))
end,
elvis_code:find(IsStateType, Root) /= [].
%% Spec includes records
%% @private
-spec spec_includes_record(ktn_code:tree_node()) -> boolean().
spec_includes_record(Node) ->
IsTypeRecord =
fun(Child) -> (ktn_code:type(Child) == type) and (ktn_code:attr(name, Child) == record)
end,
Opts = #{traverse => all},
(ktn_code:type(Node) == spec) and (elvis_code:find(IsTypeRecord, Node, Opts) /= []).
%% Don't repeat yourself
%% @private
-spec find_repeated_nodes(ktn_code:tree_node(), non_neg_integer()) ->
[ktn_code:tree_node()].
find_repeated_nodes(Root, MinComplexity) ->
TypeAttrs = #{var => [location, name, text], clause => [location, text]},
FoldFun =
fun(Node, Map) ->
Zipper = elvis_code:code_zipper(Node),
case zipper:size(Zipper) of
Count when Count >= MinComplexity ->
Loc = ktn_code:attr(location, Node),
StrippedNode = remove_attrs_zipper(Zipper, TypeAttrs),
ValsSet = maps:get(StrippedNode, Map, sets:new()),
NewValsSet = sets:add_element(Loc, ValsSet),
maps:put(StrippedNode, NewValsSet, Map);
_ ->
Map
end
end,
ZipperRoot = elvis_code:code_zipper(Root),
Grouped = zipper:fold(FoldFun, #{}, ZipperRoot),
Repeated = filter_repeated(Grouped),
LocationSets = maps:values(Repeated),
Locations = lists:map(fun sets:to_list/1, LocationSets),
lists:map(fun lists:sort/1, Locations).
%% @private
-spec remove_attrs_zipper(zipper:zipper(_), map()) -> ktn_code:tree_node().
remove_attrs_zipper(Zipper, TypeAttrs) ->
zipper:fmap(fun remove_attrs/2, [TypeAttrs], Zipper).
%% @private
-spec remove_attrs(ktn_code:tree_node() | [ktn_code:tree_node()], map()) ->
ktn_code:tree_node().
remove_attrs(Nodes, TypeAttrs) when is_list(Nodes) ->
[remove_attrs(Node, TypeAttrs) || Node <- Nodes];
remove_attrs(#{attrs := Attrs,
type := Type,
node_attrs := NodeAttrs} =
Node,
TypeAttrs) ->
AttrsName = maps:get(Type, TypeAttrs, [location]),
AttrsNoLoc = maps:without(AttrsName, Attrs),
NodeAttrsNoLoc =
[{Key, remove_attrs_zipper(elvis_code:code_zipper(Value), TypeAttrs)}
|| {Key, Value} <- maps:to_list(NodeAttrs)],
Node#{attrs => AttrsNoLoc, node_attrs => maps:from_list(NodeAttrsNoLoc)};
remove_attrs(#{attrs := Attrs, type := Type} = Node, TypeAttrs) ->
AttrsName = maps:get(Type, TypeAttrs, [location]),
AttrsNoLoc = maps:without(AttrsName, Attrs),
Node#{attrs => AttrsNoLoc};
remove_attrs(Node, _TypeAttrs) ->
Node.
%% @private
-spec filter_repeated(map()) -> map().
filter_repeated(NodesLocs) ->
NotRepeated =
[Node || {Node, LocationSet} <- maps:to_list(NodesLocs), sets:size(LocationSet) == 1],
RepeatedMap = maps:without(NotRepeated, NodesLocs),
RepeatedNodes = maps:keys(RepeatedMap),
Nested =
[Node
|| Node <- RepeatedNodes,
Parent <- RepeatedNodes,
Node =/= Parent,
is_children(Parent, Node)],
maps:without(Nested, RepeatedMap).
%% @private
is_children(Parent, Node) ->
Zipper = elvis_code:code_zipper(Parent),
[] =/= zipper:filter(fun(Child) -> Child == Node end, Zipper).
%% No call
%% @private
-spec no_call_common(elvis_config:config(),
elvis_file:file(),
[function_spec()],
string(),
RuleConfig :: elvis_core:rule_config()) ->
[elvis_result:item()].
no_call_common(Config, Target, NoCallFuns, Msg, RuleConfig) ->
Root = get_root(Config, Target, RuleConfig),
Calls = elvis_code:find(fun is_call/1, Root),
check_no_call(Calls, Msg, NoCallFuns).
%% @private
-spec check_no_call([ktn_code:tree_node()], string(), [function_spec()]) ->
[elvis_result:item()].
check_no_call(Calls, Msg, NoCallFuns) ->
BadCalls = [Call || Call <- Calls, is_in_call_list(Call, NoCallFuns)],
ResultFun =
fun(Call) ->
{M, F, A} = call_mfa(Call),
{Line, _} = ktn_code:attr(location, Call),
elvis_result:new(item, Msg, [M, F, A, Line], Line)
end,
lists:map(ResultFun, BadCalls).
%% @private
is_in_call_list(Call, DisallowedFuns) ->
MFA = call_mfa(Call),
MatchFun = fun(Spec) -> fun_spec_match(Spec, MFA) end,
lists:any(MatchFun, DisallowedFuns).
%% @private
call_mfa(Call) ->
FunctionSpec = ktn_code:node_attr(function, Call),
M = ktn_code:attr(value, ktn_code:node_attr(module, FunctionSpec)),
F = ktn_code:attr(value, ktn_code:node_attr(function, FunctionSpec)),
A = length(ktn_code:content(Call)),
{M, F, A}.
%% @private
is_call(Node, {F, A}) ->
ktn_code:type(Node) =:= call
andalso list_to_atom(ktn_code:attr(text, Node)) =:= F
andalso length(ktn_code:content(Node)) =:= A;
is_call(Node, {M, F, A}) ->
call_mfa(Node) =:= {M, F, A}.
%% @private
is_call(Node) ->
ktn_code:type(Node) =:= call.
%% @private
fun_spec_match({M, F}, MFA) ->
fun_spec_match({M, F, '_'}, MFA);
fun_spec_match({M1, F1, A1}, {M2, F2, A2}) ->
wildcard_match(M1, M2) andalso wildcard_match(F1, F2) andalso wildcard_match(A1, A2).
%% @private
wildcard_match('_', _) ->
true;
wildcard_match(X, Y) ->
X =:= Y.
%% @private
%% @doc No nested try...catch blocks
check_nested_try_catchs(ResultFun, TryExp) ->
Predicate = fun(Node) -> ktn_code:type(Node) == 'try' end,
lists:filtermap(fun (Node) when Node /= TryExp ->
{true, ResultFun(Node)};
(_) ->
false
end,
elvis_code:find(Predicate, TryExp)).
%% @private
%% @doc No #{...}#{...}
check_successive_maps(ResultFun, MapExp) ->
case ktn_code:node_attr(var, MapExp) of
undefined ->
[];
InnerVar ->
case ktn_code:type(InnerVar) of
map ->
[ResultFun(InnerVar)];
_ ->
[]
end
end.
%% Consistent Generic Type
%% @private
consistent_generic_type_predicate(TypePreference) ->
fun(Node) ->
NodeType = ktn_code:type(Node),
NodeName = ktn_code:attr(name, Node),
lists:member(NodeType, [type, callback])
andalso lists:member(NodeName, [term, any])
andalso NodeName /= TypePreference
end.
%% @private
consistent_generic_type_result(TypePreference) ->
fun(Node) ->
{Line, _} = ktn_code:attr(location, Node),
NodeName = ktn_code:attr(name, Node),
Info = [NodeName, Line, TypePreference],
elvis_result:new(item, ?CONSISTENT_GENERIC_TYPE, Info, Line)
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Internal Function Definitions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec option(OptionName, RuleConfig, Rule) -> OptionValue
when OptionName :: atom(),
RuleConfig :: elvis_core:rule_config(),
Rule :: atom(),
OptionValue :: term().
option(OptionName, RuleConfig, Rule) ->
maybe_default_option(maps:get(OptionName, RuleConfig, undefined), OptionName, Rule).
-spec maybe_default_option(UserDefinedOptionValue, OptionName, Rule) -> OptionValue
when UserDefinedOptionValue :: undefined | term(),
OptionName :: atom(),
Rule :: atom(),
OptionValue :: term().
maybe_default_option(undefined = _UserDefinedOptionValue, OptionName, Rule) ->
maps:get(OptionName, default(Rule));
maybe_default_option(UserDefinedOptionValue, _OptionName, _Rule) ->
UserDefinedOptionValue.
-spec get_root(Config, Target, RuleConfig) -> Res
when Config :: elvis_config:config(),
Target :: elvis_file:file(),
RuleConfig :: (Options :: #{atom() => term()}),
Res :: ktn_code:tree_node().
get_root(Config, Target, RuleConfig) ->
{Root0, File0} = elvis_file:parse_tree(Config, Target, RuleConfig),
case maps:get(ruleset, Config, undefined) of
beam_files ->
maps:get(abstract_parse_tree, File0);
_ ->
Root0
end.