Repository: jeremyjh/dialyxir
Pull request #432 (2021-06-11 13:48:41)

Add `ignore_file` format output
Hi,
when starting to use dialyxir in a legacy project, it's often useful to get output with all of the existing warnings ready to be put to the ignores file as is. Then to be able to enforce dialyxir checks on CI to prevent further increase of technical debt.
What do you think of this PR? If that's fine I'd add option description to README as well.

jeremyjh: Hi, thanks! Yes I'd say this looks useful. Can you please go ahead and update the README as well as the module doc ?
staring-frog: Great, thanks, will do!

Patch:
diff --git a/README.md b/README.md
index e0e02f2..7b2ef4f 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@ mix dialyzer
   *  `--format raw`        - format the warnings in format returned before Dialyzer formatting
   *  `--format dialyxir`   - format the warnings in a pretty printed format
   *  `--format dialyzer`   - format the warnings in the original Dialyzer format
+  *  `--format ignore_file` - format the warnings to be suitable for adding to "Elixir Term Format" ignore file
   *  `--quiet`             - suppress all informational messages
 
 Warning flags passed to this task are passed on to `:dialyzer` - e.g.
@@ -289,8 +290,8 @@ done (warnings were emitted)
 
 #### Elixir Term Format
 
-Dialyxir also recognizes an Elixir format of the ignore file. If your ignore file is an `exs` file, Dialyxir will evaluate it and process its data structure. The file looks like the following, and can match either tuple patterns or an arbitrary Regex
-applied to the *short-description* (`mix dialyzer --format short`):
+Dialyxir also recognizes an Elixir format of the ignore file. If your ignore file is an `exs` file, Dialyxir will evaluate it and process its data structure. Entries for existing warnings can be generated with `mix dialyzer --format ignore_file`. Lines may be either tuples or an arbitrary Regex
+applied to the *short-description* (`mix dialyzer --format short`). The file looks like the following:
 
 ```elixir
 # .dialyzer_ignore.exs
diff --git a/lib/dialyxir/dialyzer.ex b/lib/dialyxir/dialyzer.ex
index 6f9b396..c68ee89 100644
--- a/lib/dialyxir/dialyzer.ex
+++ b/lib/dialyxir/dialyzer.ex
@@ -25,6 +25,9 @@ defmodule Dialyxir.Dialyzer do
             split[:format] == "dialyxir" ->
               :dialyxir
 
+            split[:format] == "ignore_file" ->
+              :ignore_file
+
             split[:format] == "raw" ->
               :raw
 
diff --git a/lib/dialyxir/formatter.ex b/lib/dialyxir/formatter.ex
index 6aa9c38..29744cd 100644
--- a/lib/dialyxir/formatter.ex
+++ b/lib/dialyxir/formatter.ex
@@ -25,6 +25,7 @@ defmodule Dialyxir.Formatter do
       filtered_warnings
       |> filter_legacy_warnings(filterer)
       |> Enum.map(&format_warning(&1, formatter))
+      |> Enum.uniq()
 
     show_count_skipped(warnings, formatted_warnings, filter_map)
     formatted_unnecessary_skips = format_unnecessary_skips(filter_map)
@@ -73,6 +74,10 @@ defmodule Dialyxir.Formatter do
     "#{base_name}:#{line}:#{warning_name} #{string}"
   end
 
+  defp format_warning({_tag, {file, _line}, {warning_name, _arguments}}, :ignore_file) do
+    ~s({"#{file}", :#{warning_name}},)
+  end
+
   defp format_warning(dialyzer_warning = {_tag, {file, line}, message}, :dialyxir) do
     {warning_name, arguments} = message
     base_name = Path.relative_to_cwd(file)
diff --git a/lib/mix/tasks/dialyzer.ex b/lib/mix/tasks/dialyzer.ex
index 792e453..d243ea9 100644
--- a/lib/mix/tasks/dialyzer.ex
+++ b/lib/mix/tasks/dialyzer.ex
@@ -21,6 +21,7 @@ defmodule Mix.Tasks.Dialyzer do
     * `--format raw` - format the warnings in format returned before Dialyzer formatting
     * `--format dialyxir` - format the warnings in a pretty printed format
     * `--format dialyzer` - format the warnings in the original Dialyzer format
+    * `--format ignore_file` - format the warnings to be suitable for adding to Elixir Format ignore file
     * `--quiet` - suppress all informational messages
 
   Warning flags passed to this task are passed on to `:dialyzer` - e.g.


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 447bb16..9aefd58 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -108,6 +108,11 @@ jobs:
       env:
         MIX_ENV: prod
 
+    - name: Get results in ignore_file format
+      run: mix dialyzer --format ignore_file
+      env:
+        MIX_ENV: prod
+
     - name: Check examples
       run: mix dialyzer --format short --ignore-exit-status
       env:
diff --git a/test/dialyxir/formatter_test.exs b/test/dialyxir/formatter_test.exs
index a769086..8b0eaab 100644
--- a/test/dialyxir/formatter_test.exs
+++ b/test/dialyxir/formatter_test.exs
@@ -12,12 +12,18 @@ defmodule Dialyxir.FormatterTest do
 
   describe "exs ignore" do
     test "evaluates an ignore file and ignores warnings matching the pattern" do
-      warning =
-        {:warn_return_no_exit, {'a/file.ex', 17}, {:no_return, [:only_normal, :format_long, 1]}}
+      warnings = [
+        {:warn_return_no_exit, {'lib/short_description.ex', 17},
+         {:no_return, [:only_normal, :format_long, 1]}},
+        {:warn_return_no_exit, {'lib/file/warning_type.ex', 18},
+         {:no_return, [:only_normal, :format_long, 1]}},
+        {:warn_return_no_exit, {'lib/file/warning_type/line.ex', 19},
+         {:no_return, [:only_normal, :format_long, 1]}}
+      ]
 
       in_project(:ignore, fn ->
         {:error, remaining, _unused_filters_present} =
-          Formatter.format_and_filter([warning], Project, [], :short)
+          Formatter.format_and_filter(warnings, Project, [], :short)
 
         assert remaining == []
       end)
@@ -114,8 +120,10 @@ defmodule Dialyxir.FormatterTest do
 
     expected_warning = "a/another-file.ex:18"
 
-    expected_unused_filter =
-      "Unused filters:\n{\"a/file.ex:17:no_return Function format_long/1 has no local return.\"}"
+    expected_unused_filter = ~s(Unused filters:
+{"lib/short_description.ex:17:no_return Function format_long/1 has no local return."}
+{"lib/file/warning_type.ex", :no_return, 18}
+{"lib/file/warning_type/line.ex", :no_return, 19})
 
     filter_args = [{:list_unused_filters, true}]
 
@@ -126,7 +134,7 @@ defmodule Dialyxir.FormatterTest do
 
           assert {:error, [warning], {:unused_filters_present, unused}} = result
           assert warning =~ expected_warning
-          assert unused =~ expected_unused_filter
+          assert unused == expected_unused_filter
           # A warning for regex_file.ex was explicitly put into format_and_filter.
           refute unused =~ "regex_file.ex"
         end)
diff --git a/test/fixtures/ignore/ignore_test.exs b/test/fixtures/ignore/ignore_test.exs
index 3bcda6f..2a5d070 100644
--- a/test/fixtures/ignore/ignore_test.exs
+++ b/test/fixtures/ignore/ignore_test.exs
@@ -1,4 +1,6 @@
 [
-  {"a/file.ex:17:no_return Function format_long/1 has no local return."},
+  {"lib/short_description.ex:17:no_return Function format_long/1 has no local return."},
+  {"lib/file/warning_type.ex", :no_return, 18},
+  {"lib/file/warning_type/line.ex", :no_return, 19},
   ~r/regex_file.ex.*no local return/
 ]