Skip to content

Commit 4d76fe9

Browse files
authored
Support multiple formatters (#538)
Fixes #530 One scenario where multiple formatters is helpful is on CI where you want to use the GitHub formatter along with a more verbose formatter to see more about the specific failures (especially when viewing the CI logs directly).
1 parent d8cb107 commit 4d76fe9

File tree

8 files changed

+108
-69
lines changed

8 files changed

+108
-69
lines changed

README.md

+15-15
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,21 @@ mix dialyzer
3737

3838
### Command line options
3939

40-
* `--no-compile` - do not compile even if needed.
41-
* `--no-check` - do not perform (quick) check to see if PLT needs to be updated.
42-
* `--ignore-exit-status` - display warnings but do not halt the VM or return an exit status code.
43-
* `--list-unused-filters` - list unused ignore filters useful for CI. do
44-
not use with `mix do`.
45-
* `--plt` - only build the required PLT(s) and exit
46-
* `--format short` - format the warnings in a compact format, suitable for ignore file using Elixir term format.
47-
* `--format raw` - format the warnings in format returned before Dialyzer formatting.
48-
* `--format dialyxir` - format the warnings in a pretty printed format. (default)
49-
* `--format dialyzer` - format the warnings in the original Dialyzer format, suitable for ignore file using simple string matches.
50-
* `--format github` - format the warnings in the Github Actions message format.
51-
* `--format ignore_file` - format the warnings in {file, warning} format for Elixir Format ignore file.
52-
* `--format ignore_file_strict` - format the warnings in {file, short_description} format for Elixir Format ignore file.
53-
* `--quiet` - suppress all informational messages.
54-
* `--quiet-with-result` - suppress all informational messages except for the final result message
40+
* `--no-compile` - do not compile even if needed.
41+
* `--no-check` - do not perform (quick) check to see if PLT needs to be updated.
42+
* `--ignore-exit-status` - display warnings but do not halt the VM or return an exit status code.
43+
* `--list-unused-filters` - list unused ignore filters useful for CI. do not use with `mix do`.
44+
* `--plt` - only build the requir ed PLT(s) and exit.
45+
* `--format <name>` - Specify the format for the warnings, can be specified multiple times to print warnings multiple times in different output formats. Defaults to `dialyxir`.
46+
* `--format short` - format the warnings in a compact format, suitable for ignore file using Elixir term format.
47+
* `--format raw` - format the warnings in format returned before Dialyzer formatting.
48+
* `--format dialyxir` - format the warnings in a pretty printed format. (default)
49+
* `--format dialyzer` - format the warnings in the original Dialyzer format, suitable for ignore file using simple string matches.
50+
* `--format github` - format the warnings in the Github Actions message format.
51+
* `--format ignore_file` - format the warnings in {file, warning} format for Elixir Format ignore file.
52+
* `--format ignore_file_strict` - format the warnings in {file, short_description} format for Elixir Format ignore file.
53+
* `--quiet` - suppress all informational messages.
54+
* `--quiet-with-result` - suppress all informational messages except for the final result message.
5555

5656
Warning flags passed to this task are passed on to `:dialyzer` - e.g.
5757

docs/github_actions.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ steps:
4242
priv/plts
4343
4444
- name: Run dialyzer
45-
run: mix dialyzer --format github
45+
# Two formats are included for ease of debugging and it is lightly recommended to use both, see https://github.com/jeremyjh/dialyxir/issues/530 for reasoning
46+
# --format github is helpful to print the warnings in a way that GitHub understands and can place on the /files page of a PR
47+
# --format dialyxir allows the raw GitHub actions logs to be useful because they have the full warning printed
48+
run: mix dialyzer --format github --format dialyxir
4649

4750
# ...
48-
```
51+
```

lib/dialyxir/dialyzer.ex

+31-28
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,25 @@ defmodule Dialyxir.Dialyzer do
1414
:quiet_with_result
1515
]
1616

17+
@default_formatter Dialyxir.Formatter.Dialyxir
18+
1719
def run(args, filterer) do
1820
try do
1921
{split, args} = Keyword.split(args, @dialyxir_args)
2022

2123
quiet_with_result? = split[:quiet_with_result]
2224

