zf

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

abc_size.ex (7741B)


      1 defmodule Credo.Check.Refactor.ABCSize do
      2   use Credo.Check,
      3     tags: [:controversial],
      4     param_defaults: [
      5       max_size: 30,
      6       excluded_functions: []
      7     ],
      8     explanations: [
      9       check: """
     10       The ABC size describes a metric based on assignments, branches and conditions.
     11 
     12       A high ABC size is a hint that a function might be doing "more" than it
     13       should.
     14 
     15       As always: Take any metric with a grain of salt. Since this one was originally
     16       introduced for C, C++ and Java, we still have to see whether or not this can
     17       be a useful metric in a declarative language like Elixir.
     18       """,
     19       params: [
     20         max_size: "The maximum ABC size a function should have.",
     21         excluded_functions: "All functions listed will be ignored."
     22       ]
     23     ]
     24 
     25   @ecto_functions ["where", "from", "select", "join"]
     26   @def_ops [:def, :defp, :defmacro]
     27   @branch_ops [:.]
     28   @condition_ops [:if, :unless, :for, :try, :case, :cond, :and, :or, :&&, :||]
     29   @non_calls [:==, :fn, :__aliases__, :__block__, :if, :or, :|>, :%{}]
     30 
     31   @doc false
     32   @impl true
     33   def run(%SourceFile{} = source_file, params) do
     34     ignore_ecto? = imports_ecto_query?(source_file)
     35     issue_meta = IssueMeta.for(source_file, params)
     36     max_abc_size = Params.get(params, :max_size, __MODULE__)
     37     excluded_functions = Params.get(params, :excluded_functions, __MODULE__)
     38 
     39     excluded_functions =
     40       if ignore_ecto? do
     41         @ecto_functions ++ excluded_functions
     42       else
     43         excluded_functions
     44       end
     45 
     46     Credo.Code.prewalk(
     47       source_file,
     48       &traverse(&1, &2, issue_meta, max_abc_size, excluded_functions)
     49     )
     50   end
     51 
     52   defp imports_ecto_query?(source_file),
     53     do: Credo.Code.prewalk(source_file, &traverse_for_ecto/2, false)
     54 
     55   defp traverse_for_ecto(_, true), do: {nil, true}
     56 
     57   defp traverse_for_ecto({:import, _, [{:__aliases__, _, [:Ecto, :Query]} | _]}, false),
     58     do: {nil, true}
     59 
     60   defp traverse_for_ecto(ast, false), do: {ast, false}
     61 
     62   defp traverse(
     63          {:defmacro, _, [{:__using__, _, _}, _]} = ast,
     64          issues,
     65          _issue_meta,
     66          _max_abc_size,
     67          _excluded_functions
     68        ) do
     69     {ast, issues}
     70   end
     71 
     72   # TODO: consider for experimental check front-loader (ast)
     73   # NOTE: see above how we want to exclude certain front-loads
     74   for op <- @def_ops do
     75     defp traverse(
     76            {unquote(op), meta, arguments} = ast,
     77            issues,
     78            issue_meta,
     79            max_abc_size,
     80            excluded_functions
     81          )
     82          when is_list(arguments) do
     83       abc_size =
     84         ast
     85         |> abc_size_for(excluded_functions)
     86         |> round
     87 
     88       if abc_size > max_abc_size do
     89         fun_name = Credo.Code.Module.def_name(ast)
     90 
     91         {ast,
     92          [
     93            issue_for(issue_meta, meta[:line], fun_name, max_abc_size, abc_size)
     94            | issues
     95          ]}
     96       else
     97         {ast, issues}
     98       end
     99     end
    100   end
    101 
    102   defp traverse(ast, issues, _issue_meta, _max_abc_size, _excluded_functions) do
    103     {ast, issues}
    104   end
    105 
    106   @doc """
    107   Returns the ABC size for the block inside the given AST, which is expected
    108   to represent a function or macro definition.
    109 
    110       iex> {:def, [line: 1],
    111       ...>   [
    112       ...>     {:first_fun, [line: 1], nil},
    113       ...>     [do: {:=, [line: 2], [{:x, [line: 2], nil}, 1]}]
    114       ...>   ]
    115       ...> } |> Credo.Check.Refactor.ABCSize.abc_size
    116       1.0
    117   """
    118   def abc_size_for({_def_op, _meta, arguments}, excluded_functions) when is_list(arguments) do
    119     arguments
    120     |> Credo.Code.Block.do_block_for!()
    121     |> abc_size_for(arguments, excluded_functions)
    122   end
    123 
    124   @doc false
    125   def abc_size_for(nil, _arguments, _excluded_functions), do: 0
    126 
    127   def abc_size_for(ast, arguments, excluded_functions) do
    128     initial_acc = [a: 0, b: 0, c: 0, var_names: get_parameters(arguments)]
    129 
    130     [a: a, b: b, c: c, var_names: _] =
    131       Credo.Code.prewalk(ast, &traverse_abc(&1, &2, excluded_functions), initial_acc)
    132 
    133     :math.sqrt(a * a + b * b + c * c)
    134   end
    135 
    136   defp get_parameters(arguments) do
    137     case Enum.at(arguments, 0) do
    138       {_name, _meta, nil} ->
    139         []
    140 
    141       {_name, _meta, parameters} ->
    142         Enum.map(parameters, &var_name/1)
    143     end
    144   end
    145 
    146   for op <- @def_ops do
    147     defp traverse_abc({unquote(op), _, arguments} = ast, abc, _excluded_functions)
    148          when is_list(arguments) do
    149       {ast, abc}
    150     end
    151   end
    152 
    153   # Ignore string interpolation
    154   defp traverse_abc({:<<>>, _, _}, acc, _excluded_functions) do
    155     {nil, acc}
    156   end
    157 
    158   # A - assignments
    159   defp traverse_abc(
    160          {:=, _meta, [lhs | rhs]},
    161          [a: a, b: b, c: c, var_names: var_names],
    162          _excluded_functions
    163        ) do
    164     var_names =
    165       case var_name(lhs) do
    166         nil ->
    167           var_names
    168 
    169         false ->
    170           var_names
    171 
    172         name ->
    173           var_names ++ [name]
    174       end
    175 
    176     {rhs, [a: a + 1, b: b, c: c, var_names: var_names]}
    177   end
    178 
    179   # B - branch
    180   defp traverse_abc(
    181          {:->, _meta, arguments} = ast,
    182          [a: a, b: b, c: c, var_names: var_names],
    183          _excluded_functions
    184        ) do
    185     var_names = var_names ++ fn_parameters(arguments)
    186     {ast, [a: a, b: b + 1, c: c, var_names: var_names]}
    187   end
    188 
    189   for op <- @branch_ops do
    190     defp traverse_abc(
    191            {unquote(op), _meta, [{_, _, nil}, _] = arguments} = ast,
    192            [a: a, b: b, c: c, var_names: var_names],
    193            _excluded_functions
    194          )
    195          when is_list(arguments) do
    196       {ast, [a: a, b: b, c: c, var_names: var_names]}
    197     end
    198 
    199     defp traverse_abc(
    200            {unquote(op), _meta, arguments} = ast,
    201            [a: a, b: b, c: c, var_names: var_names],
    202            _excluded_functions
    203          )
    204          when is_list(arguments) do
    205       {ast, [a: a, b: b + 1, c: c, var_names: var_names]}
    206     end
    207   end
    208 
    209   defp traverse_abc(
    210          {fun_name, _meta, arguments} = ast,
    211          [a: a, b: b, c: c, var_names: var_names],
    212          excluded_functions
    213        )
    214        when is_atom(fun_name) and fun_name not in @non_calls and is_list(arguments) do
    215     if Enum.member?(excluded_functions, to_string(fun_name)) do
    216       {nil, [a: a, b: b, c: c, var_names: var_names]}
    217     else
    218       {ast, [a: a, b: b + 1, c: c, var_names: var_names]}
    219     end
    220   end
    221 
    222   defp traverse_abc(
    223          {fun_or_var_name, _meta, nil} = ast,
    224          [a: a, b: b, c: c, var_names: var_names],
    225          _excluded_functions
    226        ) do
    227     is_variable = Enum.member?(var_names, fun_or_var_name)
    228 
    229     if is_variable do
    230       {ast, [a: a, b: b, c: c, var_names: var_names]}
    231     else
    232       {ast, [a: a, b: b + 1, c: c, var_names: var_names]}
    233     end
    234   end
    235 
    236   # C - conditions
    237   for op <- @condition_ops do
    238     defp traverse_abc(
    239            {unquote(op), _meta, arguments} = ast,
    240            [a: a, b: b, c: c, var_names: var_names],
    241            _excluded_functions
    242          )
    243          when is_list(arguments) do
    244       {ast, [a: a, b: b, c: c + 1, var_names: var_names]}
    245     end
    246   end
    247 
    248   defp traverse_abc(ast, abc, _excluded_functions) do
    249     {ast, abc}
    250   end
    251 
    252   defp var_name({name, _, nil}) when is_atom(name), do: name
    253   defp var_name(_), do: nil
    254 
    255   defp fn_parameters([params, tuple]) when is_list(params) and is_tuple(tuple) do
    256     fn_parameters(params)
    257   end
    258 
    259   defp fn_parameters([[{:when, _, params}], _]) when is_list(params) do
    260     fn_parameters(params)
    261   end
    262 
    263   defp fn_parameters(params) when is_list(params) do
    264     params
    265     |> Enum.map(&var_name/1)
    266     |> Enum.reject(&is_nil/1)
    267   end
    268 
    269   defp issue_for(issue_meta, line_no, trigger, max_value, actual_value) do
    270     format_issue(
    271       issue_meta,
    272       message: "Function is too complex (ABC size is #{actual_value}, max is #{max_value}).",
    273       trigger: trigger,
    274       line_no: line_no,
    275       severity: Severity.compute(actual_value, max_value)
    276     )
    277   end
    278 end