-module(elvis_style). -export([ function_naming_convention/3, variable_naming_convention/3, line_length/3, no_tabs/3, no_spaces/3, no_trailing_whitespace/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_debug_call/3, no_nested_try_catch/3, no_seqbind/3, no_useless_seqbind/3 ]). -define(LINE_LENGTH_MSG, "Line ~p is too long: ~s."). -define(NO_TABS_MSG, "Line ~p has a tab at column ~p."). -define(NO_SPACES_MSG, "Line ~p has a spaces at column ~p."). -define(NO_TRAILING_WHITESPACE_MSG, "Line ~b has ~b trailing whitespace characters."). -define(INVALID_MACRO_NAME_MSG, "Invalid macro name ~s on line ~p. Use UPPER_CASE."). -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 has ~p lines which exceeds the " "maximum of ~p."). -define(NO_DEBUG_CALL_MSG, "Remove the debug call to ~p:~p/~p on line ~p."). -define(NO_NESTED_TRY_CATCH, "Nested try...catch block starting at line ~p."). -define(NO_SEQBIND, "Declaration of seqbind at line ~p."). -define(NO_USELESS_SEQBIND, "Module declares seqbind on line ~p, but no seq-bindings are used."). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Rules %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -type empty_rule_config() :: #{}. -type max_function_length_config() :: #{ignore_functions => [function_spec()], max_length => non_neg_integer(), count_comments => boolean(), count_whitespace => boolean()}. -type max_module_length_config() :: #{count_comments => boolean(), count_whitespace => boolean(), ignore => [atom()], max_length => integer()}. -type function_naming_convention_config() :: #{regex => string(), ignore => [module()] }. -spec function_naming_convention(elvis_config:config(), elvis_file:file(), function_naming_convention_config()) -> [elvis_result:item()]. function_naming_convention(Config, Target, RuleConfig) -> Regex = maps:get(regex, RuleConfig, ".*"), {Root, _} = elvis_file:parse_tree(Config, Target), FunctionNames = elvis_code:function_names(Root), errors_for_function_names(Regex, FunctionNames). errors_for_function_names(_Regex, []) -> []; errors_for_function_names(Regex, [FunctionName | RemainingFuncNames]) -> FunctionNameStr = atom_to_list(FunctionName), case re:run(FunctionNameStr, Regex) 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() :: #{regex => string(), ignore => [module()] }. -spec variable_naming_convention(elvis_config:config(), elvis_file:file(), variable_naming_convention_config()) -> [elvis_result:item()]. variable_naming_convention(Config, Target, RuleConfig) -> IgnoreModules = maps:get(ignore, RuleConfig, []), Regex = maps:get(regex, RuleConfig, ".*"), {Root, _} = elvis_file:parse_tree(Config, Target), ModuleName = elvis_code:module_name(Root), case lists:member(ModuleName, IgnoreModules) of false -> Vars = elvis_code:find( fun is_var/1, Root, #{traverse => all, mode => zipper}), check_variables_name(Regex, Vars); true -> [] end. -type line_length_config() :: #{limit => integer(), skip_comments => false | any | whole_line }. %% @doc Target can be either a filename or the %% name of a module. -spec line_length(elvis_config:config(), elvis_file:file(), line_length_config()) -> [elvis_result:item()]. line_length(_Config, Target, RuleConfig) -> Limit = maps:get(limit, RuleConfig, 80), SkipComments = maps:get(skip_comments, RuleConfig, false), {Src, #{encoding := Encoding}} = elvis_file:src(Target), Args = [Limit, SkipComments, Encoding], elvis_utils:check_lines(Src, fun check_line_length/3, Args). -spec no_tabs(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. no_tabs(_Config, Target, _RuleConfig) -> {Src, _} = elvis_file:src(Target), elvis_utils:check_lines(Src, fun check_no_tabs/3, []). -spec no_spaces(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. no_spaces(_Config, Target, _RuleConfig) -> {Src, _} = elvis_file:src(Target), elvis_utils:check_lines(Src, fun check_no_spaces/3, []). -spec no_trailing_whitespace(Config::elvis_config:config(), Target::elvis_file:file(), RuleConfig::map()) -> [elvis_result:item()]. no_trailing_whitespace(_Config, Target, RuleConfig) -> {Src, _} = elvis_file:src(Target), elvis_utils:check_lines(Src, fun check_no_trailing_whitespace/3, RuleConfig). -spec macro_names(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. macro_names(_Config, Target, _RuleConfig) -> {Src, _} = elvis_file:src(Target), elvis_utils:check_lines(Src, fun check_macro_names/3, []). -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, _} = elvis_file:parse_tree(Config, Target), elvis_utils:check_lines(Src, fun check_macro_module_names/3, [Root]). -type operator_spaces_config() :: #{rules => [{right|left, string()}]}. -spec operator_spaces(elvis_config:config(), elvis_file:file(), operator_spaces_config()) -> [elvis_result:item()]. operator_spaces(Config, Target, RuleConfig) -> Rules = maps:get(rules, RuleConfig, []), {Src, #{encoding := Encoding}} = elvis_file:src(Target), {Root, _} = elvis_file:parse_tree(Config, Target), Tokens = ktn_code:attr(tokens, Root), Lines = binary:split(Src, <<"\n">>, [global]), lists:flatmap( fun(Rule) -> check_operator_spaces(Lines, Tokens, Rule, Encoding) end, Rules ). -type nesting_level_config() :: #{level => integer(), ignore => [atom()]}. -spec nesting_level(elvis_config:config(), elvis_file:file(), nesting_level_config()) -> [elvis_result:item()]. nesting_level(Config, Target, RuleConfig) -> Level = maps:get(level, RuleConfig, 3), IgnoreModules = maps:get(ignore, RuleConfig, []), {Root, _} = elvis_file:parse_tree(Config, Target), ModuleName = elvis_code:module_name(Root), case lists:member(ModuleName, IgnoreModules) of false -> elvis_utils:check_nodes(Root, fun check_nesting_level/2, [Level]); true-> [] end. -type god_modules_config() :: #{limit => integer(), ignore => [atom()]}. -spec god_modules(elvis_config:config(), elvis_file:file(), god_modules_config()) -> [elvis_result:item()]. god_modules(Config, Target, RuleConfig) -> Limit = maps:get(limit, RuleConfig, 25), IgnoreModules = maps:get(ignore, RuleConfig, []), {Root, _} = elvis_file:parse_tree(Config, Target), ModuleName = elvis_code:module_name(Root), case lists:member(ModuleName, IgnoreModules) of false -> 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; true-> [] end. -spec no_if_expression(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. no_if_expression(Config, Target, _RuleConfig) -> {Root, _} = elvis_file:parse_tree(Config, Target), 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. -type invalid_dynamic_call_config() :: #{ignore => [module()]}. -spec invalid_dynamic_call(elvis_config:config(), elvis_file:file(), invalid_dynamic_call_config()) -> [elvis_result:item()]. invalid_dynamic_call(Config, Target, RuleConfig) -> IgnoreModules = maps:get(ignore, RuleConfig, []), {Root, _} = elvis_file:parse_tree(Config, Target), ModuleName = elvis_code:module_name(Root), case lists:member(ModuleName, IgnoreModules) of false -> Predicate = fun(Node) -> ktn_code:type(Node) == 'callback' end, case elvis_code:find(Predicate, Root) of [] -> check_invalid_dynamic_calls(Root); _Callbacks -> [] end; true -> [] end. -spec used_ignored_variable(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. used_ignored_variable(Config, Target, _RuleConfig) -> {Root, _} = elvis_file:parse_tree(Config, Target), 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, _} = elvis_file:parse_tree(Config, Target), 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() :: #{regex => string(), ignore => [module()] }. -spec module_naming_convention(elvis_config:config(), elvis_file:file(), module_naming_convention_config()) -> [elvis_result:item()]. module_naming_convention(Config, Target, RuleConfig) -> Regex = maps:get(regex, RuleConfig, ".*"), IgnoreModules = maps:get(ignore, RuleConfig, []), {Root, _} = elvis_file:parse_tree(Config, Target), 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, _} = elvis_file:parse_tree(Config, Target), 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, _} = elvis_file:parse_tree(Config, Target), 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() :: #{min_complexity => non_neg_integer(), ignore => [module()] }. -spec dont_repeat_yourself(elvis_config:config(), elvis_file:file(), dont_repeat_yourself_config()) -> [elvis_result:item()]. dont_repeat_yourself(Config, Target, RuleConfig) -> MinComplexity = maps:get(min_complexity, RuleConfig, 5), IgnoreModules = maps:get(ignore, RuleConfig, []), {Root, _} = elvis_file:parse_tree(Config, Target), ModuleName = elvis_code:module_name(Root), Ignored = lists:member(ModuleName, IgnoreModules), case Ignored orelse find_repeated_nodes(Root, MinComplexity) of true when Ignored -> []; [] -> []; [_|_] = Nodes -> 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) end. -spec max_module_length(elvis_config:config(), elvis_file:file(), max_module_length_config()) -> [elvis_result:item()]. max_module_length(Config, Target, RuleConfig) -> MaxLength = maps:get(max_length, RuleConfig, 500), IgnoreModules = maps:get(ignore, RuleConfig, []), CountComments = maps:get(count_comments, RuleConfig, false), CountWhitespace = maps:get(count_whitespace, RuleConfig, false), {Root, _} = elvis_file:parse_tree(Config, Target), {Src, _} = elvis_file:src(Target), ModuleName = elvis_code:module_name(Root), Ignored = lists:member(ModuleName, IgnoreModules), FilterFun = fun(Line) -> (CountComments orelse (not line_is_comment(Line))) andalso (CountWhitespace orelse (not line_is_whitespace(Line))) end, Lines = case binary:split(Src, <<"\n">>, [global, trim]) of Ls when CountComments andalso CountWhitespace -> Ls; Ls -> lists:filter(FilterFun, Ls) end, case length(Lines) of L when L > MaxLength, not Ignored -> 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) -> IgnoreFunctions = maps:get(ignore_functions, RuleConfig, []), MaxLength = maps:get(max_length, RuleConfig, 30), CountComments = maps:get(count_comments, RuleConfig, false), CountWhitespace = maps:get(count_whitespace, RuleConfig, false), {Root, _} = elvis_file:parse_tree(Config, Target), {Src, _} = elvis_file:src(Target), ModuleName = elvis_code:module_name(Root), Lines = binary:split(Src, <<"\n">>, [global, 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), {Min, Max} = node_line_limits(FunctionNode), FunLines = lists:sublist(Lines, Min, Max - Min + 1), FilteredLines = lists:filter(FilterFun, FunLines), L = length(FilteredLines), {Name, Min, L} end, IgnoreFunctionFilterFun = fun(FunctionNode) -> FunctionName = ktn_code:attr(name, FunctionNode), FunctionArity = ktn_code:attr(arity, FunctionNode), MFA = {ModuleName, FunctionName, FunctionArity}, MF = {ModuleName, FunctionName}, not (lists:member(MF, IgnoreFunctions) orelse lists:member(MFA, IgnoreFunctions)) end, Functions1 = lists:filter(IgnoreFunctionFilterFun, Functions0), FunLenInfos = lists:map(PairFun, Functions1), MaxLengthPred = fun({_, _, L}) -> L > MaxLength end, FunLenMaxPairs = lists:filter(MaxLengthPred, FunLenInfos), ResultFun = fun({Name, StartPos, L}) -> Info = [Name, L, MaxLength], Msg = ?MAX_FUNCTION_LENGTH, elvis_result:new(item, Msg, Info, StartPos) end, lists:map(ResultFun, FunLenMaxPairs). -type no_debug_call_config() :: #{debug_functions => [function_spec()], ignore => [module()] }. -type function_spec() :: {module(), atom(), non_neg_integer()} | {module(), atom()}. -spec no_debug_call(elvis_config:config(), elvis_file:file(), no_debug_call_config()) -> [elvis_result:item()]. no_debug_call(Config, Target, RuleConfig) -> IgnoreModules = maps:get(ignore, RuleConfig, []), {Root, _} = elvis_file:parse_tree(Config, Target), ModuleName = elvis_code:module_name(Root), DefaultDebugFuns = [{ct, pal}, {ct, print, 1}, {ct, print, 2}, {ct, print, 3}, {ct, print, 4}, {ct, print, 5}, {io, format, 1}, {io, format, 2}], DebugFuns = maps:get(debug_functions, RuleConfig, DefaultDebugFuns), case lists:member(ModuleName, IgnoreModules) of false -> IsCall = fun(Node) -> ktn_code:type(Node) =:= 'call' end, Calls = elvis_code:find(IsCall, Root), check_no_debug_call(Calls, DebugFuns); true -> [] end. -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, _} = elvis_file:parse_tree(Config, Target), 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. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Private %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% 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. -spec remove_comment(binary()) -> binary(). remove_comment(Line) -> case re:run(Line, "([^%]+)", [{capture, first, binary}]) of nomatch -> Line; {match, [Without]} -> Without end. -spec check_line_length(binary(), integer(), [term()]) -> no_result | {ok, elvis_result:item()}. check_line_length(Line, Num, [Limit, whole_line, Encoding]) -> case line_is_comment(Line) of false -> check_line_length(Line, Num, [Limit, Encoding]); true -> no_result end; check_line_length(Line, Num, [Limit, any, Encoding]) -> LineWithoutComment = remove_comment(Line), check_line_length(LineWithoutComment, Num, [Limit, Encoding]); check_line_length(Line, Num, [Limit, _, Encoding]) -> check_line_length(Line, Num, [Limit, Encoding]); check_line_length(Line, Num, [Limit, Encoding]) -> Chars = unicode:characters_to_list(Line, Encoding), case length(Chars) of Large when Large > Limit -> Msg = ?LINE_LENGTH_MSG, Info = [Num, binary_to_list(Line)], Result = elvis_result:new(item, Msg, Info, Num), {ok, Result}; _ -> no_result end. %% No Tabs -spec check_no_tabs(binary(), integer(), [term()]) -> no_result | {ok, elvis_result:item()}. check_no_tabs(Line, Num, _Args) -> case binary:match(Line, <<"\t">>) of nomatch -> no_result; {Index, _} -> Msg = ?NO_TABS_MSG, Info = [Num, Index], Result = elvis_result:new(item, Msg, Info, Num), {ok, Result} end. %% No Spaces -spec check_no_spaces(binary(), integer(), [term()]) -> no_result | {ok, elvis_result:item()}. check_no_spaces(Line, Num, _Args) -> case re:run(Line, <<"^\t* ">>) of nomatch -> no_result; {Index, _} -> Msg = ?NO_SPACES_MSG, Info = [Num, Index], Result = elvis_result:new(item, Msg, Info, Num), {ok, Result} end. %% No Trailing Whitespace -spec check_no_trailing_whitespace(binary(), integer(), map()) -> no_result | {ok, elvis_result:item()}. check_no_trailing_whitespace(Line, Num, RuleConfig) -> Regex = case RuleConfig of %% Lookbehind assertion: http://erlang.org/doc/man/re.html#sect17 #{ignore_empty_lines := true} -> "(?<=\\S)\\s+$"; _AnythingElse -> "\\s+$" end, case re:run(Line, Regex) of nomatch -> no_result; {match, [PosLen]} -> Msg = ?NO_TRAILING_WHITESPACE_MSG, Info = [Num, size(binary:part(Line, PosLen))], Result = elvis_result:new(item, Msg, Info, Num), {ok, Result} end. %% Macro Names -spec check_macro_names(binary(), integer(), [term()]) -> no_result | {ok, elvis_result:item()}. check_macro_names(Line, Num, _Args) -> {ok, Regex} = re:compile("^ *[-]define *[(]([^,(]+)"), case re:run(Line, Regex, [{capture, all_but_first, list}]) of nomatch -> no_result; {match, [MacroName]} -> case string:to_upper(MacroName) of MacroName -> no_result; _ -> Msg = ?INVALID_MACRO_NAME_MSG, Result = elvis_result:new(item, Msg, [MacroName, Num], Num), {ok, Result} end end. %% Macro in Function Call as Module or Functon 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, [] =/= elvis_code:find(Pred, Root, #{mode => zipper}) end. has_remote_call_parent(undefined) -> false; has_remote_call_parent(Zipper) -> Node = zipper:node(Zipper), case ktn_code:type(Node) of remote -> true; _ -> has_remote_call_parent(zipper:up(Zipper)) end. %% Operator Spaces -spec check_operator_spaces(Lines::[binary()], Tokens::[map()], Rule::{right | left, string()}, Encoding::latin1 | utf8) -> [elvis_result:item()]. check_operator_spaces(Lines, Tokens0, {Position, Operator}, Encoding) -> Tokens = case Operator =:= "-" of true -> filter_compiler_directive_dashes(Tokens0); false -> Tokens0 end, Nodes = lists:filter( fun(Node) -> ktn_code:attr(text, Node) =:= Operator end, Tokens ), SpaceChar = $\s, lists:flatmap( 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, Nodes ). filter_compiler_directive_dashes(Tokens) -> filter_compiler_directive_dashes(Tokens, []). filter_compiler_directive_dashes([], Acc) -> lists:append(lists:reverse(Acc)); filter_compiler_directive_dashes([#{type := '-'}|Tokens], Acc) -> filter_compiler_directive_dashes(Tokens, Acc); filter_compiler_directive_dashes([#{type := comment}|_] = Tokens0, Acc) -> {Tokens, Rest} = lists:splitwith(fun(#{type := T}) -> T =:= comment end, Tokens0), filter_compiler_directive_dashes(Rest, [Tokens|Acc]); filter_compiler_directive_dashes(Tokens0, Acc) -> {Tokens, Rest} = case lists:splitwith(fun(#{type := T}) -> T =/= dot end, Tokens0) of {Tokens1, [Dot|Rest0]} -> {Tokens1 ++ [Dot], Rest0}; {Tokens1, []} -> {Tokens1, []} end, filter_compiler_directive_dashes(Rest, [Tokens|Acc]). -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`. SpaceChar = $\s, case {Position, (ColToCheck > length(OperatorLineStr))} of {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) 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(ktn_code:tree_node()) -> 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(ktn_code:tree_node()) -> 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, supervisor_bridge ]), IsBehaviorAttr = fun(Node) -> behavior == 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 debug call -spec check_no_debug_call([ktn_code:node()], [function_spec()]) -> [elvis_result:item()]. check_no_debug_call(Calls, DebugFuns) -> DebugCalls = [Call || Call <- Calls, is_debug_call(Call, DebugFuns)], ResultFun = fun(Call) -> {M, F, A} = call_mfa(Call), {Line, _} = ktn_code:attr(location, Call), elvis_result:new(item, ?NO_DEBUG_CALL_MSG, [M, F, A, Line], Line) end, lists:map(ResultFun, DebugCalls). is_debug_call(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)). %% Disallow `-compile({parse_trans, seqbind})` -spec no_seqbind(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. no_seqbind(Config, Target, _RuleConfig) -> {Root, _} = elvis_file:parse_tree(Config, Target), ResultFun = result_node_line_fun(?NO_SEQBIND), SeqbindDecls = elvis_code:find(fun is_seqbind_declaration/1, Root), lists:map(ResultFun, SeqbindDecls). is_seqbind_declaration(Node) -> case ktn_code:type(Node) of compile -> declares_seqbind(ktn_code:attr(value, Node)); _ -> false end. declares_seqbind(Decls) when is_list(Decls) -> lists:keyfind(parse_transform, 1, Decls) =:= {parse_transform, seqbind}; declares_seqbind(Singleton) -> Singleton =:= {parse_transform, seqbind}. %% Warn when `-compile({parse_trans, seqbind})` is declared, %% but no seq-bindings (i.e., VarName@) are used in the module. -spec no_useless_seqbind(elvis_config:config(), elvis_file:file(), empty_rule_config()) -> [elvis_result:item()]. no_useless_seqbind(Config, Target, _RuleConfig) -> {Root, _} = elvis_file:parse_tree(Config, Target), ResultFun = result_node_line_fun(?NO_USELESS_SEQBIND), SeqbindDecls = elvis_code:find(fun is_seqbind_declaration/1, Root), case SeqbindDecls of [] -> []; _ -> case uses_seq_bindings(Root) of true -> []; false -> lists:map(ResultFun, SeqbindDecls) end end. uses_seq_bindings(Root) -> SeqBindings = elvis_code:find( fun(Node) -> ktn_code:type(Node) =:= var andalso lists:last(ktn_code:attr(text, Node)) =:= $@ end, Root), SeqBindings /= [].