23-
formatter =
24-
cond do
25-
split[:format] == "dialyzer" ->
26-
Dialyxir.Formatter.Dialyzer
27-
28-
split[:format] == "dialyxir" ->
29-
Dialyxir.Formatter.Dialyxir
30-
31-
split[:format] == "github" ->
32-
Dialyxir.Formatter.Github
33-
34-
split[:format] == "ignore_file" ->
35-
Dialyxir.Formatter.IgnoreFile
36-
37-
split[:format] == "ignore_file_strict" ->
38-
Dialyxir.Formatter.IgnoreFileStrict
39-
40-
split[:format] == "raw" ->
41-
Dialyxir.Formatter.Raw
42-
43-
split[:format] == "short" ->
44-
Dialyxir.Formatter.Short
45-
46-
split[:raw] ->
47-
Dialyxir.Formatter.Raw
25+
raw_formatters =
26+
if split[:raw] do
27+
Enum.uniq(split[:format] ++ ["raw"])
28+
else
29+
split[:format]
30+
end
4831

49-
true ->
50-
Dialyxir.Formatter.Dialyxir
32+
formatters =
33+
case raw_formatters do
34+
[] -> [@default_formatter]
35+
raw_formatters -> Enum.map(raw_formatters, &parse_formatter/1)
5136
end
5237

5338
info("Starting Dialyzer")
@@ -66,7 +51,7 @@ defmodule Dialyxir.Dialyzer do
6651
result,
6752
filterer,
6853
filter_map_args,
69-
formatter,
54+
formatters,
7055
quiet_with_result?
7156
) do
7257
{:ok, formatted_warnings, :no_unused_filters} ->
@@ -83,6 +68,24 @@ defmodule Dialyxir.Dialyzer do
8368
{:error, ":dialyzer.run error: " <> Chars.to_string(msg)}
8469
end
8570
end
71+
72+
defp parse_formatter("dialyzer"), do: Dialyxir.Formatter.Dialyzer
73+
defp parse_formatter("dialyxir"), do: Dialyxir.Formatter.Dialyxir
74+
defp parse_formatter("github"), do: Dialyxir.Formatter.Github
75+
defp parse_formatter("ignore_file"), do: Dialyxir.Formatter.IgnoreFile
76+
defp parse_formatter("ignore_file_string"), do: Dialyxir.Formatter.IgnoreFileStrict
77+
defp parse_formatter("raw"), do: Dialyxir.Formatter.Raw
78+
defp parse_formatter("short"), do: Dialyxir.Formatter.Short
79+
80+
defp parse_formatter(unknown) do
81+
warning("""
82+
Unrecognized formatter #{unknown} received. \
83+
Known formatters are dialyzer, dialyxir, github, ignore_file, ignore_file_string, raw, and short. \
84+
Falling back to dialyxir.
85+
""")
86+
87+
@default_formatter
88+
end
8689
end
8790

8891
@success_return_code 0

lib/dialyxir/formatter.ex

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ defmodule Dialyxir.Formatter do
2222
"done in #{minutes}m#{seconds}s"
2323
end
2424

25-
@spec format_and_filter([tuple], module, Keyword.t(), t(), boolean()) :: tuple
25+
@spec format_and_filter([tuple], module, Keyword.t(), [t()], boolean()) :: tuple
2626
def format_and_filter(
2727
warnings,
2828
filterer,
@@ -31,15 +31,17 @@ defmodule Dialyxir.Formatter do
3131
quiet_with_result? \\ false
3232
)
3333

34-
def format_and_filter(warnings, filterer, filter_map_args, formatter, quiet_with_result?) do
34+
def format_and_filter(warnings, filterer, filter_map_args, formatters, quiet_with_result?) do
3535
filter_map = filterer.filter_map(filter_map_args)
3636

3737
{filtered_warnings, filter_map} = filter_warnings(warnings, filterer, filter_map)
3838

3939
formatted_warnings =
4040
filtered_warnings
4141
|> filter_legacy_warnings(filterer)
42-
|> Enum.map(&formatter.format/1)
42+
|> Enum.flat_map(fn legacy_warning ->
43+
Enum.map(formatters, & &1.format(legacy_warning))
44+
end)
4345
|> Enum.uniq()
4446

4547
show_count_skipped(warnings, formatted_warnings, filter_map, quiet_with_result?)

lib/mix/tasks/dialyzer.ex

