Current section

Files

Jump to
credo lib credo check refactor nesting.ex
Raw

lib/credo/check/refactor/nesting.ex

defmodule Credo.Check.Refactor.Nesting do
@moduledoc false
@checkdoc """
Code should not be nested more than once inside a function.
defmodule CredoSampleModule do
def some_function(parameter1, parameter2) do
Enum.reduce(var1, list, fn({_hash, nodes}, list) ->
filenames = nodes |> Enum.map(&(&1.filename))
Enum.reduce(list, [], fn(item, acc) ->
if item.filename do
item # <-- this is nested 3 levels deep
end
acc ++ [item]
end)
end)
end
end
At this point it might be a good idea to refactor the code to separate the
different loops and conditions.
"""
@explanation [
check: @checkdoc,
params: [
max_nesting: "The maximum number of levels code should be nested."
]
]
@default_params [max_nesting: 2]
@def_ops [:def, :defp, :defmacro]
@nest_ops [:if, :unless, :case, :cond, :fn]
use Credo.Check
@doc false
def run(source_file, params \\ []) do
issue_meta = IssueMeta.for(source_file, params)
max_nesting = Params.get(params, :max_nesting, @default_params)
Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta, max_nesting))
end
for op <- @def_ops do
defp traverse(
{unquote(op) = op, meta, arguments} = ast,
issues,
issue_meta,
max_nesting
)
when is_list(arguments) do
arguments
|> find_depth([], meta[:line], op)
|> handle_depth(ast, issue_meta, issues, max_nesting)
end
end
defp traverse(ast, issues, _issue_meta, _max_nesting) do
{ast, issues}
end
defp handle_depth(nil, ast, _issue_meta, issues, _max_nesting) do
{ast, issues}
end
defp handle_depth(
{depth, line_no, trigger},
ast,
issue_meta,
issues,
max_nesting
) do
if depth > max_nesting do
{
ast,
issues ++ [issue_for(issue_meta, line_no, trigger, max_nesting, depth)]
}
else
{ast, issues}
end
end
# Searches for the depth level and returns a tuple `{depth, line_no, trigger}`
# where the greatest depth was reached.
defp find_depth(arguments, nest_list, line_no, trigger)
when is_list(arguments) do
arguments
|> Credo.Code.Block.do_block_for!()
|> List.wrap()
|> Enum.map(&find_depth(&1, nest_list, line_no, trigger))
|> Enum.sort()
|> List.last()
end
for op <- @nest_ops do
defp find_depth(
{unquote(op) = op, meta, arguments},
nest_list,
_line_no,
_trigger
)
when is_list(arguments) do
arguments
|> Enum.map(&find_depth(&1, nest_list ++ [op], meta[:line], op))
|> Enum.sort()
|> List.last()
end
end
defp find_depth({atom, _meta, arguments}, nest_list, line_no, trigger)
when (is_atom(atom) or is_tuple(atom)) and is_list(arguments) do
arguments
|> Enum.map(&find_depth(&1, nest_list, line_no, trigger))
|> Enum.sort()
|> List.last()
end
defp find_depth(_, nest_list, line_no, trigger) do
{Enum.count(nest_list), line_no, trigger}
end
defp issue_for(issue_meta, line_no, trigger, max_value, actual_value) do
format_issue(
issue_meta,
message:
"Function body is nested too deep (max depth is #{max_value}, was #{actual_value}).",
line_no: line_no,
trigger: trigger,
severity: Severity.compute(actual_value, max_value)
)
end
end