-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.