-
Notifications
You must be signed in to change notification settings - Fork 414
/
cyclomatic_complexity.ex
177 lines (156 loc) · 4.45 KB
/
cyclomatic_complexity.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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
defmodule Credo.Check.Refactor.CyclomaticComplexity do
use Credo.Check,
param_defaults: [max_complexity: 9],
explanations: [
check: """
Cyclomatic complexity is a software complexity metric closely correlated with
coding errors.
If a function feels like it's gotten too complex, it more often than not also
has a high CC value. So, if anything, this is useful to convince team members
and bosses of a need to refactor parts of the code based on "objective"
metrics.
""",
params: [
max_complexity: "The maximum cyclomatic complexity a function should have."
]
]
@def_ops [:def, :defp, :defmacro]
# these have two outcomes: it succeeds or does not
@double_condition_ops [:if, :unless, :for, :try, :and, :or, :&&, :||]
# these can have multiple outcomes as they are defined in their do blocks
@multiple_condition_ops [:case, :cond]
@op_complexity_map [
def: 1,
defp: 1,
defmacro: 1,
if: 1,
unless: 1,
for: 1,
try: 1,
and: 1,
or: 1,
&&: 1,
||: 1,
case: 1,
cond: 1
]
@doc false
@impl true
def run(%SourceFile{} = source_file, params) do
issue_meta = IssueMeta.for(source_file, params)
max_complexity = Params.get(params, :max_complexity, __MODULE__)
Credo.Code.prewalk(
source_file,
&traverse(&1, &2, issue_meta, max_complexity)
)
end
# exception for `__using__` macros
defp traverse({:defmacro, _, [{:__using__, _, _}, _]} = ast, issues, _, _) do
{ast, issues}
end
# TODO: consider for experimental check front-loader (ast)
# NOTE: see above how we want to exclude certain front-loads
for op <- @def_ops do
defp traverse(
{unquote(op), meta, arguments} = ast,
issues,
issue_meta,
max_complexity
)
when is_list(arguments) do
complexity =
ast
|> complexity_for
|> round
if complexity > max_complexity do
fun_name = Credo.Code.Module.def_name(ast)
{
ast,
issues ++
[
issue_for(
issue_meta,
meta[:line],
fun_name,
max_complexity,
complexity
)
]
}
else
{ast, issues}
end
end
end
defp traverse(ast, issues, _source_file, _max_complexity) do
{ast, issues}
end
@doc """
Returns the Cyclomatic Complexity score for the block inside the given AST,
which is expected to represent a function or macro definition.
iex> {:def, [line: 1],
...> [
...> {:first_fun, [line: 1], nil},
...> [do: {:=, [line: 2], [{:x, [line: 2], nil}, 1]}]
...> ]
...> } |> Credo.Check.Refactor.CyclomaticComplexity.complexity_for
1.0
"""
def complexity_for({_def_op, _meta, _arguments} = ast) do
Credo.Code.prewalk(ast, &traverse_complexity/2, 0)
end
for op <- @def_ops do
defp traverse_complexity(
{unquote(op) = op, _meta, arguments} = ast,
complexity
)
when is_list(arguments) do
{ast, complexity + @op_complexity_map[op]}
end
end
for op <- @double_condition_ops do
defp traverse_complexity(
{unquote(op) = op, _meta, arguments} = ast,
complexity
)
when is_list(arguments) do
{ast, complexity + @op_complexity_map[op]}
end
end
for op <- @multiple_condition_ops do
defp traverse_complexity({unquote(op), _meta, nil} = ast, complexity) do
{ast, complexity}
end
defp traverse_complexity(
{unquote(op) = op, _meta, arguments} = ast,
complexity
)
when is_list(arguments) do
block_cc =
arguments
|> Credo.Code.Block.do_block_for!()
|> do_block_complexity(op)
{ast, complexity + block_cc}
end
end
defp traverse_complexity(ast, complexity) do
{ast, complexity}
end
defp do_block_complexity(nil, _), do: 0
defp do_block_complexity(block, op) do
count =
block
|> List.wrap()
|> Enum.count()
count * @op_complexity_map[op]
end
defp issue_for(issue_meta, line_no, trigger, max_value, actual_value) do
format_issue(
issue_meta,
message: "Function is too complex (CC is #{actual_value}, max is #{max_value}).",
trigger: trigger,
line_no: line_no,
severity: Severity.compute(actual_value, max_value)
)
end
end