-module(elvis_style). -elvis([ {elvis_style, abc_size, #{ignore => [{elvis_style, default, 1}]}}, {elvis_style, code_complexity, #{ignore => [{elvis_style, default, 1}]}} ]). -behaviour(elvis_rule). -export([default/1]). -export([ function_naming_convention/2, variable_naming_convention/2, consistent_ok_error_spec/2, consistent_variable_naming/2, macro_naming_convention/2, no_macros/2, no_specs/2, no_types/2, no_includes/2, no_block_expressions/2, operator_spaces/2, no_space/2, no_space_after_pound/2, no_deep_nesting/2, no_god_modules/2, no_if_expression/2, no_invalid_dynamic_calls/2, no_used_ignored_variables/2, no_behavior_info/2, module_naming_convention/2, state_record_and_type/2, no_spec_with_records/2, dont_repeat_yourself/2, max_module_length/2, max_anonymous_function_arity/2, max_function_arity/2, max_anonymous_function_length/2, max_anonymous_function_clause_length/2, max_function_length/2, max_function_clause_length/2, max_record_fields/2, max_map_type_keys/2, no_call/2, no_debug_call/2, no_common_caveats_call/2, no_nested_try_catch/2, no_successive_maps/2, atom_naming_convention/2, no_throw/2, no_dollar_space/2, no_author/2, no_import/2, no_catch_expressions/2, no_single_clause_case/2, no_single_match_maybe/2, numeric_format/2, behaviour_spelling/2, always_shortcircuit/2, generic_type/2, export_used_types/2, no_match_in_condition/2, param_pattern_matching/2, private_data_types/2, no_init_lists/2, ms_transform_included/2, no_boolean_in_comparison/2, no_operation_on_same_value/2, expression_can_be_simplified/2, no_receive_without_timeout/2, prefer_sigils/2, prefer_unquoted_atoms/2, guard_operators/2, simplify_anonymous_functions/2, prefer_include/2, prefer_strict_generators/2, strict_term_equivalence/2, macro_definition_parentheses/2, code_complexity/2, abc_size/2, prefer_robot_butt/2 ]). % The whole file is considered to have either callback functions or rules. -ignore_xref(elvis_style). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Default values %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec default(RuleName :: atom()) -> elvis_rule:def(). default(no_init_lists) -> elvis_rule:defmap(#{ behaviours => [gen_server, gen_statem, gen_fsm, supervisor, supervisor_bridge, gen_event] }); default(macro_naming_convention) -> elvis_rule:defmap(#{ regex => "^[A-Z](_?[A-Z0-9]+)*$", forbidden_regex => undefined }); default(operator_spaces) -> elvis_rule:defmap(#{ rules => [ {right, "++"}, {left, "++"}, {right, "="}, {left, "="}, {right, "+"}, {left, "+"}, {right, "-"}, {left, "-"}, {right, "*"}, {left, "*"}, {right, "/"}, {left, "/"}, {right, "=<"}, {left, "=<"}, {right, "<"}, {left, "<"}, {right, ">"}, {left, ">"}, {right, ">="}, {left, ">="}, {right, "=="}, {left, "=="}, {right, "=:="}, {left, "=:="}, {right, "/="}, {left, "/="}, {right, "=/="}, {left, "=/="}, {right, "--"}, {left, "--"}, {right, "=>"}, {left, "=>"}, {right, ":="}, {left, ":="}, {right, "<-"}, {left, "<-"}, {right, "<="}, {left, "<="}, {right, "||"}, {left, "||"}, {right, "|"}, {left, "|"}, {right, "::"}, {left, "::"}, {right, "->"}, {left, "->"}, {right, ","}, {right, "!"}, {left, "!"}, {right, "?="}, {left, "?="}, {right, ";"}, {right, "<:="}, {left, "<:="}, {right, "<:-"}, {left, "<:-"}, {right, "&&"}, {left, "&&"} ] }); default(no_space) -> % ) can happen at the start of lines; all others can't elvis_rule:defmap(#{ rules => [ {right, "("}, {left, ")"}, {left, ","}, {left, ":"}, {right, ":"}, {right, "#"}, {right, "?"}, {left, "."}, {left, ";"} ] }); default(no_deep_nesting) -> elvis_rule:defmap(#{ level => 4 }); default(no_god_modules) -> elvis_rule:defmap(#{ limit => 25 }); default(function_naming_convention) -> elvis_rule:defmap(#{ regex => "^[a-z](_?[a-z0-9]+)*(_test_)?$", forbidden_regex => undefined }); default(variable_naming_convention) -> elvis_rule:defmap(#{ regex => "^_?([A-Z][0-9a-zA-Z]*)$", forbidden_regex => undefined }); default(module_naming_convention) -> elvis_rule:defmap(#{ regex => "^[a-z](_?[a-z0-9]+)*(_SUITE)?$", forbidden_regex => undefined }); default(dont_repeat_yourself) -> elvis_rule:defmap(#{ min_complexity => 10 }); default(max_module_length) -> elvis_rule:defmap(#{ max_length => 500, count_comments => false, count_whitespace => false, count_docs => false }); default(max_anonymous_function_arity) -> elvis_rule:defmap(#{ max_arity => 5 }); default(max_function_arity) -> elvis_rule:defmap(#{ max_arity => 8, non_exported_max_arity => 10 }); default(max_anonymous_function_length) -> elvis_rule:defmap(#{ max_length => 30, count_comments => false, count_whitespace => false }); default(max_anonymous_function_clause_length) -> elvis_rule:defmap(#{ max_length => 30, count_comments => false, count_whitespace => false }); default(max_function_length) -> elvis_rule:defmap(#{ max_length => 30, count_comments => false, count_whitespace => false }); default(max_function_clause_length) -> elvis_rule:defmap(#{ max_length => 30, count_comments => false, count_whitespace => false }); default(max_record_fields) -> elvis_rule:defmap(#{ max_fields => 25 }); default(max_map_type_keys) -> elvis_rule:defmap(#{ max_keys => 25 }); default(no_call) -> elvis_rule:defmap(#{ no_call_functions => [] }); default(no_debug_call) -> elvis_rule:defmap(#{ debug_functions => [ {ct, pal}, {ct, print}, {erlang, display, 1}, {io, format, 1}, {io, format, 2}, {io, put_chars, 1}, {io, put_chars, 2}, {dbg, '_'}, {dyntrace, '_'}, {instrument, '_'} ] }); default(no_common_caveats_call) -> elvis_rule:defmap(#{ caveat_functions => [ {timer, send_after, 2}, {timer, send_after, 3}, {timer, send_interval, 2}, {timer, send_interval, 3}, {erlang, size, 1}, {gen_statem, call, 2}, {gen_server, call, 2}, {gen_event, call, 3}, {erlang, list_to_atom, 1}, {erlang, binary_to_atom, 1}, {erlang, binary_to_atom, 2}, {erlang, garbage_collect, 0} ] }); default(atom_naming_convention) -> elvis_rule:defmap(#{ regex => "^[a-z](_?[a-z0-9]+)*(_SUITE)?$", enclosed_atoms => ".*", forbidden_regex => undefined, forbidden_enclosed_regex => undefined }); default(numeric_format) -> elvis_rule:defmap(#{ % Not restrictive. Those who want more restrictions can set it like "^[^_]*$" regex => ".*", int_regex => same, float_regex => same }); default(guard_operators) -> elvis_rule:defmap(#{ preferred_syntax => per_expression }); default(behaviour_spelling) -> elvis_rule:defmap(#{ spelling => behaviour }); default(param_pattern_matching) -> elvis_rule:defmap(#{ side => right }); default(generic_type) -> elvis_rule:defmap(#{ preferred_type => term }); default(private_data_types) -> elvis_rule:defmap(#{ apply_to => [record] }); default(no_operation_on_same_value) -> elvis_rule:defmap(#{ operations => [ 'and', 'or', 'xor', '==', '/=', '=<', '<', '>=', '>', '=:=', '=/=', 'andalso', 'orelse', '=', '--' ] }); default(expression_can_be_simplified) -> elvis_rule:defmap(#{ simplifications => [ list_append_left_empty, list_append_right_empty, list_subtract_from_empty, list_subtract_empty, add_zero_right, subtract_zero, subtract_from_zero, multiply_by_one_right, multiply_by_one_left, div_by_one, rem_by_one, andalso_true, orelse_false, not_true, not_false, band_neg_one, bor_zero, bxor_zero ] }); default(no_macros) -> elvis_rule:defmap(#{ allow => [] }); default(code_complexity) -> elvis_rule:defmap(#{ max_complexity => 30 }); default(abc_size) -> elvis_rule:defmap(#{ max_abc_size => 30 }); default(prefer_robot_butt) -> elvis_rule:defmap(#{ max_small_list_size => 2 }); default(_RuleName) -> elvis_rule:defmap(#{}). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Rules %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -type binary_part() :: {Start :: non_neg_integer(), Length :: integer()}. -spec function_naming_convention(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. function_naming_convention(Rule, ElvisConfig) -> Regex = elvis_rule:option(regex, Rule), ForbiddenRegex = elvis_rule:option(forbidden_regex, Rule), RegexAllow = re_compile(Regex), RegexBlock = re_compile(ForbiddenRegex), {nodes, FunctionNodes} = elvis_code:find(#{ of_types => [function], inside => elvis_code:root(Rule, ElvisConfig) }), lists:filtermap( fun(FunctionNode) -> FunctionName = function_name(FunctionNode), case re_run(FunctionName, RegexAllow) of nomatch -> {true, elvis_result:new_item( "the name of function ~p is not acceptable by regular " "expression '~s'", [FunctionName, Regex], #{node => FunctionNode} )}; {match, _} when ForbiddenRegex =/= undefined -> % We check for forbidden names only after accepted names case re_run(FunctionName, RegexBlock) of {match, _} -> {true, elvis_result:new_item( "the name of function ~p is forbidden by regular " "expression '~s'", [FunctionName, ForbiddenRegex], #{node => FunctionNode} )}; nomatch -> false end; _ -> false end end, FunctionNodes ). function_name(FunctionNode) -> FunctionName = ktn_code:attr(name, FunctionNode), unicode:characters_to_list(atom_to_list(FunctionName)). -spec consistent_variable_naming(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. consistent_variable_naming(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), TokenIndex = build_token_index(Root), % Compile regexes once; canonical_variable_tokenisation/1 used to compile per variable. CamelRe = re_compile("([a-z])([A-Z])"), HyphenRe = re_compile("-"), {nodes, VarNodes} = elvis_code:find(#{ of_types => [var], inside => Root, filtered_by => fun(N) -> is_var(N, TokenIndex) end, traverse => all }), GroupedByTokens = maps:groups_from_list( fun(N) -> canonical_variable_tokenisation( canonical_variable_name(N), CamelRe, HyphenRe ) end, VarNodes ), maps:fold( fun (_Tokens, [_], Acc) -> Acc; (_Tokens, [FirstVarNode | OtherVarNodes], Acc) -> FirstName = canonical_variable_name(FirstVarNode), case unique_other_names(OtherVarNodes, FirstName) of [] -> Acc; OtherNames -> [ elvis_result:new_item( "variable '~s' (first used in line ~p) has ~w with: ~p", [ FirstName, line(FirstVarNode), syntax_or_casing_difference, OtherNames ], #{node => FirstVarNode} ) | Acc ] end end, [], GroupedByTokens ). -spec canonical_variable_tokenisation(unicode:chardata(), term(), term()) -> [unicode:chardata()]. canonical_variable_tokenisation(Name, CamelRe, HyphenRe) -> % 1. Insert underscore between lowercase and uppercase (Camel/PascalCase) S1 = re:replace(Name, CamelRe, "\\1_\\2", [global, {return, list}]), % 2. Replace hyphens with underscores (kebab-case) S2 = re:replace(S1, HyphenRe, "_", [global, {return, list}]), % 3. Lowercase everything S3 = string:casefold(S2), % 4. Split by underscore and automatically drop empty tokens string:lexemes(S3, "_"). unique_other_names(OtherVarNodes, FirstName) -> FilteredNames = lists:filtermap( fun(OtherVarNode) -> case canonical_variable_name(OtherVarNode) of FirstName -> false; OtherName -> {true, OtherName} end end, OtherVarNodes ), lists:usort(FilteredNames). canonical_variable_name(VarNode) -> case atom_to_list(ktn_code:attr(name, VarNode)) of [$_ | Rest] -> Rest; VarNameStr -> VarNameStr end. -spec variable_naming_convention(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. variable_naming_convention(Rule, ElvisConfig) -> Regex = elvis_rule:option(regex, Rule), ForbiddenRegex = elvis_rule:option(forbidden_regex, Rule), RegexAllow = re_compile(Regex), RegexBlock = re_compile(ForbiddenRegex), Root = elvis_code:root(Rule, ElvisConfig), TokenIndex = build_token_index(Root), {nodes, VarNodes} = elvis_code:find(#{ of_types => [var], inside => Root, filtered_by => fun(N) -> is_var(N, TokenIndex) end, traverse => all }), lists:filtermap( fun(VarNode) -> VariableName = variable_name(VarNode), case re_run(VariableName, RegexAllow) of nomatch when VariableName =/= "_" -> {true, elvis_result:new_item( "the name of variable ~p is not acceptable by regular " "expression '~s'", [VariableName, Regex], #{node => VarNode} )}; {match, _} when ForbiddenRegex =/= undefined -> % We check for forbidden names only after accepted names case re_run(VariableName, RegexBlock) of {match, _} -> {true, elvis_result:new_item( "the name of variable ~p is forbidden by regular " "expression '~s'", [VariableName, ForbiddenRegex], #{node => VarNode} )}; nomatch -> false end; _ -> false end end, VarNodes ). variable_name(VarNode) -> atom_to_list(ktn_code:attr(name, VarNode)). -spec macro_naming_convention(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. macro_naming_convention(Rule, ElvisConfig) -> Regex = elvis_rule:option(regex, Rule), ForbiddenRegex = elvis_rule:option(forbidden_regex, Rule), RegexAllow = re_compile(Regex), RegexBlock = re_compile(ForbiddenRegex), {nodes, MacroNodes} = elvis_code:find(#{ of_types => [define], inside => elvis_code:root(Rule, ElvisConfig), traverse => all }), lists:filtermap( fun(MacroNode) -> MacroName = stripped_macro_name_from(MacroNode), case re_run(MacroName, RegexAllow) of nomatch -> {true, elvis_result:new_item( "the name of macro ~p is not acceptable by " "regular expression '~s'", [original_macro_name_from(MacroNode), Regex], #{node => MacroNode} )}; {match, _} when RegexBlock =/= undefined -> % We check for forbidden names only after accepted names case re_run(MacroName, RegexBlock) of {match, _} -> {true, elvis_result:new_item( "the name of macro ~p is forbidden by regular " "expression '~s'", [original_macro_name_from(MacroNode), ForbiddenRegex], #{node => MacroNode} )}; nomatch -> false end; _ -> false end end, MacroNodes ). -spec no_macros(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_macros(Rule, ElvisConfig) -> AllowedMacros = elvis_rule:option(allow, Rule) ++ eep_predef_macros() ++ logger_macros(), {nodes, MacroNodes} = elvis_code:find(#{ of_types => [macro], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(MacroNode) -> is_allowed_macro(MacroNode, AllowedMacros) end }), [ elvis_result:new_item( "an avoidable macro '~s' was found; prefer no macros", [ktn_code:attr(name, MacroNode)], #{node => MacroNode} ) || MacroNode <- MacroNodes ]. is_allowed_macro(MacroNode, AllowedMacros) -> % safe-ignore list_to_atom/1 Macro = list_to_atom(ktn_code:attr(name, MacroNode)), not lists:member(Macro, AllowedMacros). -spec no_types(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_types(Rule, ElvisConfig) -> {nodes, TypeAttrNodes} = elvis_code:find(#{ of_types => [type_attr], inside => elvis_code:root(Rule, ElvisConfig) }), [ elvis_result:new_item( "unexpected `-type` attribute '~p' was found; " "avoid specifying types in .hrl files", [ktn_code:attr(name, TypeAttrNode)], #{node => TypeAttrNode} ) || TypeAttrNode <- TypeAttrNodes ]. -spec no_includes(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_includes(Rule, ElvisConfig) -> {nodes, IncludeNodes} = elvis_code:find(#{ of_types => [include, include_lib], inside => elvis_code:root(Rule, ElvisConfig) }), [ elvis_result:new_item( "unexpected nested '-include[_lib]' attribute ('~s') was found; " "avoid including .hrl files in .hrl files", [ktn_code:attr(value, IncludeNode)], #{node => IncludeNode} ) || IncludeNode <- IncludeNodes ]. -spec no_specs(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_specs(Rule, ElvisConfig) -> {nodes, SpecNodes} = elvis_code:find(#{ of_types => [spec], inside => elvis_code:root(Rule, ElvisConfig) }), [ elvis_result:new_item( "an unexpected spec was found for function '~p'; avoid specs in .hrl files", [ktn_code:attr(name, SpecNode)], #{node => SpecNode} ) || SpecNode <- SpecNodes ]. -spec no_block_expressions(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_block_expressions(Rule, ElvisConfig) -> {nodes, BlockExprs} = elvis_code:find(#{ of_types => ['begin'], inside => tokens_as_content(elvis_code:root(Rule, ElvisConfig)), traverse => all }), [ elvis_result:new_item( "an avoidable block expression ('begin...end') was found", [], #{node => BlockExpr} ) || BlockExpr <- BlockExprs ]. eep_predef_macros() -> % From unexported epp:predef_macros/1 [ 'BASE_MODULE', 'BASE_MODULE_STRING', 'BEAM', 'FEATURE_AVAILABLE', 'FEATURE_ENABLED', 'FILE', 'FUNCTION_ARITY', 'FUNCTION_NAME', 'LINE', 'MACHINE', 'MODULE', 'MODULE_STRING', 'OTP_RELEASE' ]. logger_macros() -> % From logger.hrl [ 'LOG', 'LOG_ALERT', 'LOG_CRITICAL', 'LOG_DEBUG', 'LOG_EMERGENCY', 'LOG_ERROR', 'LOG_INFO', 'LOG_NOTICE', 'LOG_WARNING' ]. -spec no_space_after_pound(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_space_after_pound(Rule, ElvisConfig) -> {nodes, TextNodes} = elvis_code:find(#{ of_types => undefined, inside => tokens_as_content(elvis_code:root(Rule, ElvisConfig)), filtered_by => fun is_text_node/1 }), Lines = lines_in(elvis_rule:file(Rule)), generate_space_check_results(Lines, TextNodes, {right, "#"}, {should_not_have, []}). lines_in(File) -> {Src, File1} = elvis_file:src(File), Encoding = elvis_file:encoding(File1), Lines = elvis_utils:split_all_lines(Src), %% Index lines (and their characters) by 1-based line number so the space %% checks can use O(1) element/2 lookups instead of lists:nth/2 over the line %% list and the character list once per token. list_to_tuple([index_line(Line, Encoding) || Line <- Lines]). -spec index_line(binary(), latin1 | utf8) -> {binary(), tuple(), non_neg_integer()}. index_line(Line, Encoding) -> Chars = unicode:characters_to_list(Line, Encoding), {Line, list_to_tuple(Chars), length(Chars)}. -spec operator_spaces(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. operator_spaces(Rule, ElvisConfig) -> OptRules = elvis_rule:option(rules, Rule), Root = elvis_code:root(Rule, ElvisConfig), {nodes, OpNodes} = elvis_code:find(#{ of_types => undefined, inside => Root, filtered_by => fun is_operator_node/1 }), {nodes, PunctuationTokens} = elvis_code:find(#{ of_types => ['=', '&&', ',', ';', dot, '->', ':', '::', '|', '||'], inside => tokens_as_content(Root) }), AllNodes = OpNodes ++ PunctuationTokens, Lines = lines_in(elvis_rule:file(Rule)), lists:flatmap( fun(OptRule) -> generate_space_check_results(Lines, AllNodes, OptRule, {should_have, []}) end, OptRules ). %% @doc Returns true when the node is an operator with more than one operand is_operator_node(Node) -> NodeType = ktn_code:type(Node), OpOrMatch = [op | match_operators()], ExtraOpsTypes = [ b_generate, b_generate_strict, generate, generate_strict, m_generate, m_generate_strict, map_field_assoc, map_field_exact ], case ktn_code:content(Node) of [_, _ | _] -> lists:member(NodeType, OpOrMatch); _ -> lists:member(NodeType, ExtraOpsTypes) end. match_operators() -> [match, maybe_match]. -spec no_space(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_space(Rule, ElvisConfig) -> OptRules = elvis_rule:option(rules, Rule), {nodes, TextNodes} = elvis_code:find(#{ of_types => undefined, inside => tokens_as_content(elvis_code:root(Rule, ElvisConfig)), filtered_by => fun is_text_node/1 }), AllSpaceUntilText = [ {Text, re_compile("^[ ]+" ++ escape_regex(Text))} || {left, Text} <- OptRules ], Lines = lines_in(elvis_rule:file(Rule)), lists:flatmap( fun(OptRule) -> generate_space_check_results( Lines, TextNodes, OptRule, {should_not_have, AllSpaceUntilText} ) end, OptRules ). escape_regex(Text) -> EscapePattern = "(\\.|\\[|\\]|\\^|\\$|\\+|\\*|\\?|\\{|\\}|\\(|\\)|\\||\\\\)", re:replace(Text, EscapePattern, "\\\\\\1", [{return, list}, global]). is_text_node(Node) -> ktn_code:attr(text, Node) =/= "". -spec no_deep_nesting(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_deep_nesting(Rule, ElvisConfig) -> MaxLevel = elvis_rule:option(level, Rule), {nodes, ParentNodes} = elvis_code:find(#{ of_types => undefined, inside => elvis_code:root(Rule, ElvisConfig) }), lists:flatmap( fun(ParentNode) -> NestedNodes = past_nesting_limit(ParentNode, MaxLevel), lists:map( fun({Node, Level}) -> elvis_result:new_item( "an expression is nested (level = ~p) beyond the configured limit", [Level], #{node => Node, limit => MaxLevel} ) end, NestedNodes ) end, ParentNodes ). -spec no_god_modules(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_god_modules(Rule, ElvisConfig) -> Limit = elvis_rule:option(limit, Rule), Root = elvis_code:root(Rule, ElvisConfig), ExportedFunctions = exported_functions(Root), Count = sets:size(ExportedFunctions), case Count > Limit of true -> [ elvis_result:new_item( "This module's function count (~p) is higher than the configured limit", [Count], #{limit => Limit} ) ]; _ -> [] end. exported_functions(Root) -> exported_set(Root, export). exported_types(Root) -> exported_set(Root, export_type). exported_set(Root, Type) -> {nodes, ExportNodes} = elvis_code:find(#{of_types => [Type], inside => Root}), sets:from_list( lists:flatmap(fun(Node) -> ktn_code:attr(value, Node) end, ExportNodes), [{version, 2}] ). -spec no_if_expression(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_if_expression(Rule, ElvisConfig) -> {nodes, IfExprNodes} = elvis_code:find(#{ of_types => ['if'], inside => elvis_code:root(Rule, ElvisConfig) }), [ elvis_result:new_item( "an unexpected 'if' expression was found", [], #{node => IfExprNode} ) || IfExprNode <- IfExprNodes ]. -spec no_invalid_dynamic_calls(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_invalid_dynamic_calls(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), InvalidCallNodes = case has_callbacks(Root) of true -> []; false -> {nodes, InvalidCallNodes0} = elvis_code:find(#{ of_types => [call], inside => Root, filtered_by => fun is_dynamic_call/1, traverse => all }), InvalidCallNodes0 end, [ elvis_result:new_item( "an unexpected dynamic function call was found; prefer " "making dynamic calls only in modules that define callbacks", [], #{node => InvalidCallNode} ) || InvalidCallNode <- InvalidCallNodes ]. has_callbacks(Root) -> {nodes, Nodes} = elvis_code:find(#{ of_types => [callback], inside => Root }), Nodes =/= []. -spec no_used_ignored_variables(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_used_ignored_variables(Rule, ElvisConfig) -> {nodes_and_ancestors, IgnoredVars} = elvis_code:find(#{ of_types => [var], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_ignored_var/1, filtered_from => node_and_ancestors }), [ elvis_result:new_item( "an unexpected use of an ignored variable was found", [], #{node => Node} ) || {Node, _Ancestors} <- IgnoredVars ]. -spec no_behavior_info(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_behavior_info(Rule, ElvisConfig) -> {nodes, BehaviourInfoNodes} = elvis_code:find(#{ of_types => [function], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun has_behavior_info/1 }), [ elvis_result:new_item( "an avoidable 'behavio[u]r_info/1' declaration was found; prefer '-callback' " "attributes", [], #{node => BehaviourInfoNode} ) || BehaviourInfoNode <- BehaviourInfoNodes ]. has_behavior_info(FunctionNode) -> FunctionName = ktn_code:attr(name, FunctionNode), lists:member(FunctionName, [behavior_info, behaviour_info]). -spec module_naming_convention(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. module_naming_convention(Rule, ElvisConfig) -> Regex = elvis_rule:option(regex, Rule), ForbiddenRegex = elvis_rule:option(forbidden_regex, Rule), RegexAllow = re_compile(Regex), RegexBlock = re_compile(ForbiddenRegex), {nodes, ModuleNode} = elvis_code:find(#{ of_types => [module], inside => elvis_code:root(Rule, ElvisConfig) }), ModuleName = module_name_from(ModuleNode, elvis_rule:file(Rule)), case re_run(ModuleName, RegexAllow) of nomatch -> [ elvis_result:new_item( "The name of this module is not acceptable by regular " "expression '~s'", [Regex] ) ]; {match, _} when RegexBlock =/= undefined -> case re_run(ModuleName, RegexBlock) of % We check for forbidden names only after accepted names {match, _} -> [ elvis_result:new_item( "The name of this module name is forbidden by regular " "expression '~s'", [ForbiddenRegex] ) ]; nomatch -> [] end; _ -> [] end. module_name_from(ModuleNode, File) -> ModuleName = case ModuleNode of [Module] -> ktn_code:attr(value, Module); _ -> elvis_file:module(File) end, atom_to_list(ModuleName). -spec state_record_and_type(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. state_record_and_type(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), {nodes, AllNodes} = elvis_code:find(#{ of_types => [behaviour, behavior, record_attr, type_attr, opaque, nominal], inside => Root }), #{behaviours := BehaviourNodes, records := RecordAttrNodes, types := TypeOrOpaqueNodes} = partition_nodes(AllNodes, #{ behaviour => behaviours, behavior => behaviours, record_attr => records, type_attr => types, opaque => types, nominal => types }), case is_otp_behaviour(BehaviourNodes) of true -> HasStateRecord = lists:any(fun is_state_record/1, RecordAttrNodes), HasStateType = lists:any(fun is_state_type/1, TypeOrOpaqueNodes), case {HasStateRecord, HasStateType} of {true, true} -> []; {false, _} -> [ elvis_result:new_item( "This module implements an OTP behavior but is missing a '#state{}' " "record" ) ]; {true, false} -> [ elvis_result:new_item( "This module implements an OTP behavior and has a '#state{}' record " "but is missing a 'state()' type" ) ] end; false -> [] end. is_state_record(RecordAttrNode) -> ktn_code:attr(name, RecordAttrNode) =:= state. is_state_type(Node) -> case ktn_code:type(Node) of type_attr -> ktn_code:attr(name, Node) =:= state; Type when Type =:= opaque; Type =:= nominal -> case ktn_code:attr(value, Node) of {state, _, _} -> true; _ -> false end end. -spec consistent_ok_error_spec(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. consistent_ok_error_spec(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), case behaviour_callbacks(Root) of disabled -> []; BehaviourCallbacks -> {nodes, SpecsWithJustOkResult} = elvis_code:find(#{ of_types => [spec], inside => Root, filtered_by => fun(SpecNode) -> spec_has_just_ok_result(SpecNode) andalso not is_behaviour_callback_spec(SpecNode, BehaviourCallbacks) end }), [ elvis_result:new_item( "function ~p unnecessarily wraps its results in a tuple; prefer not " "wrapping them or adding alternative/error results to the spec", [ktn_code:attr(name, SpecWithJustOkResult)], #{node => SpecWithJustOkResult} ) || SpecWithJustOkResult <- SpecsWithJustOkResult ] end. behaviour_callbacks(Root) -> {nodes, BehaviourNodes} = elvis_code:find(#{ of_types => [behaviour, behavior], inside => Root }), Behaviours = lists:map( fun(BehaviourNode) -> ktn_code:attr(value, BehaviourNode) end, BehaviourNodes ), case lists:all(fun(Behaviour) -> lists:member(Behaviour, known_behaviours()) end, Behaviours) of true -> sets:from_list( lists:flatmap(fun callbacks_of_behaviour/1, Behaviours), [{version, 2}] ); false -> disabled end. known_behaviours() -> [ application, gen_event, gen_server, ssh_channel, ssh_client_channel, ssh_client_key_api, ssh_server_channel, ssh_server_key_api, ssl_crl_cache_api, ssl_session_cache_api, supervisor, supervisor_bridge, tftp ]. callbacks_of_behaviour(Behaviour) -> {module, Behaviour} = code:ensure_loaded(Behaviour), erlang:apply(Behaviour, behaviour_info, [callbacks]). is_behaviour_callback_spec(SpecNode, BehaviourCallbacks) -> sets:is_element( {ktn_code:attr(name, SpecNode), ktn_code:attr(arity, SpecNode)}, BehaviourCallbacks ). spec_has_just_ok_result(SpecNode) -> lists:all( fun(SpecType) -> [Result | _] = lists:reverse(ktn_code:content(SpecType)), case ktn_code:attr(name, Result) of tuple -> [FirstElement | _] = ktn_code:content(Result), atom =:= ktn_code:type(FirstElement) andalso ok =:= ktn_code:attr(value, FirstElement); _ -> false end end, ktn_code:node_attr(types, SpecNode) ). -spec no_spec_with_records(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_spec_with_records(Rule, ElvisConfig) -> {nodes, SpecWithRecordNodes} = elvis_code:find(#{ of_types => [spec], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun spec_has_records/1 }), [ elvis_result:new_item( "an unexpected record was found in a spec; prefer creating a type for it and " "using that", [], #{node => SpecWithRecordNode} ) || SpecWithRecordNode <- SpecWithRecordNodes ]. spec_has_records(SpecNode) -> {nodes, TypeNodes} = elvis_code:find(#{ of_types => [type], inside => SpecNode, filtered_by => fun type_is_record/1, traverse => all }), TypeNodes =/= []. type_is_record(TypeInSpecNode) -> ktn_code:attr(name, TypeInSpecNode) =:= record. -spec dont_repeat_yourself(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. dont_repeat_yourself(Rule, ElvisConfig) -> MinComplexity = elvis_rule:option(min_complexity, Rule), Root = elvis_code:root(Rule, ElvisConfig), NodesWithRepeat = find_repeated_nodes(Root, MinComplexity), [ elvis_result:new_item( "The code in the following (, ) locations has the same structure: ~ts", [comma_separate_repetitions(NodeWithRepeat)] ) || NodeWithRepeat <- NodesWithRepeat ]. comma_separate_repetitions(NodeWithRepeat) -> string:join( [io_lib:format("(~p, ~p)", [Line, Col]) || {Line, Col} <- NodeWithRepeat], ", " ). -spec max_module_length(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_module_length(Rule, _ElvisConfig) -> MaxLength = elvis_rule:option(max_length, Rule), CountComments = elvis_rule:option(count_comments, Rule), CountWhitespace = elvis_rule:option(count_whitespace, Rule), CountDocs = elvis_rule:option(count_docs, Rule), {Src0, _} = elvis_file:src(elvis_rule:file(Rule)), DocParts = doc_bin_parts(Src0), Docs = iolist_to_binary(bin_parts_to_iolist(Src0, DocParts)), SrcParts = ignore_bin_parts(Src0, DocParts), Src = iolist_to_binary(bin_parts_to_iolist(Src0, SrcParts)), Lines0 = elvis_utils:split_all_lines(Src, [trim]), LineIsCommentRegex = line_is_comment_regex(), LineIsWhitespaceRegex = line_is_whitespace_regex(), LineCount = count_matching( Lines0, fun(Line) -> filter_comments_and_whitespace( Line, CountComments, CountWhitespace, LineIsCommentRegex, LineIsWhitespaceRegex ) end ), DocLines = doc_lines(CountDocs, Docs), ModLength = LineCount + length(DocLines), case ModLength > MaxLength of true -> [ elvis_result:new_item( "This module's lines-of-code count (~p) is higher than the configured limit", [ModLength], #{limit => MaxLength} ) ]; _ -> [] end. doc_lines(true = _CountDocs, Docs) -> elvis_utils:split_all_lines(Docs, [trim]); doc_lines(false, _Docs) -> []. -spec max_anonymous_function_arity(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_anonymous_function_arity(Rule, ElvisConfig) -> MaxArity = elvis_rule:option(max_arity, Rule), {nodes, FunNodes} = elvis_code:find(#{ of_types => ['fun', named_fun], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun has_clauses/1 }), % We do this to recover the fun and arity FunArities = lists:filtermap( fun(FunNode) -> Arity = length(first_clause_args(FunNode)), case Arity > MaxArity of true -> {true, {FunNode, Arity}}; false -> false end end, FunNodes ), [ elvis_result:new_item( "the arity (~p) of the anonymous function is higher than the " "configured limit", [Arity], #{node => Fun, limit => MaxArity} ) || {Fun, Arity} <- FunArities ]. has_clauses(FunNode) -> %% Not having clauses means it's something like fun mod:f/10 and we don't want %% this rule to raise warnings for those. max_function_arity should take care of %% them. {nodes, ClauseNodes} = elvis_code:find(#{ of_types => [clause], inside => FunNode }), ClauseNodes =/= []. first_clause_args(FunNode) -> {nodes, [FirstClause | _]} = elvis_code:find(#{ of_types => [clause], inside => FunNode }), ktn_code:node_attr(pattern, FirstClause). -spec max_function_arity(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_function_arity(Rule, ElvisConfig) -> ExportedMaxArity = elvis_rule:option(max_arity, Rule), NonExportedMaxArity0 = elvis_rule:option(non_exported_max_arity, Rule), NonExportedMaxArity = specific_or_default(NonExportedMaxArity0, ExportedMaxArity), Root = elvis_code:root(Rule, ElvisConfig), ExportedFunctionsSet = exported_functions(Root), {nodes, FunctionNodes0} = elvis_code:find(#{ of_types => [function], inside => Root }), % We do this to recover the max arity (because it depends on "exported or not") FunctionNodeMaxArities = lists:filtermap( fun(FunctionNode) -> MaxArity = arity_for_function_exports( ExportedFunctionsSet, FunctionNode, ExportedMaxArity, NonExportedMaxArity ), case ktn_code:attr(arity, FunctionNode) > MaxArity of true -> {true, {FunctionNode, MaxArity}}; false -> false end end, FunctionNodes0 ), [ elvis_result:new_item( "the arity of function '~p/~p' is higher than the configured limit", [ktn_code:attr(name, FunctionNode), ktn_code:attr(arity, FunctionNode)], #{node => FunctionNode, limit => MaxArity} ) || {FunctionNode, MaxArity} <- FunctionNodeMaxArities ]. arity_for_function_exports( ExportedFunctions, FunctionNode, ExportedMaxArity, NonExportedMaxArity ) -> case is_exported_function(FunctionNode, ExportedFunctions) of true -> ExportedMaxArity; false -> NonExportedMaxArity end. is_exported_function(FunctionNode, ExportedFunctionsSet) -> Name = ktn_code:attr(name, FunctionNode), Arity = ktn_code:attr(arity, FunctionNode), sets:is_element({Name, Arity}, ExportedFunctionsSet). -spec max_anonymous_function_clause_length(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_anonymous_function_clause_length(Rule, ElvisConfig) -> MaxLength = elvis_rule:option(max_length, Rule), CountComments = elvis_rule:option(count_comments, Rule), CountWhitespace = elvis_rule:option(count_whitespace, Rule), {Src, _} = elvis_file:src(elvis_rule:file(Rule)), Lines = elvis_utils:split_all_lines(Src, [trim]), {nodes_and_ancestors, ClauseResults} = elvis_code:find(#{ of_types => [clause], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun({_Node, Ancestors}) -> is_function_clause(Ancestors, ['fun', named_fun]) end, filtered_from => node_and_ancestors, traverse => all }), CommentRegex = line_is_comment_regex(), WhitespaceRegex = line_is_whitespace_regex(), BigClauses = big_clauses( ClauseResults, Lines, CountComments, CountWhitespace, MaxLength, CommentRegex, WhitespaceRegex ), lists:map( fun({_ClauseNode, Parent, ClauseNum, LineLen}) -> elvis_result:new_item( "the code for the ~s clause of the anonymous function has ~p lines, " "which is higher than the configured limit", [parse_clause_num(ClauseNum), LineLen], #{node => Parent, limit => MaxLength} ) end, lists:reverse(BigClauses) ). -spec max_function_clause_length(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_function_clause_length(Rule, ElvisConfig) -> MaxLength = elvis_rule:option(max_length, Rule), CountComments = elvis_rule:option(count_comments, Rule), CountWhitespace = elvis_rule:option(count_whitespace, Rule), {Src, _} = elvis_file:src(elvis_rule:file(Rule)), Lines = elvis_utils:split_all_lines(Src, [trim]), {nodes_and_ancestors, ClauseResults} = elvis_code:find(#{ of_types => [clause], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun({_Node, Ancestors}) -> is_function_clause(Ancestors, [function]) end, filtered_from => node_and_ancestors }), LineIsCommentRegex = line_is_comment_regex(), LineIsWhitespaceRegex = line_is_whitespace_regex(), BigClauses = big_clauses( ClauseResults, Lines, CountComments, CountWhitespace, MaxLength, LineIsCommentRegex, LineIsWhitespaceRegex ), lists:map( fun({_ClauseNode, FunctionNode, ClauseNum, LineLen}) -> elvis_result:new_item( "the code for the ~s clause of function '~p/~p' has ~p lines, which is higher than " "the configured limit", [ parse_clause_num(ClauseNum), ktn_code:attr(name, FunctionNode), ktn_code:attr(arity, FunctionNode), LineLen ], #{node => FunctionNode, limit => MaxLength} ) end, lists:reverse(BigClauses) ). big_clauses( ClauseResults, Lines, CountComments, CountWhitespace, MaxLength, LineIsCommentRegex, LineIsWhitespaceRegex ) -> % We do this to recover the clause number and apply the configured filters {BigClauses, _} = lists:foldl( fun({ClauseNode, [Parent | _]}, {BigClauses0, ClauseNum}) -> LineLen = filtered_line_count_in( ClauseNode, Lines, CountComments, CountWhitespace, LineIsCommentRegex, LineIsWhitespaceRegex ), AccOut = case LineLen > MaxLength of true -> [{ClauseNode, Parent, ClauseNum, LineLen} | BigClauses0]; false -> BigClauses0 end, {AccOut, ClauseNum + 1} end, {[], 1}, ClauseResults ), BigClauses. filtered_line_count_in( Node, Lines, CountComments, CountWhitespace, LineIsCommentRegex, LineIsWhitespaceRegex ) -> {Min, Max} = node_line_limits(Node), NodeLines = lists:sublist(Lines, Min, Max - Min + 1), %% Count matching lines directly rather than building a filtered list only to %% take its length. count_matching( NodeLines, fun(NodeLine) -> filter_comments_and_whitespace( NodeLine, CountComments, CountWhitespace, LineIsCommentRegex, LineIsWhitespaceRegex ) end ). count_matching(List, Pred) -> lists:foldl( fun(Elem, Count) -> case Pred(Elem) of true -> Count + 1; false -> Count end end, 0, List ). filter_comments_and_whitespace( NodeLine, CountComments, CountWhitespace, LineIsCommentRegex, LineIsWhitespaceRegex ) -> (CountComments orelse not line_is_comment(NodeLine, LineIsCommentRegex)) andalso (CountWhitespace orelse not line_is_whitespace(NodeLine, LineIsWhitespaceRegex)). parse_clause_num(Num) when Num rem 100 >= 11, Num rem 100 =< 13 -> integer_to_list(Num) ++ "th"; parse_clause_num(Num) when Num rem 10 =:= 1 -> integer_to_list(Num) ++ "st"; parse_clause_num(Num) when Num rem 10 =:= 2 -> integer_to_list(Num) ++ "nd"; parse_clause_num(Num) when Num rem 10 =:= 3 -> integer_to_list(Num) ++ "rd"; parse_clause_num(Num) -> integer_to_list(Num) ++ "th". -spec max_anonymous_function_length(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_anonymous_function_length(Rule, ElvisConfig) -> MaxLength = elvis_rule:option(max_length, Rule), CountComments = elvis_rule:option(count_comments, Rule), CountWhitespace = elvis_rule:option(count_whitespace, Rule), {Src, _} = elvis_file:src(elvis_rule:file(Rule)), Lines = elvis_utils:split_all_lines(Src, [trim]), {nodes, FunctionNodes} = elvis_code:find(#{ of_types => ['fun', named_fun], inside => elvis_code:root(Rule, ElvisConfig), traverse => all }), LineIsCommentRegex = line_is_comment_regex(), LineIsWhitespaceRegex = line_is_whitespace_regex(), BigFunctions = big_functions( FunctionNodes, Lines, CountComments, CountWhitespace, MaxLength, LineIsCommentRegex, LineIsWhitespaceRegex ), lists:map( fun({FunctionNode, LineLen}) -> elvis_result:new_item( "the code for the anonymous function has ~p lines, which is higher than the " "configured limit", [LineLen], #{node => FunctionNode, limit => MaxLength} ) end, BigFunctions ). -spec max_function_length(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_function_length(Rule, ElvisConfig) -> MaxLength = elvis_rule:option(max_length, Rule), CountComments = elvis_rule:option(count_comments, Rule), CountWhitespace = elvis_rule:option(count_whitespace, Rule), {Src, _} = elvis_file:src(elvis_rule:file(Rule)), Lines = elvis_utils:split_all_lines(Src, [trim]), {nodes, FunctionNodes} = elvis_code:find(#{ of_types => [function], inside => elvis_code:root(Rule, ElvisConfig) }), LineIsCommentRegex = line_is_comment_regex(), LineIsWhitespaceRegex = line_is_whitespace_regex(), BigFunctions = big_functions( FunctionNodes, Lines, CountComments, CountWhitespace, MaxLength, LineIsCommentRegex, LineIsWhitespaceRegex ), lists:map( fun({FunctionNode, LineLen}) -> elvis_result:new_item( "the code for function '~p/~p' has ~p lines, which is higher than the configured " "limit", [ktn_code:attr(name, FunctionNode), ktn_code:attr(arity, FunctionNode), LineLen], #{node => FunctionNode, limit => MaxLength} ) end, BigFunctions ). big_functions( FunctionNodes, Lines, CountComments, CountWhitespace, MaxLength, LineIsCommentRegex, LineIsWhitespaceRegex ) -> % We do this to apply the configured filters lists:filtermap( fun(FunctionNode) -> LineLen = filtered_line_count_in( FunctionNode, Lines, CountComments, CountWhitespace, LineIsCommentRegex, LineIsWhitespaceRegex ), case LineLen > MaxLength of true -> {true, {FunctionNode, LineLen}}; false -> false end end, FunctionNodes ). -spec max_record_fields(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_record_fields(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), MaxFields = elvis_rule:option(max_fields, Rule), {nodes, RecordNodes} = elvis_code:find(#{ of_types => [record_attr], inside => Root }), [ elvis_result:new_item( "record ~p has ~p fields, which is higher than the configured limit", [ktn_code:attr(name, RecordNode), FieldCount], #{node => RecordNode, limit => MaxFields} ) || RecordNode <- RecordNodes, FieldCount <- [length(ktn_code:content(RecordNode))], FieldCount > MaxFields ]. -spec max_map_type_keys(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. max_map_type_keys(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), MaxFields = elvis_rule:option(max_keys, Rule), max_map_type_keys_on_types(Root, MaxFields) ++ max_map_type_keys_on_opaques_and_nominals(Root, MaxFields). max_map_type_keys_on_types(Root, MaxFields) -> {nodes, MapTypeNodes} = elvis_code:find(#{ of_types => [type_attr], inside => Root, filtered_by => fun is_map_type_with_atom_keys/1 }), [ elvis_result:new_item( "map type ~p has ~p fields, which is higher than the configured limit", [ktn_code:attr(name, MapTypeNode), FieldCount], #{node => MapTypeNode, limit => MaxFields} ) || MapTypeNode <- MapTypeNodes, FieldCount <- [length(ktn_code:content(ktn_code:node_attr(type, MapTypeNode)))], FieldCount > MaxFields ]. is_map_type_with_atom_keys(TypeAttrNode) -> TypeNode = ktn_code:node_attr(type, TypeAttrNode), ktn_code:attr(name, TypeNode) =:= map andalso all_type_keys_are_atoms(ktn_code:content(TypeNode)). all_type_keys_are_atoms(TypeNodes) -> lists:all( fun(TypeNode) -> case ktn_code:content(TypeNode) of [] -> %% -type t() :: map(). false; [KeyType | _] -> ktn_code:type(KeyType) =:= atom end end, TypeNodes ). max_map_type_keys_on_opaques_and_nominals(Root, MaxFields) -> {nodes, MapNodes} = elvis_code:find(#{ of_types => [opaque, nominal], inside => Root, filtered_by => fun is_map_opaque_or_nominal_with_atom_keys/1 }), [ elvis_result:new_item( "map type ~p has ~p fields, which is higher than the configured limit", [MapName, FieldCount], #{node => MapNode, limit => MaxFields} ) || MapNode <- MapNodes, {MapName, MapTypeAST, _} <- [ktn_code:attr(value, MapNode)], FieldCount <- [length(erl_syntax:type_application_arguments(MapTypeAST))], FieldCount > MaxFields ]. is_map_opaque_or_nominal_with_atom_keys(Node) -> {_Name, TypeAST, _Params} = ktn_code:attr(value, Node), erl_syntax:type(TypeAST) =:= map_type andalso all_opaque_or_nominal_keys_are_atoms(erl_syntax:type_application_arguments(TypeAST)). all_opaque_or_nominal_keys_are_atoms(KeyASTs) -> %% for -opaque/-nominal t() :: map()., KeyASTs =:= any. is_list(KeyASTs) andalso lists:all( fun(KeyAST) -> [KeyType | _] = erl_syntax:type_application_arguments(KeyAST), erl_syntax:type(KeyType) =:= atom end, KeyASTs ). -spec no_call(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_call(Rule, ElvisConfig) -> DefaultFns = elvis_rule:option(no_call_functions, Rule), Msg = "an unexpected call to '~p:~p/~p' was found (check no_call list)", no_call_common(Rule, ElvisConfig, DefaultFns, Msg). -spec no_debug_call(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_debug_call(Rule, ElvisConfig) -> DefaultFns = elvis_rule:option(debug_functions, Rule), Msg = "an unexpected debug call to '~p:~p/~p' was found", no_call_common(Rule, ElvisConfig, DefaultFns, Msg). -spec no_common_caveats_call(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_common_caveats_call(Rule, ElvisConfig) -> DefaultFns = elvis_rule:option(caveat_functions, Rule), Msg = "the call to '~p:~p/~p' might have performance drawbacks or implicit behavior", no_call_common(Rule, ElvisConfig, DefaultFns, Msg). -spec node_line_limits(ktn_code:tree_node()) -> {Min :: integer(), Max :: integer()}. node_line_limits(FunctionNode) -> % Min = first line of the function node itself (not lists:min, because % macros are often defined at the top of the module, skewing the minimum). Min = line(FunctionNode), Max = max_line(FunctionNode, Min), {Min, Max}. max_line(#{content := Content} = Node, Acc) -> Acc1 = erlang:max(line(Node), Acc), lists:foldl(fun max_line/2, Acc1, Content); max_line(Node, Acc) when is_map(Node) -> erlang:max(line(Node), Acc); max_line(_, Acc) -> Acc. -spec no_nested_try_catch(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_nested_try_catch(Rule, ElvisConfig) -> {nodes, TryExprNodes} = elvis_code:find(#{ of_types => ['try'], inside => elvis_code:root(Rule, ElvisConfig) }), InnerTryExprNodes = inner_try_exprs(TryExprNodes), [ elvis_result:new_item( "an unexpected nested 'try...catch' expression was found", [], #{node => InnerTryExprNode} ) || InnerTryExprNode <- InnerTryExprNodes ]. inner_try_exprs(TryExprNodes) -> [ TryExprContentNode || TryExprNode <- TryExprNodes, TryExprContentNode <- ktn_code:content(TryExprNode), ktn_code:type(TryExprContentNode) =:= 'try' ]. -spec no_successive_maps(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_successive_maps(Rule, ElvisConfig) -> {nodes, MapExprNodes} = elvis_code:find(#{ of_types => [map], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_successive_map/1, traverse => all }), [ elvis_result:new_item( "an unexpected map update after map construction/update was found", [], #{node => MapExprNode} ) || MapExprNode <- MapExprNodes ]. is_successive_map(MapExprNode) -> InnerVar = ktn_code:node_attr(var, MapExprNode), ktn_code:type(InnerVar) =:= map. -spec atom_naming_convention(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. atom_naming_convention(Rule, ElvisConfig) -> Regex = elvis_rule:option(regex, Rule), ForbiddenRegex = elvis_rule:option(forbidden_regex, Rule), RegexEnclosed0 = elvis_rule:option(enclosed_atoms, Rule), RegexEnclosed = specific_or_default(RegexEnclosed0, Regex), ForbiddenEnclosedRegex0 = elvis_rule:option(forbidden_enclosed_regex, Rule), ForbiddenEnclosedRegex = specific_or_default(ForbiddenEnclosedRegex0, ForbiddenRegex), % Compile each regex once instead of per atom (was the main performance bottleneck). Compiled = #{ allow_normal => re_compile_for_atom_type(false, Regex, RegexEnclosed), allow_enclosed => re_compile_for_atom_type(true, Regex, RegexEnclosed), block_normal => re_compile_for_atom_type(false, ForbiddenRegex, ForbiddenEnclosedRegex), block_enclosed => re_compile_for_atom_type(true, ForbiddenRegex, ForbiddenEnclosedRegex) }, Source = #{ allow_normal => Regex, allow_enclosed => RegexEnclosed, block_normal => ForbiddenRegex, block_enclosed => ForbiddenEnclosedRegex }, % Use content only: 'all' would also traverse node_attrs (location, text, value, etc.), % multiplying visited nodes and making the rule much slower. {nodes_and_ancestors, AtomResults} = elvis_code:find(#{ of_types => [atom], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun parent_is_not_remote/1, filtered_from => node_and_ancestors, traverse => content }), lists:filtermap( fun({AtomNode, _Ancestors}) -> case atom_name_matches_regexes(AtomNode, Compiled, Source) of ok -> false; {not_acceptable, Kind, AtomName, RegexAllowSource} -> {true, elvis_result:new_item( "the name of ~ts ~p is not acceptable by regular " "expression '~s'", [Kind, AtomName, RegexAllowSource], #{node => AtomNode} )}; {blocked, Kind, AtomName, RegexBlockSource} -> {true, elvis_result:new_item( "the name of ~ts ~p is forbidden by regular " "expression '~s'", [Kind, AtomName, RegexBlockSource], #{node => AtomNode} )} end end, AtomResults ). atom_name_matches_regexes(AtomNode, Compiled, Source) -> {AtomName0, AtomNodeValue} = atom_name_and_node_value(AtomNode), {IsEnclosed, AtomName} = string_strip_enclosed(AtomName0), IsExceptionClass = is_exception_or_non_reversible(AtomNodeValue), AllowKey = case IsEnclosed of true -> allow_enclosed; false -> allow_normal end, BlockKey = case IsEnclosed of true -> block_enclosed; false -> block_normal end, RegexAllow = maps:get(AllowKey, Compiled), RegexBlock = maps:get(BlockKey, Compiled), RegexAllowSource = maps:get(AllowKey, Source), RegexBlockSource = maps:get(BlockKey, Source), AtomNameUnicode = unicode:characters_to_list(AtomName), case re_run(AtomNameUnicode, RegexAllow) of _ when IsExceptionClass, not IsEnclosed -> ok; nomatch when not IsEnclosed -> {not_acceptable, "atom", AtomName, RegexAllowSource}; nomatch when IsEnclosed -> {not_acceptable, "enclosed atom", AtomName, RegexAllowSource}; {match, _Captured} when RegexBlock =/= undefined -> % We check for forbidden names only after accepted names case re_run(AtomNameUnicode, RegexBlock) of _ when IsExceptionClass, not IsEnclosed -> ok; {match, _} when not IsEnclosed -> {blocked, "atom", AtomName, RegexBlockSource}; {match, _} when IsEnclosed -> {blocked, "enclosed atom", AtomName, RegexBlockSource}; _ -> ok end; _ -> ok end. atom_name_and_node_value(AtomNode) -> {ktn_code:attr(text, AtomNode), ktn_code:attr(value, AtomNode)}. -spec no_init_lists(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_init_lists(Rule, ElvisConfig) -> ConfigBehaviors = elvis_rule:option(behaviours, Rule), Root = elvis_code:root(Rule, ElvisConfig), InitClauseNodes = case is_behaviour_in(Root, ConfigBehaviors) of true -> {nodes, FunctionNodes} = elvis_code:find(#{ of_types => [function], inside => Root, filtered_by => fun is_init_1/1 }), lists:flatmap( fun(InitFun) -> Content = ktn_code:content(InitFun), case lists:all(fun is_list_clause/1, Content) of true -> Content; false -> [] end end, FunctionNodes ); false -> [] end, [ elvis_result:new_item( "an avoidable list was found as argument to 'init' callback; prefer tuples, maps " "or records", [], #{node => InitClauseNode} ) || InitClauseNode <- InitClauseNodes ]. is_init_1(FunctionNode) -> ktn_code:attr(name, FunctionNode) =:= init andalso ktn_code:attr(arity, FunctionNode) =:= 1. is_list_clause(Clause) -> [Attribute] = ktn_code:node_attr(pattern, Clause), is_list_node(Attribute). is_behaviour_in(Root, ConfigBehaviors) -> {nodes, Behaviours} = elvis_code:find(#{of_types => [behaviour, behavior], inside => Root}), lists:any( fun(BehaviourNode) -> lists:member( ktn_code:attr(value, BehaviourNode), ConfigBehaviors ) end, Behaviours ). is_list_node(#{type := cons}) -> true; is_list_node(#{type := nil}) -> true; is_list_node(#{type := match, content := Content}) -> lists:any(fun is_list_node/1, Content); is_list_node(_) -> false. -spec ms_transform_included(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. ms_transform_included(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), case has_include_ms_transform(Root) of true -> []; false -> {nodes, FunctionNodes} = elvis_code:find(#{ of_types => [call], inside => Root, filtered_by => fun is_ets_fun2ms/1 }), [ elvis_result:new_item( "'ets:fun2ms/1' is used but the module is missing " "'-include_lib(\"stdlib/include/ms_transform.hrl\").'", [], #{node => FunctionNode} ) || FunctionNode <- FunctionNodes ] end. is_ets_fun2ms(Node) -> FunctionInNode = ktn_code:node_attr(function, Node), FunctionRef0 = ktn_code:node_attr(function, FunctionInNode), FunctionRef = ktn_code:attr(value, FunctionRef0), ModuleRef0 = ktn_code:node_attr(module, FunctionInNode), ModuleRef = ktn_code:attr(value, ModuleRef0), {ModuleRef, FunctionRef} =:= {ets, fun2ms}. -spec no_boolean_in_comparison(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_boolean_in_comparison(Rule, ElvisConfig) -> {nodes, OpNodes} = elvis_code:find(#{ of_types => [op], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_boolean_in_comparison/1, traverse => all }), [ elvis_result:new_item( "an avoidable comparison to boolean was found", [], #{node => OpNode} ) || OpNode <- lists:uniq(OpNodes) ]. is_boolean_in_comparison(OpNode) -> is_op(OpNode, ['==', '=:=', '/=', '=/=']) andalso operates_on_boolean(OpNode). is_op(OpNode, Ops) -> Operation = ktn_code:attr(operation, OpNode), lists:member(Operation, Ops). operates_on_boolean(OpNode) -> lists:any( fun(OpContentNode) -> is_boolean(ktn_code:attr(value, OpContentNode)) end, ktn_code:content(OpNode) ). -spec no_receive_without_timeout(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_receive_without_timeout(Rule, ElvisConfig) -> {nodes, ReceiveExprNodes} = elvis_code:find(#{ of_types => ['receive'], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_receive_without_timeout/1 }), [ elvis_result:new_item( "a 'receive' expression was found without an 'after' clause; " "prefer to include 'after' in 'receive' expressions", [], #{node => ReceiveExprNode} ) || ReceiveExprNode <- ReceiveExprNodes ]. is_receive_without_timeout(Receive) -> {nodes, ReceiveAfterNodes} = elvis_code:find(#{ of_types => [receive_after], inside => Receive }), ReceiveAfterNodes =:= []. -spec strict_term_equivalence(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. strict_term_equivalence(Rule, ElvisConfig) -> Operators = #{'==' => '=:=', '/=' => '=/='}, {nodes, OpNodes} = elvis_code:find(#{ of_types => [op], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(OpNode) -> is_op(OpNode, maps:keys(Operators)) end, traverse => all }), [ elvis_result:new_item( "unexpected weak comparison operator ~p was found; prefer ~p", [ ktn_code:attr(operation, OpNode), maps:get(ktn_code:attr(operation, OpNode), Operators) ], #{node => OpNode} ) || OpNode <- OpNodes ]. -spec no_operation_on_same_value(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_operation_on_same_value(Rule, ElvisConfig) -> InterestingOps = elvis_rule:option(operations, Rule), {nodes, OpNodes} = elvis_code:find(#{ of_types => [op], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(OpNode) -> is_op(OpNode, InterestingOps) andalso same_value_on_both_sides(OpNode) end, traverse => all }), [ elvis_result:new_item( "redundant operation '~s' has the same value on both sides", [atom_to_list(ktn_code:attr(operation, OpNode))], #{node => OpNode} ) || OpNode <- lists:uniq(OpNodes) ]. -spec expression_can_be_simplified(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. expression_can_be_simplified(Rule, ElvisConfig) -> Simplifications = elvis_rule:option(simplifications, Rule), {nodes, OpNodes} = elvis_code:find(#{ of_types => [op], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(OpNode) -> is_tuple(recommended_simplification(OpNode, Simplifications)) end, traverse => all }), UniqOpNodes = lists:uniq(OpNodes), lists:map( fun(OpNode) -> {Label, Msg} = recommended_simplification(OpNode, Simplifications), elvis_result:new_item( "expression can be simplified (~s): ~s", [Label, Msg], #{node => OpNode} ) end, UniqOpNodes ). -spec recommended_simplification(ktn_code:tree_node(), [atom()]) -> false | {atom(), string()}. recommended_simplification(OpNode, Simplifications) -> case ktn_code:content(OpNode) of [Operand] -> Op = ktn_code:attr(operation, OpNode), Val = ktn_code:attr(value, Operand), try_unary_patterns(Op, Val, Simplifications); [Left, Right] -> Op = ktn_code:attr(operation, OpNode), try_binary_patterns(Op, Left, Right, Simplifications); _ -> false end. try_unary_patterns('not', false, Simplifications) -> %% Operand is the literal atom false lists:member(not_false, Simplifications) andalso {not_false, "not false can be simplified to true"}; try_unary_patterns('not', true, Simplifications) -> lists:member(not_true, Simplifications) andalso {not_true, "not true can be simplified to false"}; try_unary_patterns(_, _, _) -> false. try_binary_patterns(Op, Left, Right, Simplifications) -> Patterns = [ {'++', fun is_empty_list/1, no_check, list_append_left_empty, "[] ++ X can be simplified to X"}, {'++', no_check, fun is_empty_list/1, list_append_right_empty, "X ++ [] can be simplified to X"}, {'--', fun is_empty_list/1, no_check, list_subtract_from_empty, "[] -- X can be simplified to []"}, {'--', no_check, fun is_empty_list/1, list_subtract_empty, "X -- [] can be simplified to X"}, {'+', no_check, fun is_integer_zero/1, add_zero_right, "X + 0 can be simplified to X"}, {'-', no_check, fun is_integer_zero/1, subtract_zero, "X - 0 can be simplified to X"}, {'-', fun is_integer_zero/1, no_check, subtract_from_zero, "0 - X can be simplified to -X"}, {'*', no_check, fun is_integer_one/1, multiply_by_one_right, "X * 1 can be simplified to X"}, {'*', fun is_integer_one/1, no_check, multiply_by_one_left, "1 * X can be simplified to X"}, {'div', no_check, fun is_integer_one/1, div_by_one, "X div 1 can be simplified to X"}, {'rem', no_check, fun is_integer_one/1, rem_by_one, "X rem 1 can be simplified to 0"}, {'andalso', fun(N) -> is_atom_val(N, true) end, no_check, andalso_true, "true andalso X can be simplified to X"}, {'orelse', fun(N) -> is_atom_val(N, false) end, no_check, orelse_false, "false orelse X can be simplified to X"}, {'band', no_check, fun is_negative_one/1, band_neg_one, "X band -1 can be simplified to X"}, {'bor', no_check, fun is_integer_zero/1, bor_zero, "X bor 0 can be simplified to X"}, {'bxor', no_check, fun is_integer_zero/1, bxor_zero, "X bxor 0 can be simplified to X"} ], Fun = fun({OpPat, LeftOk, RightOk, Label, Msg}) -> Result = Op =:= OpPat andalso lists:member(Label, Simplifications) andalso (LeftOk =:= no_check orelse LeftOk(Left)) andalso (RightOk =:= no_check orelse RightOk(Right)), case Result of true -> {true, {Label, Msg}}; false -> false end end, case lists:filtermap(Fun, Patterns) of [First | _] -> First; [] -> none end. is_empty_list(Node) -> ktn_code:type(Node) =:= nil. is_integer_zero(Node) -> ktn_code:type(Node) =:= integer andalso ktn_code:attr(value, Node) =:= 0. is_integer_one(Node) -> ktn_code:type(Node) =:= integer andalso ktn_code:attr(value, Node) =:= 1. is_atom_val(Node, V) -> ktn_code:type(Node) =:= atom andalso ktn_code:attr(value, Node) =:= V. is_negative_one(Node) -> (ktn_code:type(Node) =:= integer andalso ktn_code:attr(value, Node) =:= -1) orelse (ktn_code:type(Node) =:= op andalso ktn_code:attr(operation, Node) =:= '-' andalso case ktn_code:content(Node) of [One] -> is_integer_one(One); _ -> false end). -spec prefer_robot_butt(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. prefer_robot_butt(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), MaxSize = elvis_rule:option(max_small_list_size, Rule), AllowedEq = lists:seq(0, MaxSize), {nodes, Nodes} = elvis_code:find(#{ of_types => [op, 'case', 'try'], inside => Root, filtered_by => fun(Node) -> is_robot_butt_violation(Node, AllowedEq) end, traverse => all }), lists:map(fun build_robot_butt_warning_for/1, Nodes). is_robot_butt_violation(Node, AllowedEq) -> case ktn_code:type(Node) of op -> is_length_comparison(Node, AllowedEq); 'case' -> tuple_length_matched_on_int(Node, AllowedEq); 'try' -> tuple_length_matched_on_int(Node, AllowedEq); _ -> false end. build_robot_butt_warning_for(Node) -> case ktn_code:type(Node) of op -> build_robot_butt_warning(Node); 'case' -> build_tuple_length_warning(Node); 'try' -> build_tuple_length_warning(Node); _ -> error(badarg, [Node]) end. is_length_comparison(OpNode, AllowedEq) -> Op = ktn_code:attr(operation, OpNode), E = effective_length(Op, OpNode), is_integer(E) andalso lists:member(E, allowed_for_op(Op, AllowedEq)). allowed_for_op('=:=', AllowedEq) -> AllowedEq; allowed_for_op('==', AllowedEq) -> AllowedEq; allowed_for_op(_, _) -> [0, 1]. %% length(L) op M: RHS must be literal integer (no arithmetic on length side). effective_length(Op, OpNode) -> case lists:member(Op, operators()) of true -> case ktn_code:content(OpNode) of [L, R] -> case rhs_int_when_length_left(L, R) of undefined -> rhs_int_when_length_left(R, L); E -> E end; _ -> undefined end; false -> undefined end. rhs_int_when_length_left(LengthSide, IntSide) -> is_length_call(LengthSide) andalso int_value(IntSide). int_value(Node) -> case ktn_code:type(Node) of integer -> ktn_code:attr(value, Node); _ -> undefined end. is_length_call(Node) -> ktn_code:type(Node) =:= call andalso call_mfa(Node) =:= {erlang, length, 1}. build_robot_butt_warning(OpNode) -> Op = ktn_code:attr(operation, OpNode), E = effective_length(Op, OpNode), {Message, Args} = case Op of _ when Op =:= '=:='; Op =:= '==' -> length_eq_message(E); _ -> length_ineq_message(Op) end, elvis_result:new_item(Message, Args, #{node => OpNode}). length_eq_message(0) -> { "prefer pattern matching over 'length/1' comparison: " "length(L) =:= 0 can be replaced by matching on []", [] }; length_eq_message(N) when N >= 1 -> P = "[" ++ lists:join(", ", lists:duplicate(N, "_")) ++ "]", { "prefer pattern matching over 'length/1' comparison: " "length(L) =:= ~w can be replaced by matching on ~s", [N, P] }. length_ineq_message(Op) when Op =:= '>'; Op =:= '>='; Op =:= '=/='; Op =:= '/=' -> { "prefer pattern matching over 'length/1' comparison: " "length(L) ~p 0 etc. can be replaced by matching on [_|_]", [Op] }; length_ineq_message(Op) when Op =:= '<'; Op =:= '=<' -> { "prefer pattern matching over 'length/1' comparison: " "0 ~p length(L) etc. can be replaced by matching on [_|_]", [Op] }. tuple_length_matched_on_int(Node, AllowedEq) -> case ktn_code:type(Node) of 'case' -> [ExprNode | _] = ktn_code:content(Node), Expr = unwrap_case_expr(ExprNode), Clauses = case_clauses_in(Node), direct_length_matched_on_int(Expr, Clauses, AllowedEq) orelse tuple_has_length_matched_on_int(Expr, Clauses, AllowedEq); 'try' -> [TryCase | _] = ktn_code:content(Node), case ktn_code:type(TryCase) of try_case -> OfClauses = ktn_code:content(TryCase), Expr = ensure_single_node(ktn_code:node_attr(expression, TryCase)), undefined =/= Expr andalso (direct_length_matched_on_int(Expr, OfClauses, AllowedEq) orelse tuple_has_length_matched_on_int(Expr, OfClauses, AllowedEq)); _ -> false end; _ -> false end. %% case/try: expression is length/1 call and a clause pattern is 0/1/2 direct_length_matched_on_int(Expr, Clauses, AllowedEq) -> is_length_call(Expr) andalso any_clause_pattern_int_in_allowed(Clauses, AllowedEq). any_clause_pattern_int_in_allowed(Clauses, AllowedEq) -> lists:any( fun(Clause) -> Pattern = clause_pattern(Clause), Pattern =/= undefined andalso is_int_in_allowed(Pattern, AllowedEq) end, Clauses ). unwrap_case_expr(Node) -> case ktn_code:type(Node) of case_expr -> case ktn_code:content(Node) of [Expr | _] -> Expr; _ -> Node end; _ -> Node end. ensure_single_node([N]) when is_map(N) -> N; ensure_single_node([L]) when is_list(L) -> hd(L); ensure_single_node(N) when is_map(N) -> N; ensure_single_node(_) -> undefined. tuple_has_length_matched_on_int(TupleExpr, Clauses, AllowedEq) -> ktn_code:type(TupleExpr) =:= tuple andalso case tuple_length_indices(TupleExpr) of [] -> false; Indices -> any_clause_has_int_at_indices(Clauses, Indices, AllowedEq) end. tuple_length_indices(TupleNode) -> Elements = ktn_code:content(TupleNode), [ I || {I, E} <- lists:enumerate(1, Elements), is_length_call(E) ]. any_clause_has_int_at_indices(Clauses, Indices, AllowedEq) -> lists:any( fun(Clause) -> Pattern = clause_pattern(Clause), Pattern =/= undefined andalso ktn_code:type(Pattern) =:= tuple andalso lists:any( fun(I) -> Elem = tuple_elem(Pattern, I), Elem =/= undefined andalso is_int_in_allowed(Elem, AllowedEq) end, Indices ) end, Clauses ). clause_pattern(Clause) -> case ktn_code:node_attr(pattern, Clause) of [P | _] -> P; _ -> undefined end. tuple_elem(TupleNode, OneBasedIdx) -> try lists:nth(OneBasedIdx, ktn_code:content(TupleNode)) catch error:function_clause -> undefined end. is_int_in_allowed(Node, AllowedEq) -> ktn_code:type(Node) =:= integer andalso lists:member(ktn_code:attr(value, Node), AllowedEq). build_tuple_length_warning(Node) -> Msg = "prefer pattern matching over 'length/1' in tuple: tuple contains length/1 and is " "matched on 0/1/2; prefer matching on the list structure directly (e.g. [] or [_|_])", elvis_result:new_item(Msg, [], #{node => Node}). same_value_on_both_sides(Node) -> case ktn_code:content(Node) of [Left, Right] -> nodes_same_except_location(Left, Right); _ -> false end. nodes_same_except_location([_ | _], []) -> false; nodes_same_except_location([], [_ | _]) -> false; nodes_same_except_location([], []) -> true; nodes_same_except_location([LeftNode | LeftNodes], [RightNode | RigthNodes]) -> nodes_same_except_location(LeftNode, RightNode) andalso nodes_same_except_location(LeftNodes, RigthNodes); nodes_same_except_location(LeftNode, RightNode) -> %% If we're evaluating a function, then even if we evaluate the same function on both sides, %% the results may be different. nodes_not_call(LeftNode, RightNode) andalso nodes_same_type(LeftNode, RightNode) andalso nodes_same_attrs(LeftNode, RightNode) andalso nodes_same_attrs_except_location(LeftNode, RightNode) andalso nodes_same_except_location(ktn_code:content(LeftNode), ktn_code:content(RightNode)). nodes_not_call(LeftNode, RightNode) -> not is_call(LeftNode) andalso not is_call(RightNode). nodes_same_type(LeftNode, RightNode) -> ktn_code:type(LeftNode) =:= ktn_code:type(RightNode). nodes_same_attrs(LeftNode, RightNode) -> maps:remove(location, maps:get(attrs, LeftNode)) =:= maps:remove(location, maps:get(attrs, RightNode)). nodes_same_attrs_except_location(#{node_attrs := LeftAttrs}, #{node_attrs := RightAttrs}) -> nodes_same_attr_keys(LeftAttrs, RightAttrs) andalso lists:all( fun(AttrKey) -> nodes_same_except_location( maps:get(AttrKey, LeftAttrs), maps:get(AttrKey, RightAttrs) ) end, maps:keys(LeftAttrs) ); nodes_same_attrs_except_location(LeftNode, RightNode) -> not maps:is_key(node_attrs, LeftNode) andalso not maps:is_key(node_attrs, RightNode). nodes_same_attr_keys(LeftAttrs, RightAttrs) -> maps:keys(LeftAttrs) =:= maps:keys(RightAttrs). has_include_ms_transform(Root) -> {nodes, IncludeLibNodes} = elvis_code:find(#{ of_types => [include_lib], inside => Root, filtered_by => fun is_ms_transform_hrl/1 }), IncludeLibNodes =/= []. is_ms_transform_hrl(IncludeLibNode) -> ktn_code:attr(value, IncludeLibNode) =:= "stdlib/include/ms_transform.hrl". -spec no_throw(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_throw(Rule, ElvisConfig) -> {nodes, ThrowNodes} = elvis_code:find(#{ of_types => [call], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_throw/1 }), [ elvis_result:new_item( "an avoidable call to 'throw/1' was found; prefer 'exit/1' or 'error/1'", [], #{node => ThrowNode} ) || ThrowNode <- ThrowNodes ]. is_throw(OpNode) -> lists:any( fun(MFA) -> is_call(OpNode, MFA) end, [{throw, 1}, {erlang, throw, 1}] ). -spec no_dollar_space(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_dollar_space(Rule, ElvisConfig) -> {nodes, CharNodes} = elvis_code:find(#{ of_types => [char], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_dollar_space/1, traverse => all }), [ elvis_result:new_item( "unexpected character '$ ' was found; prefer $\\s", [], #{node => CharNode} ) || CharNode <- CharNodes ]. is_dollar_space(CharNode) -> ktn_code:attr(text, CharNode) =:= "$ ". -spec no_author(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_author(Rule, ElvisConfig) -> {nodes, AuthorNodes} = elvis_code:find(#{ of_types => [author], inside => elvis_code:root(Rule, ElvisConfig) }), [ elvis_result:new_item( "avoidable attribute '-author' was found", [], #{node => AuthorNode} ) || AuthorNode <- AuthorNodes ]. -spec no_import(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_import(Rule, ElvisConfig) -> {nodes, ImportNodes} = elvis_code:find(#{ of_types => [import], inside => elvis_code:root(Rule, ElvisConfig) }), [ elvis_result:new_item( "unexpected attribute '-import' was found", [], #{node => ImportNode} ) || ImportNode <- ImportNodes ]. -spec no_catch_expressions(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_catch_expressions(Rule, ElvisConfig) -> {nodes_and_ancestors, CatchExprResults} = elvis_code:find(#{ of_types => ['catch'], inside => elvis_code:root(Rule, ElvisConfig), filtered_from => node_and_ancestors, filtered_by => fun(NA) -> not result_discarded(NA) end }), [ elvis_result:new_item( "an unexpected 'catch' expression was found; prefer a 'try' expression", [], #{node => Node} ) || {Node, _Ancestors} <- lists:uniq(CatchExprResults) ]. %% @doc is this node in a _ = ... expression, where the result is explicitly discarded? result_discarded({_Node, [Parent | _]}) -> case ktn_code:type(Parent) of match -> [LeftSide | _] = ktn_code:content(Parent), ktn_code:type(LeftSide) =:= var andalso ktn_code:attr(name, LeftSide) =:= '_'; _ -> false end; result_discarded({_Node, []}) -> false. -spec no_single_clause_case(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_single_clause_case(Rule, ElvisConfig) -> {nodes, CaseExprs} = elvis_code:find(#{ of_types => ['case'], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_single_clause_case/1 }), [ elvis_result:new_item( "an avoidable single-clause 'case' expression was found", [], #{node => CaseExpr} ) || CaseExpr <- CaseExprs ]. is_single_clause_case(CaseExpr) -> case case_clauses_in(CaseExpr) of [_] -> true; _ -> false end. case_clauses_in(Node) -> [ Clause || SubNode <- ktn_code:content(Node), ktn_code:type(SubNode) =:= case_clauses, Clause <- ktn_code:content(SubNode) ]. -spec no_single_match_maybe(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_single_match_maybe(Rule, ElvisConfig) -> {nodes, MaybeNodes} = elvis_code:find(#{ of_types => ['maybe'], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_single_match_maybe/1 }), [ elvis_result:new_item( "an avoidable single-match 'maybe' block was found", [], #{node => MaybeNode} ) || MaybeNode <- MaybeNodes ]. is_single_match_maybe(MaybeNode) -> case ktn_code:content(MaybeNode) of [_] -> true; _ -> false end. -spec no_match_in_condition(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. no_match_in_condition(Rule, ElvisConfig) -> {nodes, CaseExprNodes} = elvis_code:find(#{ of_types => [case_expr], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(CaseExprNode) -> has_match_child(CaseExprNode) orelse has_match_child_block(CaseExprNode) end }), [ elvis_result:new_item( "an avoidable match condition in a 'case' expression was found; prefer matching " "in 'case' clauses", [], #{node => CaseExprNode} ) || CaseExprNode <- CaseExprNodes ]. is_match(Node) -> lists:member(ktn_code:type(Node), match_operators()). has_match_child(Node) -> %% case_expr followed by a match lists:any(fun is_match/1, ktn_code:content(Node)). has_match_child_block(CaseExprNode) -> %% case_expr followed by a block which contains a match in the first layer lists:any( fun(CaseExprContent) -> ktn_code:type(CaseExprContent) =:= block andalso has_match_child(CaseExprContent) end, ktn_code:content(CaseExprNode) ). -spec numeric_format(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. numeric_format(Rule, ElvisConfig) -> Regex = elvis_rule:option(regex, Rule), IntRegex0 = elvis_rule:option(int_regex, Rule), IntRegex = specific_or_default(IntRegex0, Regex), FloatRegex0 = elvis_rule:option(float_regex, Rule), FloatRegex = specific_or_default(FloatRegex0, Regex), IntRegexCompiled = re_compile(IntRegex), FloatRegexCompiled = re_compile(FloatRegex), Root = elvis_code:root(Rule, ElvisConfig), {nodes, IntegerNodes} = elvis_code:find(#{ of_types => [integer], inside => Root, filtered_by => fun(NumberNode) -> is_not_acceptable_number(NumberNode, IntRegexCompiled) end }), {nodes, FloatNodes} = elvis_code:find(#{ of_types => [float], inside => Root, filtered_by => fun(NumberNode) -> is_not_acceptable_number(NumberNode, FloatRegexCompiled) end }), [ elvis_result:new_item( "the format of number '~s' is not acceptable by regular " "expression '~s'", [ktn_code:attr(text, NumberNode), Regex], #{node => NumberNode} ) || NumberNode <- IntegerNodes ] ++ [ elvis_result:new_item( "the format of number '~s' is not acceptable by regular " "expression '~s'", [ktn_code:attr(text, NumberNode), FloatRegex], #{node => NumberNode} ) || NumberNode <- FloatNodes ]. is_not_acceptable_number(NumberNode, Regex) -> Number = ktn_code:attr(text, NumberNode), Number =/= undefined andalso re_run(Number, Regex) =:= nomatch. -spec prefer_sigils(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. prefer_sigils(Rule, ElvisConfig) -> case string:to_integer(erlang:system_info(otp_release)) of {R, _} when R < 27 -> []; _ -> {nodes, BinaryNodes} = elvis_code:find(#{ of_types => [binary], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_binary_string/1, traverse => all }), lists:map( fun(BinaryNode) -> elvis_result:new_item( "binary string was found; prefer using sigils", [], #{node => BinaryNode} ) end, BinaryNodes ) end. is_binary_string(BinaryNode) -> ktn_code:attr(text, BinaryNode) =:= "<<" andalso empty_or_single_binary_element(ktn_code:content(BinaryNode)). empty_or_single_binary_element([BinaryElement]) -> string =:= ktn_code:type(ktn_code:node_attr(value, BinaryElement)); empty_or_single_binary_element(NonSingleton) -> [] =:= NonSingleton. -spec prefer_unquoted_atoms(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. prefer_unquoted_atoms(Rule, ElvisConfig) -> QuotesRegex = doesnt_need_quotes_regex(), {nodes, AtomNodes} = elvis_code:find(#{ of_types => [atom], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(AtomNode) -> doesnt_need_quotes(AtomNode, QuotesRegex) end, traverse => all }), lists:map( fun(AtomNode) -> elvis_result:new_item( "unnecessarily quoted atom ~s was found; prefer removing the quotes when " "not syntactically required", [ktn_code:attr(text, AtomNode)], #{node => AtomNode} ) end, AtomNodes ). doesnt_need_quotes_regex() -> re_compile("^'[a-z][a-zA-Z0-9_@]*'$"). doesnt_need_quotes(AtomNode, Regex) -> AtomName0 = ktn_code:attr(text, AtomNode), case re:run(AtomName0, Regex, [{capture, none}]) of match -> AtomName = string:trim(AtomName0, both, "'"), % safe-ignore list_to_atom/1 Atom = list_to_atom(AtomName), not lists:member(Atom, ['maybe', 'else']) andalso not erl_scan:f_reserved_word(Atom); _ -> false end. -spec behaviour_spelling(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. behaviour_spelling(Rule, ElvisConfig) -> Spelling = elvis_rule:option(spelling, Rule), {nodes, BehaviourNodes} = elvis_code:find(#{ of_types => [behaviour, behavior], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(BehaviourNode) -> ktn_code:type(BehaviourNode) =/= Spelling end }), [ elvis_result:new_item( "an unexpected spelling of 'behavio[u]r' was found; prefer ~p", [Spelling], #{node => BehaviourNode} ) || BehaviourNode <- BehaviourNodes ]. -spec guard_operators(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. guard_operators(Rule, ElvisConfig) -> case elvis_rule:option(preferred_syntax, Rule) of per_expression -> {nodes_and_ancestors, GuardedClauseResults} = elvis_code:find(#{ of_types => [clause], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun({Node, _Ancestors}) -> has_guards(Node) end, filtered_from => node_and_ancestors, traverse => all }), GuardedExpressionNodes = [Parent || {_Node, [Parent | _]} <- GuardedClauseResults], check_guard_operators(per_expression, GuardedExpressionNodes); PreferredSyntax -> {nodes, GuardedClauseNodes} = elvis_code:find(#{ of_types => [clause], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun has_guards/1 }), check_guard_operators(PreferredSyntax, GuardedClauseNodes) end. check_guard_operators(punctuation, ClauseNodes) -> [ elvis_result:new_item( "an unexpected shortcircuit operator was found; prefer ';' or ','", [], #{node => ClauseNode} ) || ClauseNode <- ClauseNodes, has_guard_defined_with_words(ClauseNode) ]; check_guard_operators(words, ClauseNodes) -> [ elvis_result:new_item( "one or more unexpected punctutation operators were found;" " prefer 'andalso' or 'orelse'", [], #{node => ClauseNode} ) || ClauseNode <- ClauseNodes, has_guard_defined_with_punctuation(ClauseNode) ]; check_guard_operators(per_clause, ClauseNodes) -> [ elvis_result:new_item( "an unexpected combination of punctuation and shortcircuit operators was found", [], #{node => ClauseNode} ) || ClauseNode <- ClauseNodes, has_guard_defined_with_punctuation(ClauseNode), has_guard_defined_with_words(ClauseNode) ]; check_guard_operators(per_expression, ExpressionNodes) -> lists:uniq([ elvis_result:new_item( "an unexpected combination of punctuation and shortcircuit operators was found", [], #{node => ExpressionNode} ) || ExpressionNode <- ExpressionNodes, {nodes, []} =/= elvis_code:find(#{ of_types => [clause], inside => ExpressionNode, filtered_by => fun has_guard_defined_with_punctuation/1 }) andalso {nodes, []} =/= elvis_code:find(#{ of_types => [clause], inside => ExpressionNode, filtered_by => fun has_guard_defined_with_words/1 }) ]). has_guards(ClauseNode) -> [] =/= ktn_code:node_attr(guards, ClauseNode). %% @doc If guards are joined by ; or guard-expressions are joined by , ktn_code reports them %% as lists of lists. %% If they only use words, then we just have [[#{type := op, attrs := #{operation = '...'}}]] has_guard_defined_with_punctuation(ClauseNode) -> case ktn_code:node_attr(guards, ClauseNode) of [_, _ | _] -> true; [[_, _ | _]] -> true; _ -> false end. has_guard_defined_with_words(ClauseNode) -> [] =/= [ GuardExpression || Guard <- ktn_code:node_attr(guards, ClauseNode), GuardExpression <- Guard, is_two_sided_boolean_op(GuardExpression) ]. %% @doc This function returns true for X andalso Y, X orelse Y, X and Y, etc... %% It also returns true for not not not not X andalso Y %% But it returns false for not not not X. is_two_sided_boolean_op(Node) -> op =:= ktn_code:type(Node) andalso lists:member(ktn_code:attr(operation, Node), ['and', 'or', 'andalso', 'orelse']) orelse ktn_code:attr(operation, Node) =:= 'not' andalso is_two_sided_boolean_op(hd(ktn_code:content(Node))). -spec param_pattern_matching(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. param_pattern_matching(Rule, ElvisConfig) -> Side = elvis_rule:option(side, Rule), {nodes_and_ancestors, ClauseResults} = elvis_code:find(#{ of_types => [clause], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun({_Node, Ancestors}) -> is_function_clause(Ancestors, [function, 'fun', named_fun]) end, filtered_from => node_and_ancestors, traverse => all }), MatchesInFunctionClauses = matches_in_function_clauses(ClauseResults), MatchVars = match_vars_at(Side, MatchesInFunctionClauses), [ elvis_result:new_item( "variable '~s' is used to match an argument, but placed on " "the wrong side of it; prefer the ~p side", [atom_to_list(ktn_code:attr(name, Var)), Side], #{node => Match} ) || {Match, Var} <- MatchVars ]. match_vars_at(Side, MatchesInFunctionClauses) -> lists:filtermap( fun(Match) -> case lists:map(fun ktn_code:type/1, ktn_code:content(Match)) of [var, var] -> false; [var, _] when Side =:= right -> [Var, _] = ktn_code:content(Match), {true, {Match, Var}}; [_, var] when Side =:= left -> [_, Var] = ktn_code:content(Match), {true, {Match, Var}}; _ -> false end end, MatchesInFunctionClauses ). matches_in_function_clauses(ClauseResults) -> lists:append( lists:map( fun({ClauseNode, _Ancestors}) -> ClausePatterns = ktn_code:node_attr(pattern, ClauseNode), lists:filter( fun(ClausePattern) -> ktn_code:type(ClausePattern) =:= match end, ClausePatterns ) end, ClauseResults ) ). is_function_clause([Parent | _], ParentNodeTypes) -> lists:member(ktn_code:type(Parent), ParentNodeTypes); is_function_clause([], _ParentNodeTypes) -> false. -spec generic_type(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. generic_type(Rule, ElvisConfig) -> PreferredType = elvis_rule:option(preferred_type, Rule), {nodes, TypeNodes} = elvis_code:find(#{ of_types => [type, callback], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(TypeNode) -> is_inconsistent_generic_type(TypeNode, PreferredType) end, traverse => all }), [ elvis_result:new_item( "unexpected type '~p/0' was found; prefer ~p/0", [ktn_code:attr(name, TypeNode), PreferredType], #{node => TypeNode} ) || TypeNode <- TypeNodes ]. is_inconsistent_generic_type(TypeNode, PreferredType) -> TypeNodeName = ktn_code:attr(name, TypeNode), IsTermOrAny = lists:member(TypeNodeName, [term, any]), IsTermOrAny andalso TypeNodeName =/= PreferredType. -spec always_shortcircuit(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. always_shortcircuit(Rule, ElvisConfig) -> Operators = #{'and' => 'andalso', 'or' => 'orelse'}, {nodes, OpNodes} = elvis_code:find(#{ of_types => [op], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(OpNode) -> is_op(OpNode, maps:keys(Operators)) end, traverse => all }), [ elvis_result:new_item( "unexpected non-shortcircuiting operator ~p was found; prefer ~p", [ ktn_code:attr(operation, OpNode), maps:get(ktn_code:attr(operation, OpNode), Operators) ], #{node => OpNode} ) || OpNode <- OpNodes ]. -spec simplify_anonymous_functions(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. simplify_anonymous_functions(Rule, ElvisConfig) -> {nodes, FunNodes} = elvis_code:find(#{ of_types => ['fun'], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun is_simple_anonymous_function/1, traverse => all }), [ elvis_result:new_item( "an unnecessary anonymous function wrapper was found; prefer 'fun M:F/A'", [], #{node => Fun} ) || Fun <- FunNodes ]. %% @doc Has this anonymous function just one clause that is a call to a %% regular function with the same args? i.e., something like %% fun() -> x() end ; or %% fun(A) -> some:funct(A) end ; or %% fun(A, B, C) -> x:y(A, B, C) end %% Note that we assume that FunNode is, in fact, an anonymous function node. is_simple_anonymous_function(FunNode) -> case elvis_code:find(#{ of_types => [clause], inside => FunNode }) of {nodes, [Clause]} -> case ktn_code:content(Clause) of [Expression] -> ktn_code:node_attr(guards, Clause) =:= [] andalso ktn_code:type(Expression) =:= call andalso nodes_same_except_location( ktn_code:node_attr(pattern, Clause), ktn_code:content(Expression) ); _ -> false end; _ -> false end. -spec prefer_strict_generators(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. prefer_strict_generators(Rule, ElvisConfig) -> case string:to_integer(erlang:system_info(otp_release)) of {R, _} when R < 28 -> []; _ -> WeakGenerators = #{b_generate => '<=', generate => '<-', m_generate => '<-'}, StrictGenerators = #{b_generate => '<:=', generate => '<:-', m_generate => '<:-'}, {nodes, GenNodes} = elvis_code:find(#{ of_types => maps:keys(WeakGenerators), inside => elvis_code:root(Rule, ElvisConfig), traverse => all }), [ elvis_result:new_item( "unexpected weak generator ~p was found; prefer ~p", [ maps:get(ktn_code:type(GenNode), WeakGenerators), maps:get(ktn_code:type(GenNode), StrictGenerators) ], #{node => GenNode} ) || GenNode <- GenNodes ] end. -spec prefer_include(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. prefer_include(Rule, ElvisConfig) -> {nodes, FunNodes} = elvis_code:find(#{ of_types => [include_lib], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(#{attrs := #{value := Value}}) -> filename:basename(Value) =:= Value end, traverse => all }), [ elvis_result:new_item( "an unexpected '-include_lib' was found; prefer '-include'", [], #{node => Fun} ) || Fun <- FunNodes ]. -spec export_used_types(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. export_used_types(Rule, ElvisConfig) -> Root = elvis_code:root(Rule, ElvisConfig), %% Single traversal to collect all needed node types. {nodes, AllNodes} = elvis_code:find(#{ of_types => [behaviour, behavior, export, export_type, spec, type_attr], inside => Root }), #{ behaviours := BehaviourNodes, exports := ExportNodes, export_types := ExportTypeNodes, specs := SpecNodes0, type_attrs := TypeAttrNodes } = partition_nodes(AllNodes, #{ behaviour => behaviours, behavior => behaviours, export => exports, export_type => export_types, spec => specs, type_attr => type_attrs }), case is_otp_behaviour(BehaviourNodes) of false -> ExportedFunctionsSet = sets:from_list( lists:flatmap(fun(N) -> ktn_code:attr(value, N) end, ExportNodes), [{version, 2}] ), ExportedTypesSet = sets:from_list( lists:flatmap(fun(N) -> ktn_code:attr(value, N) end, ExportTypeNodes), [{version, 2}] ), SpecNodes = [S || S <- SpecNodes0, is_exported_function(S, ExportedFunctionsSet)], UsedTypes = used_types(SpecNodes), UnexportedUsedTypes = [T || T <- UsedTypes, not sets:is_element(T, ExportedTypesSet)], Locations = type_attr_locations(TypeAttrNodes), lists:map( fun({Name, Arity}) -> {Line, Column} = maps:get({Name, Arity}, Locations, {-1, -1}), elvis_result:new_item( "type '~p/~p' is used by an exported function; prefer to also export the " "type", [Name, Arity], #{line => Line, column => Column} ) end, UnexportedUsedTypes ); true -> [] end. partition_nodes(Nodes, TypeToKey) -> Init = maps:from_keys(lists:usort(maps:values(TypeToKey)), []), lists:foldl( fun(Node, Acc) -> Key = maps:get(ktn_code:type(Node), TypeToKey), Acc#{Key := [Node | maps:get(Key, Acc)]} end, Init, Nodes ). is_otp_behaviour([]) -> false; is_otp_behaviour(BehaviourNodes) -> OtpSet = sets:from_list([gen_server, gen_event, gen_fsm, gen_statem, supervisor_bridge]), Names = lists:map(fun(Node) -> ktn_code:attr(value, Node) end, BehaviourNodes), BehaviorsSet = sets:from_list(Names), not sets:is_empty(sets:intersection(OtpSet, BehaviorsSet)). type_attr_locations(TypeAttrNodes) -> lists:foldl( fun (#{attrs := #{location := Loc, name := Name}, node_attrs := #{args := Args}}, Acc) -> maps:put({Name, length(Args)}, Loc, Acc); (_, Acc) -> Acc end, #{}, TypeAttrNodes ). used_types(SpecNodes) -> lists:usort( lists:flatmap( fun(SpecNode) -> {nodes, UserTypeNodes} = elvis_code:find(#{ of_types => [user_type], inside => SpecNode, traverse => all }), % yes, on a -type line, the arity is based on `args`, but on % a -spec line, it's based on `content` [ {Name, length(Vars)} || #{attrs := #{name := Name}, content := Vars} <- UserTypeNodes ] end, SpecNodes ) ). -spec private_data_types(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. private_data_types(Rule, ElvisConfig) -> TypesToCheck = elvis_rule:option(apply_to, Rule), Root = elvis_code:root(Rule, ElvisConfig), ExportedTypesSet = exported_types(Root), PublicDataTypes = public_data_types(TypesToCheck, Root, ExportedTypesSet), Locations = map_type_declarations_to_location(Root), lists:map( fun({Name, Arity} = Info) -> {Line, Column} = maps:get(Info, Locations, {-1, -1}), elvis_result:new_item( "private data type '~p/~p' is exported; prefer not exporting it or making it " "opaque", [Name, Arity], #{line => Line, column => Column} ) end, PublicDataTypes ). public_data_types(TypesToCheck, Root, ExportedTypesSet) -> {nodes, TypeAttrNodes} = elvis_code:find(#{ of_types => [type_attr], inside => Root, filtered_by => fun(TypeAttrNode) -> is_public_data_type_in(TypesToCheck, TypeAttrNode) end, traverse => all }), NameArities = lists:map( fun(#{attrs := #{name := Name}, node_attrs := #{args := Args}}) -> {Name, length(Args)} end, TypeAttrNodes ), lists:filter( fun(NameArity) -> sets:is_element(NameArity, ExportedTypesSet) end, NameArities ). is_public_data_type_in(TypesToCheck, TypeAttrNode) -> TypeAttrNodeType = ktn_code:node_attr(type, TypeAttrNode), TypeAttrNodeType =/= undefined andalso lists:member(ktn_code:attr(name, TypeAttrNodeType), TypesToCheck). -spec macro_definition_parentheses(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. macro_definition_parentheses(Rule, ElvisConfig) -> StringifiedRegex = is_stringified_function_regex(), TypeMismatchingDefine = fun(#{attrs := #{value := [Elem1, Elem2]}}) -> case {macro_attr_type(Elem1), macro_attr_type(Elem2)} of {call, call} -> false; {call, op} -> false; {call, _} -> macro_define_call_args(Elem1); {var, tree} -> is_stringified_function(get_tree_content(Elem2), StringifiedRegex); _ -> false end end, {nodes, InvalidMacroNodes} = elvis_code:find(#{ of_types => [define], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => TypeMismatchingDefine }), lists:map( fun(Node) -> elvis_result:new_item( "Invalid parenthesis at a macro definition." "Functions should contain parenthesis, constants should not", [], #{node => Node} ) end, InvalidMacroNodes ). macro_attr_type({Type, _, _}) -> Type; macro_attr_type({Type, _, _, _}) -> Type; macro_attr_type({Type, _, _, _, _}) -> Type. %% Only -define(NAME(), rhs) (empty arg list) can be a constant with stray (). macro_define_call_args({call, _, _, []}) -> true; macro_define_call_args(_) -> false. is_stringified_function_regex() -> re_compile("^[a-zA-Z_][a-zA-Z0-9_]* ?:? ?[a-zA-Z0-9_]*\\([^)]*\\)$"). is_stringified_function(Tree, StringifiedRegex) -> match =:= re:run(Tree, StringifiedRegex, [{capture, none}]). get_tree_content({tree, text, _, Content}) -> Content. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Private %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec map_type_declarations_to_location(ktn_code:tree_node()) -> #{{atom(), number()} => number()}. map_type_declarations_to_location(Root) -> {nodes, AllTypes} = elvis_code:find(#{of_types => [type_attr], inside => Root}), lists:foldl( fun ( #{ attrs := #{location := Location, name := Name}, node_attrs := #{args := Args} }, Acc ) -> maps:put({Name, length(Args)}, Location, Acc); (_, Acc) -> Acc end, #{}, AllTypes ). specific_or_default(same, Regex) -> Regex; specific_or_default(RegexEnclosed, _Regex) -> RegexEnclosed. is_exception_or_non_reversible(error) -> true; is_exception_or_non_reversible(exit) -> true; is_exception_or_non_reversible(throw) -> true; is_exception_or_non_reversible(non_reversible_form) -> true; is_exception_or_non_reversible(_) -> false. 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, undefined, _) -> undefined; re_compile_for_atom_type(false, Regex, _) -> re_compile(Regex); re_compile_for_atom_type(true, _, undefined) -> undefined; re_compile_for_atom_type(true, _, RegexEnclosed) -> re_compile(RegexEnclosed). line_is_comment_regex() -> re_compile("^[ \t]*%"). line_is_comment(Line, Regex) -> case re_run(Line, Regex) of nomatch -> false; {match, _} -> true end. line_is_whitespace_regex() -> re_compile("^[ \t]*$"). line_is_whitespace(Line, Regex) -> case re_run(Line, Regex) of nomatch -> false; {match, _} -> true end. original_macro_name_from(MacroNode) -> MacroNodeValue = ktn_code:attr(value, MacroNode), MacroAsAtom = macro_as_atom(false, [call, var, atom], MacroNodeValue), atom_to_list(MacroAsAtom). stripped_macro_name_from(MacroNode) -> MacroNameOriginal = original_macro_name_from(MacroNode), unicode:characters_to_list(string:strip(MacroNameOriginal, both, $')). 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; Type =:= atom -> MacroAsAtom; macro_as_atom(false, [Type | OtherTypes], MacroNodeValue) -> macro_as_atom(lists:keyfind(Type, _N = 1, MacroNodeValue), OtherTypes, MacroNodeValue). generate_space_check_results( Lines, UnfilteredNodes, {Position, Text}, {How0, _} = How ) -> Nodes = lists:filter( fun(Node) -> ktn_code:attr(text, Node) =:= Text orelse (ktn_code:type(Node) =:= dot andalso Text =:= ".") end, UnfilteredNodes ), lists:filtermap( fun(Node) -> Location = ktn_code:attr(location, Node), case character_at_location(Position, Lines, Text, Location, How) of Char when Char =:= $\s, How0 =:= should_have -> false; Char when Char =/= $\s, How0 =:= should_not_have -> false; _ when How0 =:= should_have -> {true, elvis_result:new_item( "there is a missing space to the ~p of '~s'", [Position, Text], #{node => Node} )}; _ when How0 =:= should_not_have -> {true, elvis_result:new_item( "an unexpected space was found to the ~p of '~s'", [Position, Text], #{node => Node} )} end end, Nodes ). maybe_re_run(_Line, undefined = _Regex) -> nomatch; maybe_re_run(Line, Regex) -> re_run(Line, Regex). -spec character_at_location( Position :: atom(), LinesIndex :: tuple(), Text :: string(), Location :: {integer(), integer()}, How :: {should_have, []} | {should_not_have, [{string(), {ok, MP}}]} ) -> char() when % MP is re:mp/0 MP :: _. character_at_location( Position, LinesIndex, Text, {LineNo, Col}, {How, TextRegexes} ) -> {Line, CharsTuple, LineLen} = element(LineNo, LinesIndex), TextRegex = case TextRegexes of [] -> nomatch; _ -> maybe_re_run(Line, proplists:get_value(Text, TextRegexes)) end, ColToCheck = case Position of left -> Col - 1; right -> Col + length(Text) end, % If ColToCheck is greater than the length of the line, it % means the end of line was reached so return " " (or "") to make the check pass, % otherwise return the character at the given column. % NOTE: text below only applies when the given Position is equal to `right`, % or Position is equal to `left` and Col is 1. SpaceChar = $\s, case {ColToCheck, Position, LineLen} of {0, _, _} when Text =/= ")" -> SpaceChar; {_, right, LenLine} when How =:= should_have andalso ColToCheck > LenLine -> SpaceChar; {_, right, LenLine} when How =:= should_not_have andalso ColToCheck > LenLine -> ""; _ when How =:= should_have orelse TextRegex =:= nomatch andalso ColToCheck > 1 -> element(ColToCheck, CharsTuple); _ when How =:= should_not_have andalso ColToCheck > 1 andalso (Text =:= ":" orelse Text =:= "." orelse Text =:= ";") -> element(ColToCheck - 1, CharsTuple); _ -> "" end. %% @doc Takes a node and returns all nodes where the nesting limit is exceeded. -spec past_nesting_limit(ktn_code:tree_node(), integer()) -> [{ktn_code:tree_node(), integer()}]. past_nesting_limit(Node, MaxLevel) -> past_nesting_limit(Node, 0, MaxLevel). past_nesting_limit(Node, CurrentLevel, MaxLevel) when CurrentLevel > MaxLevel -> [{Node, CurrentLevel}]; past_nesting_limit(#{content := Content}, CurrentLevel, MaxLevel) -> Fun = fun(ChildNode) -> Increment = level_increment(ChildNode), past_nesting_limit(ChildNode, Increment + CurrentLevel, MaxLevel) end, lists:flatmap(Fun, Content); past_nesting_limit(_Node, _CurrentLeve, _MaxLevel) -> []. %% @doc Takes a node and determines its nesting level increment. level_increment(#{type := 'fun', content := _}) -> 1; level_increment(#{type := 'fun'}) -> 0; level_increment(#{type := Type}) -> IncrementOne = [function, 'case', 'if', try_case, try_catch, named_fun, receive_case, 'maybe'], case lists:member(Type, IncrementOne) of true -> 1; false -> 0 end. 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 -> Module = ktn_code:node_attr(module, FunctionSpec), Function = ktn_code:node_attr(function, FunctionSpec), (ktn_code:type(Module) =/= atom andalso not is_the_module_macro(Module)) orelse ktn_code:type(Function) =/= atom; _Other -> false end; _ -> false end. is_the_module_macro(Module) -> ktn_code:type(Module) =:= macro andalso ktn_code:attr(name, Module) =:= "MODULE". is_var(Node, TokenIndex) -> PrevLocation = case ktn_code:attr(location, Node) of {L, 1} -> {L - 1, 9999}; {L, C} -> {L, C - 1} end, case TokenIndex of #{PrevLocation := PrevToken} -> ktn_code:type(PrevToken) =/= '?'; #{} -> true end. build_token_index(Root) -> Tokens = ktn_code:attr(tokens, Root), lists:foldl(fun index_token/2, #{}, Tokens). index_token(#{attrs := #{location := {Line, Col}}} = Token, Acc) -> Text = ktn_code:attr(text, Token), Len = length(Text), lists:foldl( fun(C, A) -> A#{{Line, C} => Token} end, Acc, lists:seq(Col, Col + Len - 1) ); index_token(_, Acc) -> Acc. is_ignored_var({Node, Ancestors}) -> case ktn_code:type(Node) of var -> Name = ktn_code:attr(name, Node), [FirstChar | _] = atom_to_list(Name), (FirstChar =:= $_) andalso (Name =/= '_') andalso not is_first_child_of_match_or_macro(Node, Ancestors); _OtherType -> false end. is_first_child_of_match_or_macro(_Node, []) -> false; is_first_child_of_match_or_macro(Node, [Parent | Rest]) -> IsMatchOrMacro = lists:member(ktn_code:type(Parent), [macro | match_operators()]), case IsMatchOrMacro of true -> case ktn_code:content(Parent) of [FirstChild | _] -> ktn_code:attr(location, FirstChild) =:= ktn_code:attr(location, Node); _ -> false end; false -> is_first_child_of_match_or_macro(Parent, Rest) end. parent_is_not_remote({_Node, []}) -> true; parent_is_not_remote({_Node, [Parent | _]}) -> ktn_code:type(Parent) =/= remote. find_repeated_nodes(Root, MinComplexity) -> TypeAttrs = #{var => [location, name, text], clause => [location, text]}, FoldFun = fun(Node, Map) -> case node_size(Node) of Count when Count >= MinComplexity -> Loc = ktn_code:attr(location, Node), StrippedNode = remove_attrs(Node, TypeAttrs), ValsSet = maps:get(StrippedNode, Map, sets:new()), NewValsSet = sets:add_element(Loc, ValsSet), maps:put(StrippedNode, NewValsSet, Map); _ -> Map end end, Grouped = fold_content(FoldFun, #{}, Root), Repeated = filter_repeated(Grouped), LocationSets = maps:values(Repeated), Locations = lists:map(fun sets:to_list/1, LocationSets), lists:map(fun lists:sort/1, Locations). node_size(#{content := Content}) -> 1 + lists:sum([node_size(Child) || Child <- Content]); node_size(Node) when is_map(Node) -> 1; node_size(_) -> 0. fold_content(Fun, Acc, #{content := Content} = Node) -> Acc1 = Fun(Node, Acc), lists:foldl(fun(Child, A) -> fold_content(Fun, A, Child) end, Acc1, Content); fold_content(Fun, Acc, Node) when is_map(Node) -> Fun(Node, Acc); fold_content(_Fun, Acc, _) -> Acc. -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, content := Content } = Node, TypeAttrs ) -> AttrsName = maps:get(Type, TypeAttrs, [location]), AttrsNoLoc = maps:without(AttrsName, Attrs), NodeAttrsNoLoc = maps:map(fun(_Key, Value) -> remove_attrs(Value, TypeAttrs) end, NodeAttrs), ContentNoLoc = [remove_attrs(Child, TypeAttrs) || Child <- Content], Node#{attrs => AttrsNoLoc, node_attrs => NodeAttrsNoLoc, content => ContentNoLoc}; remove_attrs( #{ attrs := Attrs, type := Type, node_attrs := NodeAttrs } = Node, TypeAttrs ) -> AttrsName = maps:get(Type, TypeAttrs, [location]), AttrsNoLoc = maps:without(AttrsName, Attrs), NodeAttrsNoLoc = maps:map(fun(_Key, Value) -> remove_attrs(Value, TypeAttrs) end, NodeAttrs), Node#{attrs => AttrsNoLoc, node_attrs => NodeAttrsNoLoc}; remove_attrs(#{attrs := Attrs, type := Type, content := Content} = Node, TypeAttrs) -> AttrsName = maps:get(Type, TypeAttrs, [location]), AttrsNoLoc = maps:without(AttrsName, Attrs), ContentNoLoc = [remove_attrs(Child, TypeAttrs) || Child <- Content], Node#{attrs => AttrsNoLoc, content => ContentNoLoc}; 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) -> case ktn_code:content(Parent) of [] -> false; Content -> lists:any( fun(Child) -> Child =:= Node orelse is_children(Child, Node) end, Content ) end. no_call_common(Rule, ElvisConfig, NoCallFuns, Msg) -> {nodes, CallNodes} = elvis_code:find(#{ of_types => [call], inside => elvis_code:root(Rule, ElvisConfig), filtered_by => fun(CallNode) -> is_in_call_list(CallNode, NoCallFuns) end }), lists:map( fun(CallNode) -> {M, F, A} = call_mfa(CallNode), elvis_result:new_item( Msg, [M, F, A], #{node => CallNode} ) end, CallNodes ). is_in_call_list(Call, DisallowedFuns) -> MFA = call_mfa(Call), MatchFun = fun(Spec) -> fun_spec_match(Spec, MFA) end, lists:any(MatchFun, DisallowedFuns). call_mfa(Call) -> FunctionSpec = ktn_code:node_attr(function, Call), M0 = ktn_code:attr(value, ktn_code:node_attr(module, FunctionSpec)), M = case M0 of undefined -> % this is a bare call, e.g. list_to_atom/1; assume 'erlang' erlang; _ -> M0 end, F0 = ktn_code:attr(value, ktn_code:node_attr(function, FunctionSpec)), F = case F0 of undefined -> % this is what we get for local (or erlang:) calls ktn_code:attr(value, FunctionSpec); _ -> F0 end, A = length(ktn_code:content(Call)), {M, F, A}. is_call(Node, {F, A}) -> list_to_atom(ktn_code:attr(text, Node)) =:= F andalso length(ktn_code:content(Node)) =:= A; is_call(Node, {M, F, A}) -> call_mfa(Node) =:= {M, F, A}. is_call(Node) -> ktn_code:type(Node) =:= call. fun_spec_match({M, F}, MFA) -> fun_spec_match({M, F, '_'}, MFA); fun_spec_match({M1, F1, A1}, {M2, F2, A2}) -> wildcard_match(M1, M2) andalso wildcard_match(F1, F2) andalso wildcard_match(A1, A2). wildcard_match('_', _) -> true; wildcard_match(X, Y) -> X =:= Y. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Internal Function Definitions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% tokens_as_content(Root) -> % Minor trick to have elvis_code assume searches the way it usually does #{type => root, content => ktn_code:attr(tokens, Root)}. -spec doc_bin_parts(Src) -> [Part] when Src :: binary(), Part :: binary_part(). doc_bin_parts(Src) when is_binary(Src) -> RE = "(?ms)^-(?:(moduledoc|doc))\\b\\s*(\"*)?.*?\\2\\.(\\r\\n|\\n)", case re_run(Src, RE, [global, {capture, first, index}, unicode]) of {match, Parts} -> [Part || [Part] <- Parts]; nomatch -> [] end. -spec ignore_bin_parts(Src, [DocPart]) -> [TextPart] when Src :: binary(), DocPart :: binary_part(), TextPart :: binary_part(). ignore_bin_parts(Src, DocParts) when is_binary(Src), is_list(DocParts) -> ignore_bin_parts_1(DocParts, 0, Src). ignore_bin_parts_1([], Prev, Src) -> [{Prev, byte_size(Src) - Prev}]; ignore_bin_parts_1([{Start, Len} | T], Prev, Src) -> [{Prev, Start - Prev} | ignore_bin_parts_1(T, Start + Len, Src)]. -spec bin_parts_to_iolist(Src, Parts) -> iolist() when Src :: binary(), Parts :: [binary_part()]. bin_parts_to_iolist(Src, Parts) when is_binary(Src), is_list(Parts) -> [binary_part(Src, Start, Len) || {Start, Len} <- Parts]. -spec code_complexity(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. code_complexity(Rule, ElvisConfig) -> MaxComplexity = elvis_rule:option(max_complexity, Rule), Root = elvis_code:root(Rule, ElvisConfig), {nodes, FunctionNodes} = elvis_code:find(#{ of_types => [function], inside => Root }), lists:filtermap( fun(FunctionNode) -> Complexity = compute_cyclomatic_complexity(FunctionNode), MaxComplexity < Complexity andalso {true, elvis_result:new_item( "the cyclomatic complexity of '~p/~p' is ~p," " which exceeds the configured maximum of ~p", [ ktn_code:attr(name, FunctionNode), ktn_code:attr(arity, FunctionNode), Complexity, MaxComplexity ], #{node => FunctionNode, limit => MaxComplexity} )} end, FunctionNodes ). %% Computes cyclomatic complexity for a function node. %% Base complexity is 1. Each additional function clause adds 1. %% Then we recursively count decision points in the function body. -spec compute_cyclomatic_complexity(ktn_code:tree_node()) -> pos_integer(). compute_cyclomatic_complexity(FunctionNode) -> ExtraClauses = max(0, count_direct_clauses(FunctionNode) - 1), 1 + ExtraClauses + cyclomatic_decision_points(FunctionNode). %% Recursively counts decision points in a node's subtree. %% Skips anonymous functions (their complexity is their own). -spec cyclomatic_decision_points(ktn_code:tree_node()) -> non_neg_integer(). cyclomatic_decision_points(#{type := Type}) when Type =:= 'fun'; Type =:= named_fun -> 0; cyclomatic_decision_points(#{content := Content}) -> lists:sum([ cyclomatic_points_for(Child) + cyclomatic_decision_points(Child) || Child <- Content ]); cyclomatic_decision_points(_) -> 0. %% Returns the number of decision points introduced by a single node. -spec cyclomatic_points_for(ktn_code:tree_node()) -> non_neg_integer(). cyclomatic_points_for(#{type := Type} = Node) when Type =:= 'case'; Type =:= 'if'; Type =:= receive_case; Type =:= try_case; Type =:= try_catch; Type =:= 'maybe'; Type =:= 'receive' -> max(0, count_direct_clauses(Node) - 1); cyclomatic_points_for(#{type := op} = Node) -> case ktn_code:attr(operation, Node) of 'andalso' -> 1; 'orelse' -> 1; _ -> 0 end; cyclomatic_points_for(_) -> 0. -spec abc_size(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()]. abc_size(Rule, ElvisConfig) -> MaxAbcSize = elvis_rule:option(max_abc_size, Rule), Root = elvis_code:root(Rule, ElvisConfig), {nodes, FunctionNodes} = elvis_code:find(#{ of_types => [function], inside => Root }), lists:filtermap( fun(FunctionNode) -> {A, B, C, Magnitude} = compute_abc_size(FunctionNode), MaxAbcSize < Magnitude andalso {true, elvis_result:new_item( "the ABC size of '~p/~p' is ~.1f (<~p, ~p, ~p>)," " which exceeds the configured maximum of ~p", [ ktn_code:attr(name, FunctionNode), ktn_code:attr(arity, FunctionNode), Magnitude, A, B, C, MaxAbcSize ], #{node => FunctionNode, limit => MaxAbcSize} )} end, FunctionNodes ). %% Computes the ABC size vector and its magnitude for a function node. -spec compute_abc_size(ktn_code:tree_node()) -> {non_neg_integer(), non_neg_integer(), non_neg_integer(), float()}. compute_abc_size(FunctionNode) -> A = count_abc_nodes(FunctionNode, fun add_assignment/1), B = count_abc_nodes(FunctionNode, fun add_branch/1), C = count_abc_conditions(FunctionNode), Magnitude = math:sqrt(A * A + B * B + C * C), {A, B, C, Magnitude}. %% Recursively counts nodes matching a predicate, skipping anonymous functions. -spec count_abc_nodes(ktn_code:tree_node(), fun((ktn_code:tree_node()) -> 0 | 1)) -> non_neg_integer(). count_abc_nodes(#{type := Type}, _Pred) when Type =:= 'fun'; Type =:= named_fun -> 0; count_abc_nodes(#{content := Content} = Node, Pred) -> Own = Pred(Node), Own + lists:sum([count_abc_nodes(Child, Pred) || Child <- Content]); count_abc_nodes(Node, Pred) -> Pred(Node). %% Counts conditions: branching clauses beyond first + andalso/orelse + comparison ops. -spec count_abc_conditions(ktn_code:tree_node()) -> non_neg_integer(). count_abc_conditions(#{type := Type}) when Type =:= 'fun'; Type =:= named_fun -> 0; count_abc_conditions(#{content := Content} = Node) -> Own = abc_condition_points(Node), Own + lists:sum([count_abc_conditions(Child) || Child <- Content]); count_abc_conditions(_) -> 0. -spec abc_condition_points(ktn_code:tree_node()) -> non_neg_integer(). abc_condition_points(#{type := Type} = Node) when Type =:= 'case'; Type =:= 'if'; Type =:= receive_case; Type =:= try_case; Type =:= try_catch; Type =:= 'maybe'; Type =:= 'receive' -> max(0, count_direct_clauses(Node) - 1); abc_condition_points(#{type := op} = Node) -> case ktn_code:attr(operation, Node) of Op when Op =:= 'andalso'; Op =:= 'orelse'; Op =:= 'and'; Op =:= 'or'; Op =:= '=='; Op =:= '/='; Op =:= '=:='; Op =:= '=/='; Op =:= '<'; Op =:= '>'; Op =:= '=<'; Op =:= '>=' -> 1; _ -> 0 end; abc_condition_points(_) -> 0. %% Checks if a node is an assignment (match expression). -spec add_assignment(ktn_code:tree_node()) -> 0 | 1. add_assignment(#{type := match}) -> 1; add_assignment(_) -> 0. %% Checks if a node is a branch (function call). -spec add_branch(ktn_code:tree_node()) -> 0 | 1. add_branch(#{type := call}) -> 1; add_branch(_) -> 0. %% Counts direct clause children of a node (not deeply nested ones). -spec count_direct_clauses(ktn_code:tree_node()) -> non_neg_integer(). count_direct_clauses(Node) -> lists:foldl( fun(C, Acc) -> case ktn_code:type(C) of clause -> Acc + 1; _ -> Acc end end, 0, ktn_code:content(Node) ). line(Node) -> {Line, _} = ktn_code:attr(location, Node), Line. re_run(Subject, RE) -> re_run(Subject, RE, []). re_run(Subject, RE, Options) -> re:run(Subject, RE, Options). re_compile(Regexp) -> re_compile(Regexp, [unicode]). re_compile(undefined, _Options) -> undefined; re_compile(Regexp, Options) -> {ok, MP} = re:compile(Regexp, Options), MP. operators() -> ['=:=', '==', '>', '>=', '<', '=<', '=/=', '/='].