zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

perceived_complexity.ex (4565B)


      1 defmodule Credo.Check.Refactor.PerceivedComplexity do
      2   use Credo.Check,
      3     param_defaults: [max_complexity: 9],
      4     explanations: [
      5       check: """
      6       Cyclomatic complexity is a software complexity metric closely correlated with
      7       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: 0.3,
     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 for how we want to exclude `__using__` macros
     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: "Function is too complex (CC is #{actual_value}, max is #{max_value}).",
    172       trigger: trigger,
    173       line_no: line_no,
    174       severity: Severity.compute(actual_value, max_value)
    175     )
    176   end
    177 end