/
nesting.ex
133 lines (117 loc) · 3.47 KB
/
nesting.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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, __MODULE__)
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