+10-9
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ defmodule Mix.Tasks.Dialyzer do
1717
* `--list-unused-filters` - list unused ignore filters useful for CI. do
1818
not use with `mix do`.
1919
* `--plt` - only build the required PLT(s) and exit
20-
* `--format short` - format the warnings in a compact format
21-
* `--format raw` - format the warnings in format returned before Dialyzer formatting
22-
* `--format dialyxir` - format the warnings in a pretty printed format
23-
* `--format dialyzer` - format the warnings in the original Dialyzer format
24-
* `--format github` - format the warnings in the Github Actions message format
25-
* `--format ignore_file` - format the warnings in {file, warning} format for Elixir Format ignore file
26-
* `--format ignore_file_strict` - format the warnings in {file, short_description} format for Elixir Format ignore file.
20+
* `--format <name>` - Specify the format for the warnings, can be specified multiple times to print warnings multiple times in different output formats. Defaults to `dialyxir`.
21+
* `--format short` - format the warnings in a compact format, suitable for ignore file using Elixir term format.
22+
* `--format raw` - format the warnings in format returned before Dialyzer formatting
23+
* `--format dialyxir` - format the warnings in a pretty printed format (default)
24+
* `--format dialyzer` - format the warnings in the original Dialyzer format
25+
* `--format github` - format the warnings in the Github Actions message format
26+
* `--format ignore_file` - format the warnings in {file, warning} format for Elixir Format ignore file
27+
* `--format ignore_file_strict` - format the warnings in {file, short_description} format for Elixir Format ignore file.
2728
* `--quiet` - suppress all informational messages
2829
* `--quiet-with-result` - suppress all informational messages except for the final result message
2930
@@ -154,7 +155,7 @@ defmodule Mix.Tasks.Dialyzer do
154155
quiet: :boolean,
155156
quiet_with_result: :boolean,
156157
raw: :boolean,
157-
format: :string
158+
format: [:string, :keep]
158159
)
159160

160161
def run(args) do
@@ -265,7 +266,7 @@ defmodule Mix.Tasks.Dialyzer do
265266
{:init_plt, String.to_charlist(Project.plt_file())},
266267
{:files, Project.dialyzer_files()},
267268
{:warnings, dialyzer_warnings(dargs)},
268-
{:format, opts[:format]},
269+
{:format, Keyword.get_values(opts, :format)},
269270
{:raw, opts[:raw]},
270271
{:list_unused_filters, opts[:list_unused_filters]},
271272
{:ignore_exit_status, opts[:ignore_exit_status]},

mix.exs

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule Dialyxir.Mixfile do
1515
deps: deps(),
1616
aliases: [test: "test --no-start"],
1717
dialyzer: [
18-
plt_apps: [:dialyzer, :elixir, :kernel, :mix, :stdlib, :erlex],
18+
plt_apps: [:dialyzer, :elixir, :kernel, :mix, :stdlib, :erlex, :logger],
1919
ignore_warnings: ".dialyzer_ignore.exs",
2020
flags: [:unmatched_returns, :error_handling, :underspecs]
2121
],
@@ -33,7 +33,10 @@ defmodule Dialyxir.Mixfile do
3333
end
3434

3535
def application do
36-
[mod: {Dialyxir, []}, extra_applications: [:dialyzer, :crypto, :mix, :erts, :syntax_tools]]
36+
[
37+
mod: {Dialyxir, []},
38+
extra_applications: [:dialyzer, :crypto, :mix, :erts, :syntax_tools, :logger]
39+
]
3740
end
3841

3942
defp description do

test/dialyxir/formatter_test.exs

+27-10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Dialyxir.FormatterTest do
66
alias Dialyxir.Formatter
77
alias Dialyxir.Formatter.Dialyxir, as: DialyxirFormatter
88
alias Dialyxir.Formatter.Dialyzer, as: DialyzerFormatter
9+
alias Dialyxir.Formatter.Github, as: GithubFormatter
910
alias Dialyxir.Formatter.Short, as: ShortFormatter
1011
alias Dialyxir.Formatter.IgnoreFileStrict, as: IgnoreFileStrictFormatter
1112
alias Dialyxir.Project
@@ -45,7 +46,7 @@ defmodule Dialyxir.FormatterTest do
4546
],
4647
Project,
4748
[],
48-
unquote(formatter)
49+
[unquote(formatter)]
4950
)
5051

5152
assert message =~ unquote(message)
@@ -67,7 +68,7 @@ defmodule Dialyxir.FormatterTest do
6768

6869
in_project(:ignore, fn ->
6970
{:error, remaining, _unused_filters_present} =
70-
Formatter.format_and_filter(warnings, Project, [], ShortFormatter)
71+
Formatter.format_and_filter(warnings, Project, [], [ShortFormatter])
7172

7273
assert remaining == []
7374
end)
@@ -85,7 +86,7 @@ defmodule Dialyxir.FormatterTest do
8586

8687
in_project(:ignore_strict, fn ->
8788
{:ok, remaining, :no_unused_filters} =
88-
Formatter.format_and_filter(warnings, Project, [], IgnoreFileStrictFormatter)
89+
Formatter.format_and_filter(warnings, Project, [], [IgnoreFileStrictFormatter])
8990

9091
assert remaining == []
9192
end)
@@ -98,7 +99,7 @@ defmodule Dialyxir.FormatterTest do
9899

