-
Notifications
You must be signed in to change notification settings - Fork 414
/
specs.ex
130 lines (109 loc) · 3.57 KB
/
specs.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
defmodule Credo.Check.Readability.Specs do
use Credo.Check,
tags: [:controversial],
param_defaults: [
include_defp: false
],
explanations: [
check: """
Functions, callbacks and macros need typespecs.
Adding typespecs gives tools like Dialyzer more information when performing
checks for type errors in function calls and definitions.
@spec add(integer, integer) :: integer
def add(a, b), do: a + b
Functions with multiple arities need to have a spec defined for each arity:
@spec foo(integer) :: boolean
@spec foo(integer, integer) :: boolean
def foo(a), do: a > 0
def foo(a, b), do: a > b
The check only considers whether the specification is present, it doesn't
perform any actual type checking.
Like all `Readability` issues, this one is not a technical concern.
But you can improve the odds of others reading and liking your code by making
it easier to follow.
""",
params: [
include_defp: "Include private functions."
]
]
@doc false
@impl true
def run(%SourceFile{} = source_file, params) do
issue_meta = IssueMeta.for(source_file, params)
specs = Credo.Code.prewalk(source_file, &find_specs(&1, &2))
Credo.Code.prewalk(source_file, &traverse(&1, &2, specs, issue_meta))
end
defp find_specs(
{:spec, _, [{:when, _, [{:"::", _, [{name, _, args}, _]}, _]} | _]} = ast,
specs
) do
{ast, [{name, length(args)} | specs]}
end
defp find_specs({:spec, _, [{_, _, [{name, _, args} | _]}]} = ast, specs)
when is_list(args) or is_nil(args) do
args = with nil <- args, do: []
{ast, [{name, length(args)} | specs]}
end
defp find_specs({:impl, _, [impl]} = ast, specs) when impl != false do
{ast, [:impl | specs]}
end
defp find_specs({keyword, meta, [{:when, _, def_ast} | _]}, [:impl | specs])
when keyword in [:def, :defp] do
find_specs({keyword, meta, def_ast}, [:impl | specs])
end
defp find_specs({keyword, _, [{name, _, nil}, _]} = ast, [:impl | specs])
when keyword in [:def, :defp] do
{ast, [{name, 0} | specs]}
end
defp find_specs({keyword, _, [{name, _, args}, _]} = ast, [:impl | specs])
when keyword in [:def, :defp] do
{ast, [{name, length(args)} | specs]}
end
defp find_specs(ast, issues) do
{ast, issues}
end
# TODO: consider for experimental check front-loader (ast)
defp traverse(
{keyword, meta, [{:when, _, def_ast} | _]},
issues,
specs,
issue_meta
)
when keyword in [:def, :defp] do
traverse({keyword, meta, def_ast}, issues, specs, issue_meta)
end
defp traverse(
{keyword, meta, [{name, _, args} | _]} = ast,
issues,
specs,
issue_meta
)
when is_list(args) or is_nil(args) do
args = with nil <- args, do: []
if keyword not in enabled_keywords(issue_meta) or {name, length(args)} in specs do
{ast, issues}
else
{ast, [issue_for(issue_meta, meta[:line], name) | issues]}
end
end
defp traverse(ast, issues, _specs, _issue_meta) do
{ast, issues}
end
defp issue_for(issue_meta, line_no, trigger) do
format_issue(
issue_meta,
message: "Functions should have a @spec type specification.",
trigger: trigger,
line_no: line_no
)
end
defp enabled_keywords(issue_meta) do
issue_meta
|> IssueMeta.params()
|> Params.get(:include_defp, __MODULE__)
|> case do
true -> [:def, :defp]
_ -> [:def]
end
end
end