Skip to content

Commit d38e42f

Browse files
authored
feat: Adds a github formatter (#463)
* refactor: Extracts formatter modules * refactor: Uses a behaviour over string and atom matching * feat: Adds a github formatter * chore: Updates log level and documentation * fix: Cleans up some warnings Co-authored-by: Isaac Sanders <isanders@drwholdings.com>
1 parent a78730f commit d38e42f

File tree

11 files changed

+227
-146
lines changed

11 files changed

+227
-146
lines changed

README.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ mix dialyzer
4949

5050
### Command line options
5151

52-
* `--no-compile` - do not compile even if needed.
53-
* `--no-check` - do not perform (quick) check to see if PLT needs to be updated.
54-
* `--ignore-exit-status` - display warnings but do not halt the VM or return an exit status code.
55-
* `--format short` - format the warnings in a compact format, suitable for ignore file using Elixir term format.
56-
* `--format raw` - format the warnings in format returned before Dialyzer formatting.
57-
* `--format dialyxir` - format the warnings in a pretty printed format.
58-
* `--format dialyzer` - format the warnings in the original Dialyzer format, suitable for ignore file using simple string matches.
59-
* `--quiet` - suppress all informational messages.
52+
* `--no-compile` - do not compile even if needed.
53+
* `--no-check` - do not perform (quick) check to see if PLT needs to be updated.
54+
* `--ignore-exit-status` - display warnings but do not halt the VM or return an exit status code.
55+
* `--format short` - format the warnings in a compact format, suitable for ignore file using Elixir term format.
56+
* `--format raw` - format the warnings in format returned before Dialyzer formatting.
57+
* `--format dialyxir` - format the warnings in a pretty printed format.
58+
* `--format dialyzer` - format the warnings in the original Dialyzer format, suitable for ignore file using simple string matches.
59+
* `--format github` - format the warnings in the Github Actions message format.
60+
* `--format ignore_file` - format the warnings to be suitable for adding to Elixir Format ignore file.
61+
* `--quiet` - suppress all informational messages.
6062

6163
Warning flags passed to this task are passed on to `:dialyzer` - e.g.
6264

@@ -133,7 +135,7 @@ cache:
133135
run: mix dialyzer --plt
134136

135137
- name: Run dialyzer
136-
run: mix dialyzer
138+
run: mix dialyzer --format github
137139

138140
```
139141

lib/dialyxir/dialyzer.ex

+10-7
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,28 @@ defmodule Dialyxir.Dialyzer do
2020
formatter =
2121
cond do
2222
split[:format] == "dialyzer" ->
23-
:dialyzer
23+
Dialyxir.Formatter.Dialyzer
2424

2525
split[:format] == "dialyxir" ->
26-
:dialyxir
26+
Dialyxir.Formatter.Dialyxir
27+
28+
split[:format] == "github" ->
29+
Dialyxir.Formatter.Github
2730

2831
split[:format] == "ignore_file" ->
29-
:ignore_file
32+
Dialyxir.Formatter.IgnoreFile
3033

3134
split[:format] == "raw" ->
32-
:raw
35+
Dialyxir.Formatter.Raw
3336

3437
split[:format] == "short" ->
35-
:short
38+
Dialyxir.Formatter.Short
3639

3740
split[:raw] ->
38-
:raw
41+
Dialyxir.Formatter.Raw
3942

4043
true ->
41-
:dialyxir
44+
Dialyxir.Formatter.Dialyxir
4245
end
4346

4447
info("Starting Dialyzer")

lib/dialyxir/formatter.ex

+10-122
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ defmodule Dialyxir.Formatter do
99

1010
alias Dialyxir.FilterMap
1111

12+
@type warning() :: {tag :: term(), {file :: Path.t(), line :: pos_integer()}, {atom(), list()}}
13+
14+
@type t() :: module()
15+
16+
@callback format(warning()) :: String.t()
17+
1218
def formatted_time(duration_us) do
1319
minutes = div(duration_us, 60_000_000)
1420
seconds = (rem(duration_us, 60_000_000) / 1_000_000) |> Float.round(2)
1521
"done in #{minutes}m#{seconds}s"
1622
end
1723

18-
@spec format_and_filter([tuple], module, Keyword.t(), atom) :: tuple
24+
@spec format_and_filter([tuple], module, Keyword.t(), t()) :: tuple
1925
def format_and_filter(warnings, filterer, filter_map_args, formatter) do
2026
filter_map = filterer.filter_map(filter_map_args)
2127

@@ -24,7 +30,7 @@ defmodule Dialyxir.Formatter do
2430
formatted_warnings =
2531
filtered_warnings
2632
|> filter_legacy_warnings(filterer)
27-
|> Enum.map(&format_warning(&1, formatter))
33+
|> Enum.map(&formatter.format/1)
2834
|> Enum.uniq()
2935

3036
show_count_skipped(warnings, formatted_warnings, filter_map)
@@ -46,114 +52,6 @@ defmodule Dialyxir.Formatter do
4652
end
4753
end
4854

49-
defp format_warning(warning, :raw) do
50-
inspect(warning, limit: :infinity)
51-
end
52-
53-
defp format_warning(warning, :dialyzer) do
54-
# OTP 22 uses indented output, but that's incompatible with dialyzer.ignore-warnings format.
55-
# Can be disabled, but OTP 21 and older only accept an atom, so only disable on OTP 22+.
56-
opts =
57-
if String.to_integer(System.otp_release()) < 22,
58-
do: :fullpath,
59-
else: [{:filename_opt, :fullpath}, {:indent_opt, false}]
60-
61-
warning
62-
|> :dialyzer.format_warning(opts)
63-
|> String.Chars.to_string()
64-
|> String.replace_trailing("\n", "")
65-
end
66-
67-
defp format_warning({_tag, {file, line}, message}, :short) do
68-
{warning_name, arguments} = message
69-
base_name = Path.relative_to_cwd(file)
70-
71-
warning = warning(warning_name)
72-
string = warning.format_short(arguments)
73-
74-
"#{base_name}:#{line}:#{warning_name} #{string}"
75-
end
76-
77-
defp format_warning({_tag, {file, _line}, {warning_name, _arguments}}, :ignore_file) do
78-
~s({"#{file}", :#{warning_name}},)
79-
end
80-
81-
defp format_warning(dialyzer_warning = {_tag, {file, line}, message}, :dialyxir) do
82-
{warning_name, arguments} = message
83-
base_name = Path.relative_to_cwd(file)
84-
85-
formatted =
86-
try do
87-
warning = warning(warning_name)
88-
string = warning.format_long(arguments)
89-
90-
"""
91-
#{base_name}:#{line}:#{warning_name}
92-
#{string}
93-
"""
94-
rescue
95-
e ->
96-
message = """
97-
Unknown error occurred: #{inspect(e)}
98-
"""
99-
100-
wrap_error_message(message, dialyzer_warning)
101-
catch
102-
{:error, :unknown_warning, warning_name} ->
103-
message = """
104-
Unknown warning:
105-
#{inspect(warning_name)}
106-
"""
107-
108-
wrap_error_message(message, dialyzer_warning)
109-
110-
{:error, :lexing, warning} ->
111-
message = """
112-
Failed to lex warning:
113-
#{inspect(warning)}
114-
"""
115-
116-
wrap_error_message(message, dialyzer_warning)
117-
118-
{:error, :parsing, failing_string} ->
119-
message = """
120-
Failed to parse warning:
121-
#{inspect(failing_string)}
122-
"""
123-
124-
wrap_error_message(message, dialyzer_warning)
125-
126-
{:error, :pretty_printing, failing_string} ->
127-
message = """
128-
Failed to pretty print warning:
129-
#{inspect(failing_string)}
130-
"""
131-
132-
wrap_error_message(message, dialyzer_warning)
133-
134-
{:error, :formatting, code} ->
135-
message = """
136-
Failed to format warning:
137-
#{inspect(code)}
138-
"""
139-
140-
wrap_error_message(message, dialyzer_warning)
141-
end
142-
143-
formatted <> String.duplicate("_", 80)
144-
end
145-
146-
defp wrap_error_message(message, warning) do
147-
"""
148-
Please file a bug in https://github.com/jeremyjh/dialyxir/issues with this message.
149-
150-
#{message}
151-
152-
Legacy warning:
153-
#{format_warning(warning, :dialyzer)}
154-
"""
155-
end
156-
15755
defp show_count_skipped(warnings, filtered_warnings, filter_map) do
15856
warnings_count = Enum.count(warnings)
15957
filtered_warnings_count = Enum.count(filtered_warnings)
@@ -189,16 +87,6 @@ defmodule Dialyxir.Formatter do
18987
|> Enum.count()
19088
end
19189

192-
defp warning(warning_name) do
193-
warnings = Dialyxir.Warnings.warnings()
194-
195-
if Map.has_key?(warnings, warning_name) do
196-
Map.get(warnings, warning_name)
197-
else
198-
throw({:error, :unknown_warning, warning_name})
199-
end
200-
end
201-
20290
defp filter_warnings(warnings, filterer, filter_map) do
20391
{warnings, filter_map} =
20492
Enum.map_reduce(warnings, filter_map, &filter_warning(filterer, &1, &2))
@@ -212,7 +100,7 @@ defmodule Dialyxir.Formatter do
212100
{skip?, matching_filters} =
213101
try do
214102
filterer.filter_warning?(
215-
{to_string(file), warning_type, line, format_warning(warning, :short)},
103+
{to_string(file), warning_type, line, Dialyxir.Formatter.Short.format(warning)},
216104
filter_map
217105
)
218106
rescue
@@ -242,7 +130,7 @@ defmodule Dialyxir.Formatter do
242130
Enum.reject(warnings, fn warning ->
243131
formatted_warnings =
244132
warning
245-
|> format_warning(:dialyzer)
133+
|> Dialyxir.Formatter.Dialyzer.format()
246134
|> List.wrap()
247135

248136
Enum.empty?(filterer.filter_legacy_warnings(formatted_warnings))

lib/dialyxir/formatter/dialyxir.ex

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
defmodule Dialyxir.Formatter.Dialyxir do
2+
@moduledoc false
3+
4+
@behaviour Dialyxir.Formatter
5+
6+
@impl Dialyxir.Formatter
7+
def format(dialyzer_warning = {_tag, {file, line}, message}) do
8+
{warning_name, arguments} = message
9+
base_name = Path.relative_to_cwd(file)
10+
11+
formatted =
12+
try do
13+
warning = warning(warning_name)
14+
string = warning.format_long(arguments)
15+
16+
"""
17+
#{base_name}:#{line}:#{warning_name}
18+
#{string}
19+
"""
20+
rescue
21+
e ->
22+
message = """
23+
Unknown error occurred: #{inspect(e)}
24+
"""
25+
26+
wrap_error_message(message, dialyzer_warning)
27+
catch
28+
{:error, :unknown_warning, warning_name} ->
29+
message = """
30+
Unknown warning:
31+
#{inspect(warning_name)}
32+
"""
33+
34+
wrap_error_message(message, dialyzer_warning)
35+
36+
{:error, :lexing, warning} ->
37+
message = """
38+
Failed to lex warning:
39+
#{inspect(warning)}
40+
"""
41+
42+
wrap_error_message(message, dialyzer_warning)
43+
44+
{:error, :parsing, failing_string} ->
45+
message = """
46+
Failed to parse warning:
47+
#{inspect(failing_string)}
48+
"""
49+
50+
wrap_error_message(message, dialyzer_warning)
51+
52+
{:error, :pretty_printing, failing_string} ->
53+
message = """
54+
Failed to pretty print warning:
55+
#{inspect(failing_string)}
56+
"""
57+
58+
wrap_error_message(message, dialyzer_warning)
59+
60+
{:error, :formatting, code} ->
61+
message = """
62+
Failed to format warning:
63+
#{inspect(code)}
64+
"""
65+
66+
wrap_error_message(message, dialyzer_warning)
67+
end
68+
69+
formatted <> String.duplicate("_", 80)
70+
end
71+
72+
defp wrap_error_message(message, warning) do
73+
"""
74+
Please file a bug in https://github.com/jeremyjh/dialyxir/issues with this message.
75+
76+
#{message}
77+
78+
Legacy warning:
79+
#{Dialyxir.Formatter.Dialyzer.format(warning)}
80+
"""
81+
end
82+
83+
defp warning(warning_name) do
84+
warnings = Dialyxir.Warnings.warnings()
85+
86+
if Map.has_key?(warnings, warning_name) do
87+
Map.get(warnings, warning_name)
88+
else
89+
throw({:error, :unknown_warning, warning_name})
90+
end
91+
end
92+
end

lib/dialyxir/formatter/dialyzer.ex

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
defmodule Dialyxir.Formatter.Dialyzer do
2+
@moduledoc false
3+
4+
@behaviour Dialyxir.Formatter
5+
6+
@impl Dialyxir.Formatter
7+
def format(warning) do
8+
# OTP 22 uses indented output, but that's incompatible with dialyzer.ignore-warnings format.
9+
# Can be disabled, but OTP 21 and older only accept an atom, so only disable on OTP 22+.
10+
opts =
11+
if String.to_integer(System.otp_release()) < 22,
12+
do: :fullpath,
13+
else: [{:filename_opt, :fullpath}, {:indent_opt, false}]
14+
15+
warning
16+
|> :dialyzer.format_warning(opts)
17+
|> String.Chars.to_string()
18+
|> String.replace_trailing("\n", "")
19+
end
20+
end

lib/dialyxir/formatter/github.ex

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule Dialyxir.Formatter.Github do
2+
@moduledoc false
3+
4+
@behaviour Dialyxir.Formatter
5+
6+
@impl Dialyxir.Formatter
7+
def format({_tag, {file, line}, {warning_name, arguments}}) do
8+
base_name = Path.relative_to_cwd(file)
9+
10+
warning = warning(warning_name)
11+
string = warning.format_short(arguments)
12+
13+
"::warning file=#{base_name},line=#{line},title=#{warning_name}::#{string}"
14+
end
15+
16+
defp warning(warning_name) do
17+
warnings = Dialyxir.Warnings.warnings()
18+
19+
if Map.has_key?(warnings, warning_name) do
20+
Map.get(warnings, warning_name)
21+
else
22+
throw({:error, :unknown_warning, warning_name})
23+
end
24+
end
25+
end

0 commit comments

Comments
 (0)