99100
in_project(:ignore, fn ->
100101
{:error, [remaining], _} =
101-
Formatter.format_and_filter([warning], Project, [], ShortFormatter)
102+
Formatter.format_and_filter([warning], Project, [], [ShortFormatter])
102103

103104
assert remaining =~ ~r/different_file.* no local return/
104105
end)
@@ -111,7 +112,7 @@ defmodule Dialyxir.FormatterTest do
111112

112113
in_project(:ignore, fn ->
113114
{:error, remaining, _unused_filters_present} =
114-
Formatter.format_and_filter([warning], Project, [], ShortFormatter)
115+
Formatter.format_and_filter([warning], Project, [], [ShortFormatter])
115116

116117
assert remaining == []
117118
end)
@@ -126,7 +127,7 @@ defmodule Dialyxir.FormatterTest do
126127

127128
in_project(:ignore, fn ->
128129
assert {:warn, [], {:unused_filters_present, warning}} =
129-
Formatter.format_and_filter([warning], Project, filter_args, :dialyxir)
130+
Formatter.format_and_filter([warning], Project, filter_args, [:dialyxir])
130131

131132
assert warning =~ "Unused filters:"
132133
end)
@@ -141,7 +142,7 @@ defmodule Dialyxir.FormatterTest do
141142

142143
in_project(:ignore, fn ->
143144
{:error, [], {:unused_filters_present, error}} =
144-
Formatter.format_and_filter([warning], Project, filter_args, :dialyxir)
145+
Formatter.format_and_filter([warning], Project, filter_args, [:dialyxir])
145146

146147
assert error =~ "Unused filters:"
147148
end)
@@ -156,7 +157,7 @@ defmodule Dialyxir.FormatterTest do
156157

157158
in_project(:ignore, fn ->
158159
assert {:warn, [], {:unused_filters_present, warning}} =
159-
Formatter.format_and_filter([warning], Project, filter_args, :dialyxir)
160+
Formatter.format_and_filter([warning], Project, filter_args, [:dialyxir])
160161

161162
refute warning =~ "Unused filters:"
162163
end)
@@ -169,12 +170,28 @@ defmodule Dialyxir.FormatterTest do
169170
{:warn_matching, {~c"a/file.ex", 17}, {:pattern_match, [~c"pattern 'ok'", ~c"'error'"]}}
170171

171172
in_project(:ignore_string, fn ->
172-
assert Formatter.format_and_filter([warning], Project, [], :dialyzer) ==
173+
assert Formatter.format_and_filter([warning], Project, [], [:dialyzer]) ==
173174
{:ok, [], :no_unused_filters}
174175
end)
175176
end
176177
end
177178

179+
describe "multiple formatters" do
180+
test "short and github" do
181+
warning =
182+
{:warn_return_no_exit, {~c"a/different_file.ex", 17},
183+
{:no_return, [:only_normal, :format_long, 1]}}
184+
185+
in_project(:ignore, fn ->
186+
{:error, [short_formatted, github_formatted], _} =
187+
Formatter.format_and_filter([warning], Project, [], [ShortFormatter, GithubFormatter])
188+
189+
assert short_formatted =~ ~r/different_file.* no local return/
190+
assert github_formatted =~ ~r/^::warning file=a\/different_file\.ex.* no local return/
191+
end)
192+
end
193+
end
194+
178195
test "listing unused filter behaves the same for different formats" do
179196
warnings = [
180197
{:warn_return_no_exit, {~c"a/regex_file.ex", 17},
@@ -194,7 +211,7 @@ defmodule Dialyxir.FormatterTest do
194211
for format <- [ShortFormatter, DialyxirFormatter, DialyzerFormatter] do
195212
in_project(:ignore, fn ->
196213
capture_io(fn ->
197-
result = Formatter.format_and_filter(warnings, Project, filter_args, format)
214+
result = Formatter.format_and_filter(warnings, Project, filter_args, [format])
198215

199216
assert {:error, [warning], {:unused_filters_present, unused}} = result
200217
assert warning =~ expected_warning

test/mix/tasks/dialyzer_test.exs

+10
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,14 @@ defmodule Mix.Tasks.DialyzerTest do
6767
assert result =~
6868
~r/Total errors: ., Skipped: ., Unnecessary Skips: .\ndone \(passed successfully\)\n/
6969
end
70+
71+
@tag :output_tests
72+
test "Warning is printed when unknown format is requested" do
73+
args = ["dialyzer", "--format", "foo"]
74+
env = [{"MIX_ENV", "prod"}]
75+
{result, 0} = System.cmd("mix", args, env: env)
76+
77+
assert result =~
78+
"Unrecognized formatter foo received. Known formatters are dialyzer, dialyxir, github, ignore_file, ignore_file_string, raw, and short. Falling back to dialyxir."
79+
end
7080
end

0 commit comments

Comments
 (0)