cyclomatic_complexity.ex (4593B)
1 defmodule Credo.Check.Refactor.CyclomaticComplexity do 2 use Credo.Check, 3 param_defaults: [max_complexity: 9], 4 explanations: [ 5 check: """ 6 Cyclomatic complexity (CC) is a software complexity metric closely 7 correlated with coding errors. 8 9 If a function feels like it's gotten too complex, it more often than not also 10 has a high CC value. So, if anything, this is useful to convince team members 11 and bosses of a need to refactor parts of the code based on "objective" 12 metrics. 13 """, 14 params: [ 15 max_complexity: "The maximum cyclomatic complexity a function should have." 16 ] 17 ] 18 19 @def_ops [:def, :defp, :defmacro] 20 # these have two outcomes: it succeeds or does not 21 @double_condition_ops [:if, :unless, :for, :try, :and, :or, :&&, :||] 22 # these can have multiple outcomes as they are defined in their do blocks 23 @multiple_condition_ops [:case, :cond] 24 @op_complexity_map [ 25 def: 1, 26 defp: 1, 27 defmacro: 1, 28 if: 1, 29 unless: 1, 30 for: 1, 31 try: 1, 32 and: 1, 33 or: 1, 34 &&: 1, 35 ||: 1, 36 case: 1, 37 cond: 1 38 ] 39 40 @doc false 41 @impl true 42 def run(%SourceFile{} = source_file, params) do 43 issue_meta = IssueMeta.for(source_file, params) 44 max_complexity = Params.get(params, :max_complexity, __MODULE__) 45 46 Credo.Code.prewalk( 47 source_file, 48 &traverse(&1, &2, issue_meta, max_complexity) 49 ) 50 end 51 52 # exception for `__using__` macros 53 defp traverse({:defmacro, _, [{:__using__, _, _}, _]} = ast, issues, _, _) do 54 {ast, issues} 55 end 56 57 # TODO: consider for experimental check front-loader (ast) 58 # NOTE: see above how we want to exclude certain front-loads 59 for op <- @def_ops do 60 defp traverse( 61 {unquote(op), meta, arguments} = ast, 62 issues, 63 issue_meta, 64 max_complexity 65 ) 66 when is_list(arguments) do 67 complexity = 68 ast 69 |> complexity_for 70 |> round 71 72 if complexity > max_complexity do 73 fun_name = Credo.Code.Module.def_name(ast) 74 75 { 76 ast, 77 issues ++ 78 [ 79 issue_for( 80 issue_meta, 81 meta[:line], 82 fun_name, 83 max_complexity, 84 complexity 85 ) 86 ] 87 } 88 else 89 {ast, issues} 90 end 91 end 92 end 93 94 defp traverse(ast, issues, _source_file, _max_complexity) do 95 {ast, issues} 96 end 97 98 @doc """ 99 Returns the Cyclomatic Complexity score for the block inside the given AST, 100 which is expected to represent a function or macro definition. 101 102 iex> {:def, [line: 1], 103 ...> [ 104 ...> {:first_fun, [line: 1], nil}, 105 ...> [do: {:=, [line: 2], [{:x, [line: 2], nil}, 1]}] 106 ...> ] 107 ...> } |> Credo.Check.Refactor.CyclomaticComplexity.complexity_for 108 1.0 109 """ 110 def complexity_for({_def_op, _meta, _arguments} = ast) do 111 Credo.Code.prewalk(ast, &traverse_complexity/2, 0) 112 end 113 114 for op <- @def_ops do 115 defp traverse_complexity( 116 {unquote(op) = op, _meta, arguments} = ast, 117 complexity 118 ) 119 when is_list(arguments) do 120 {ast, complexity + @op_complexity_map[op]} 121 end 122 end 123 124 for op <- @double_condition_ops do 125 defp traverse_complexity( 126 {unquote(op) = op, _meta, arguments} = ast, 127 complexity 128 ) 129 when is_list(arguments) do 130 {ast, complexity + @op_complexity_map[op]} 131 end 132 end 133 134 for op <- @multiple_condition_ops do 135 defp traverse_complexity({unquote(op), _meta, nil} = ast, complexity) do 136 {ast, complexity} 137 end 138 139 defp traverse_complexity( 140 {unquote(op) = op, _meta, arguments} = ast, 141 complexity 142 ) 143 when is_list(arguments) do 144 block_cc = 145 arguments 146 |> Credo.Code.Block.do_block_for!() 147 |> do_block_complexity(op) 148 149 {ast, complexity + block_cc} 150 end 151 end 152 153 defp traverse_complexity(ast, complexity) do 154 {ast, complexity} 155 end 156 157 defp do_block_complexity(nil, _), do: 0 158 159 defp do_block_complexity(block, op) do 160 count = 161 block 162 |> List.wrap() 163 |> Enum.count() 164 165 count * @op_complexity_map[op] 166 end 167 168 defp issue_for(issue_meta, line_no, trigger, max_value, actual_value) do 169 format_issue( 170 issue_meta, 171 message: 172 "Function is too complex (cyclomatic complexity is #{actual_value}, max is #{max_value}).", 173 trigger: trigger, 174 line_no: line_no, 175 severity: Severity.compute(actual_value, max_value) 176 ) 177 end 178 end