Packages
elvis_core
1.3.1
5.0.4
5.0.3
5.0.2
5.0.1
5.0.0
retired
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.0
3.0.1
3.0.0
2.0.1
2.0.0
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0
0.7.0
0.6.1
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
Core library for the Erlang style reviewer
Current section
Files
Jump to
Current section
Files
src/elvis_style.erl
-module(elvis_style).
-export([
default/1,
function_naming_convention/3,
variable_naming_convention/3,
macro_names/3,
macro_module_names/3,
operator_spaces/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_function_length/3,
no_call/3,
no_debug_call/3,
no_common_caveats_call/3,
no_nested_try_catch/3,
atom_naming_convention/3,
numeric_format/3,
behaviour_spelling/3,
option/3
]).
-export_type([empty_rule_config/0]).
-export_type([ignorable/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(OPERATOR_SPACE_MSG, "Missing space ~s ~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(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_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(ATOM_NAMING_CONVENTION_MSG,
"Atom ~p on line ~p does not respect the format "
"defined by the regular expression '~p'.").
-define(NUMERIC_FORMAT_MSG,
"Number ~p on line ~p does not respect the format "
"defined by the regular expression '~p'.").
-define(BEHAVIOUR_SPELLING,
"The behavior/behaviour in line ~p is misspelt, please use the "
"~p spelling.").
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Default values
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec default(Rule :: atom()) -> DefaultRuleConfig :: term().
default(macro_names) ->
#{ regex => "^([A-Z][A-Z_0-9]+)$"
};
default(macro_module_names) ->
#{};
default(operator_spaces) ->
#{ rules => [ {right, ","}
, {right, "++"}
, {left, "++"}
]
};
default(nesting_level) ->
#{ level => 4
};
default(god_modules) ->
#{ limit => 25
};
default(no_if_expression) ->
#{};
default(no_nested_try_catch) ->
#{};
default(invalid_dynamic_call) ->
#{};
default(used_ignored_variable) ->
#{};
default(no_behavior_info) ->
#{};
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(state_record_and_type) ->
#{};
default(no_spec_with_records) ->
#{};
default(dont_repeat_yourself) ->
#{ min_complexity => 10
};
default(max_module_length) ->
#{ max_length => 500
, count_comments => false
, count_whitespace => false
};
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}
, {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
}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 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 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) ->
{Src, _} = elvis_file:src(Target),
Root = get_root(Config, Target, RuleConfig),
elvis_utils:check_lines(Src, fun check_macro_module_names/3, [Root]).
-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_operator_spaces(Lines, AllNodes, Rule, Encoding)
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.
%% @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, 0),
[Result];
_ ->
[]
end.
-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.
-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, []).
-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, [])).
-spec behaviour_spelling(elvis_config:config(),
elvis_file:file(),
empty_rule_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, Info, Line)
end,
lists:map(ResultFun, InconsistentBehaviorNodes)
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
specific_or_default(same, Regex) ->
Regex;
specific_or_default(RegexEnclosed, _Regex) ->
RegexEnclosed.
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]),
[Result|AccIn];
{match, _} ->
AccIn
end
end,
check_numeric_format(Regex, RemainingNumNodes, AccOut).
is_integer_node(Node) ->
ktn_code:type(Node) =:= integer.
is_float_node(Node) ->
ktn_code:type(Node) =:= float.
check_atom_names(_Regex, _RegexEnclosed, [] = _AtomNodes, Acc) ->
Acc;
check_atom_names(Regex, RegexEnclosed, [AtomNode | RemainingAtomNodes], AccIn) ->
AtomName0 = ktn_code:attr(text, AtomNode),
{IsEnclosed, AtomName} = string_strip_enclosed(AtomName0),
RE = re_compile_for_atom_type(IsEnclosed, Regex, RegexEnclosed),
AccOut
= case re:run(_Subject = unicode:characters_to_list(AtomName, unicode), RE) of
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),
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),
AccIn ++ [Result];
{match, _Captured} ->
AccIn
end,
check_atom_names(Regex, RegexEnclosed, RemainingAtomNodes, AccOut).
string_strip_enclosed([$' | Rest]) ->
[$' | Reversed] = lists:reverse(Rest),
IsEnclosed = true,
EnclosedAtomName = lists:reverse(Reversed),
{IsEnclosed, EnclosedAtomName};
string_strip_enclosed(NonEnclosedAtomName) ->
IsEnclosed = false,
{IsEnclosed, NonEnclosedAtomName}.
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.
is_atom_node(MaybeAtom) ->
ktn_code:type(MaybeAtom) =:= atom.
%% Variables name
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
result_node_line_fun(Msg) ->
fun(Node) ->
{Line, _} = ktn_code:attr(location, Node),
Info = [Line],
elvis_result:new(item, Msg, Info, Line)
end.
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
-spec line_is_comment(binary()) -> boolean().
line_is_comment(Line) ->
case re:run(Line, "^[ \t]*%") of
nomatch -> false;
{match, _} -> true
end.
-spec line_is_whitespace(binary()) -> boolean().
line_is_whitespace(Line) ->
case re:run(Line, "^[ \t]*$") of
nomatch -> false;
{match, _} -> true
end.
%% Macro Names
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(_Subject = 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),
ResultsIn ++ [Result];
{match, _Captured} ->
ResultsIn
end,
check_macro_names(Regexp, RemainingMacroNodes, ResultsOut).
-dialyzer({no_match, is_macro_define_node/1}).
is_macro_define_node(MaybeMacro) ->
case ktn_code:type(MaybeMacro) of
{atom, [_, _], define} ->
true;
_ ->
false
end.
macro_name_from_node(MacroNode) ->
MacroNodeValue = ktn_code:attr(value, MacroNode),
MacroAsAtom = macro_as_atom(false, [var, atom, call], MacroNodeValue),
MacroNameOriginal = atom_to_list(MacroAsAtom),
MacroNameStripped = string:strip(MacroNameOriginal, both, $'),
{MacroNameStripped, MacroNameOriginal}.
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).
%% Macro in Function Call as Module or Function Name
-spec check_macro_module_names(binary(), integer(), [term()]) ->
no_result | {ok, elvis_result:item()}.
check_macro_module_names(Line, Num, [Root]) ->
{ok, ModNameRegex} = re:compile("[?](\\w+)[:][?]?\\w+\\s*\\("),
{ok, FunNameRegex} = re:compile("[?]?\\w+[:][?](\\w+)\\s*\\("),
ModuleMsg = ?MACRO_AS_MODULE_NAME_MSG,
ModuleResults =
apply_macro_module_names(Line, Num, ModNameRegex, ModuleMsg, Root),
FunctionMsg = ?MACRO_AS_FUNCTION_NAME_MSG,
FunResults =
apply_macro_module_names(Line, Num, FunNameRegex, FunctionMsg, Root),
case FunResults ++ ModuleResults of
[] ->
no_result;
Results ->
{ok, Results}
end.
-spec apply_macro_module_names(Line::binary(),
Num::integer(),
Regex::{re_pattern, _, _, _, _},
Msg::string(),
Root::term()) ->
[elvis_result:item()].
apply_macro_module_names(Line, Num, Regex, Msg, Root) ->
case re:run(Line, Regex, [{capture, all_but_first, index}]) of
nomatch ->
[];
{match, [{Col, Len}]} ->
MacroName = binary_to_list(binary:part(Line, Col, Len)),
case
lists:member(MacroName, ?MACRO_MODULE_NAMES_EXCEPTIONS)
orelse not is_remote_call({Num, Col}, Root)
of
true ->
[];
false ->
Result = elvis_result:new(item, Msg, [MacroName, Num], Num),
[Result]
end
end.
is_remote_call({Num, Col}, Root) ->
case elvis_code:find_by_location(Root, {Num, Col}) of
not_found ->
true;
{ok, Node0} ->
Pred =
fun(Zipper) ->
(Node0 == zipper:node(Zipper))
andalso has_remote_call_parent(Zipper)
end,
Opts = #{mode => zipper, traverse => all},
[] =/= elvis_code:find(Pred, Root, Opts)
end.
has_remote_call_parent(undefined) ->
false;
has_remote_call_parent(Zipper) ->
Node = zipper:node(Zipper),
case ktn_code:type(Node) of
remote ->
true;
call ->
ktn_code:type(zipper:node(zipper:down(Zipper))) =:= remote;
_ ->
has_remote_call_parent(zipper:up(Zipper))
end.
%% Operator Spaces
-spec check_operator_spaces(Lines :: [binary()],
OperatorNodes :: [ktn_code:tree_node()],
Rule :: {right | left, string()},
Encoding :: latin1 | utf8) ->
[elvis_result:item()].
check_operator_spaces(Lines, OperatorNodes, {Position, Operator}, Encoding) ->
FilterFun = fun(Node) -> ktn_code:attr(text, Node) =:= Operator end,
Nodes = lists:filter(FilterFun, OperatorNodes),
SpaceChar = $\s,
FlatFun = fun(Node) ->
Location = ktn_code:attr(location, Node),
case
character_at_location(Position, Lines, Operator, Location, Encoding)
of
SpaceChar -> [];
_ ->
Msg = ?OPERATOR_SPACE_MSG,
{Line, _Col} = Location,
Info = [Position, Operator, Line],
Result = elvis_result:new(item, Msg, Info, Line),
[Result]
end
end,
lists:flatmap(FlatFun, Nodes).
-spec character_at_location(Position::atom(),
Lines::[binary()],
Operator::string(),
Location::{integer(), integer()},
Encoding::latin1|utf8) -> char().
character_at_location(Position, Lines, Operator, {LineNo, Col}, Encoding) ->
Line = lists:nth(LineNo, Lines),
OperatorLineStr = unicode:characters_to_list(Line, Encoding),
ColToCheck = case Position of
left -> Col - 1;
right -> Col + length(Operator)
end,
% If ColToCheck is greater than the length of OperatorLineStr variable, it
% means the end of line was reached so return " " 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(OperatorLineStr))} of
true -> SpaceChar;
{right, true} -> SpaceChar;
_ -> lists:nth(ColToCheck, OperatorLineStr)
end.
%% Nesting Level
-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
-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.
-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
-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
-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.
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;
_ -> check_parent_match(ParentZipper)
end
end.
%% State record in OTP module
-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),
case sets:to_list(sets:intersection(OtpSet, BehaviorsSet)) of
[] -> false;
_ -> true
end
end.
-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,
case elvis_code:find(IsStateRecord, Root) of
[] -> false;
_ -> true
end.
-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
-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
-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).
-spec remove_attrs_zipper(zipper:zipper(_), map()) -> ktn_code:tree_node().
remove_attrs_zipper(Zipper, TypeAttrs) ->
zipper:fmap(fun remove_attrs/2, [TypeAttrs], Zipper).
-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.
-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).
is_children(Parent, Node) ->
Zipper = elvis_code:code_zipper(Parent),
[] =/= zipper:filter(fun(Child) -> Child == Node end, Zipper).
%% No call
-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),
IsCall = fun(Node) -> ktn_code:type(Node) =:= 'call' end,
Calls = elvis_code:find(IsCall, Root),
check_no_call(Calls, Msg, NoCallFuns).
-spec check_no_call([ktn_code:tree_node()], string(), [function_spec()]) ->
[elvis_result:item()].
check_no_call(Calls, Msg, NoCallFuns) ->
DebugCalls = [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, DebugCalls).
is_in_call_list(Call, DebugFuns) ->
MFA = call_mfa(Call),
MatchFun = fun(Spec) -> fun_spec_match(Spec, MFA) end,
lists:any(MatchFun, DebugFuns).
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}.
fun_spec_match({M, F}, {M, F, _}) -> true;
fun_spec_match({M, F, A}, {M, F, A}) -> true;
fun_spec_match(_, _) -> false.
%% 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)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 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.