-
Notifications
You must be signed in to change notification settings - Fork 13
/
moduledoc.ex
153 lines (117 loc) · 4.1 KB
/
moduledoc.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
defmodule Recode.Task.Moduledoc do
@shortdoc "There should be a @moduledoc in the module."
@moduledoc """
Any module should contain a `@moudledoc` attribute.
For a public module, comprehensive documentation should be available. The
module documentation helps the user of your package, contributors, and your
future self understand what the module is for.
For private modules, it is also okay to set `@moduled false`. Modules marked
in this way are not displayed in the documentation.
## Options
* `ignore_names` - accepts a regex or a list of regexes to recognize modules
that this task ignores.
"""
use Recode.Task, corrector: false, category: :readability
alias Recode.AST
alias Recode.Issue
alias Rewrite.Source
alias Sourceror.Zipper
@skip_def_kinds [:def, :defp, :defmacro, :defmacrop]
@default_config [ignore_names: []]
@error_message """
The config for the Recode.Task.Moduledoc is wrong. The task excepts the option \
:ignore_names with a regexp or a list of regexps.
"""
@impl Recode.Task
def run(source, config) do
ignore_names = Keyword.fetch!(config, :ignore_names)
source
|> Source.get(:quoted)
|> Zipper.zip()
|> Zipper.traverse_while([], module(ignore_names))
|> update(source)
end
@impl Recode.Task
def init([]), do: {:ok, @default_config}
def init(config) do
with {:ok, config} <- validate_keys(config) do
config =
Keyword.update!(config, :ignore_names, fn ignore_names -> List.wrap(ignore_names) end)
if validate_config(config) do
{:ok, config}
else
{:error, @error_message}
end
end
end
defp validate_keys(config) do
with {:error, unknown} <- Keyword.validate(config, @default_config) do
{:error, "#{@error_message}. Unknown keys: #{inspect(unknown)}"}
end
end
defp validate_config(config) do
case Keyword.fetch!(config, :ignore_names) do
[_ | _] = list -> Enum.all?(list, fn regex -> is_struct(regex, Regex) end)
%Regex{} -> true
_invalid -> false
end
end
defp update({_zipper, []}, source), do: source
defp update({_zipper, issues}, source), do: Source.add_issues(source, issues)
defp module(ignore_names) do
fn
%Zipper{node: {:defmodule, meta, args}} = zipper, issues ->
issues = issues ++ check(zipper, name(args), meta, ignore_names)
{:skip, zipper, issues}
zipper, issues ->
{:cont, zipper, issues}
end
end
defp check(zipper, name, meta, ignore_names) do
acc = if ignore?(name, ignore_names), do: [:ignore], else: []
{_zipper, issues} =
zipper
|> do_block()
|> Zipper.traverse_while(acc, moduledoc(ignore_names))
issues(issues, name, meta)
end
defp moduledoc(ignore_names) do
fn
%Zipper{node: {def_kind, _, _}} = zipper, issues when def_kind in @skip_def_kinds ->
{:skip, zipper, issues}
%Zipper{node: {:defmodule, meta, args}} = zipper, issues ->
issues = issues ++ check(zipper, name(args), meta, ignore_names)
{:skip, zipper, issues}
zipper, [:ignore | _issues] = issues ->
{:cont, zipper, issues}
%Zipper{node: {:@, _, [{:moduledoc, _, _} | _]}} = zipper, issues ->
{:cont, zipper, [:exist | issues]}
zipper, issues ->
{:cont, zipper, issues}
end
end
defp issues([issue | issues], _name, _meta) when issue in [:exist, :ignore], do: issues
defp issues(issues, name, meta) do
[issue(name, meta) | issues]
end
defp issue(name, meta) do
message = "The moudle #{name} is missing @moduledoc."
Issue.new(Moduledoc, message, meta)
end
defp do_block(zipper) do
Zipper.traverse_while(zipper, fn
%Zipper{node: {{:__block__, _, [:do]}, block}} ->
{:halt, Zipper.zip(block)}
zipper ->
{:cont, zipper}
end)
end
defp ignore?(_name, []), do: false
defp ignore?(name, [ignore_name | ignore_names]) do
with false <- Regex.match?(ignore_name, name) do
ignore?(name, ignore_names)
end
end
defp name([arg | _args]), do: name(arg)
defp name(arg), do: arg |> AST.aliases_concat() |> inspect()
end