Repository: jeremyjh/dialyxir (elixir)

Issue:
Failed to parse warning
Hi, got an error with the message to report this. :)

```
Please file a bug in https://github.com/jeremyjh/dialyxir/issues with this message.

Failed to parse warning:
[{:"(", 1}, {:atom_full, 1, '\'nil\''}, {:",", 1}, {:atom_full, 1, '\'Elixir.Dnsimple.Events.HostCreateRequested\''}, {:":", 1}, {:atom_part, 1, 't'}, {:"(", 1}, {:")", 1}, {:")", 1}, {:->, 1}, {:"{", 1}, {:atom_full, 1, '\'ok\''}, {:",", 1}, {:"{", 1}, {:atom_full, 1, '\'Elixir.Dnsimple.Models.Host\''}, {:":", 1}, {:atom_part, 1, 't'}, {:"(", 1}, {:")", 1}, {:",", 1}, {:atom_full, 1, '\'Elixir.Dnsimple.Models.Order\''}, {:":", 1}, {:atom_part, 1, 't'}, {:"(", 1}, {:")", 1}, {:"}", 1}, {:"}", 1}, {:atom_part, 2, ';'}, {:"(", 2}, {:"{", 2}, {:atom_full, 2, '\'Elixir.Dnsimple.Models.Host\''}, {:":", 2}, {:atom_part, 2, 't'}, {:"(", 2}, {:")", 2}, {:",", 2}, {:atom_full, 2, '\'Elixir.Dnsimple.Models.Order\''}, {:":", 2}, {:atom_part, 2, 't'}, {:"(", 2}, {:")", 2}, {:"}", 2}, {:",", 2}, {:atom_full, 2, '\'Elixir.Dnsimple.Events.HostCreateSucceeded\''}, {:":", 2}, {:atom_part, 2, 't'}, {:"(", 2}, {:")", 2}, {:")", ...}, {...}, ...]


Legacy warning:
apps/dnsimple/lib/dnsimple/app/hosts.ex:16: The call 'Elixir.Dnsimple.App.Hosts':apply_event(Vhost_and_order@1::{#{'__meta__':=_, '__struct__':='Elixir.Dnsimple.Models.Host', 'id':=binary(), 'inserted_at':='nil' | #{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}, 'ipv4':='nil' | [binary()], 'ipv6':='nil' | [binary()], 'name':=binary(), 'revision':='nil' | integer(), 'updated_at':='nil' | #{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}},#{'__struct__':='Elixir.Dnsimple.Models.Order', 'error':=_, 'id':=binary(), 'inserted_at':='nil' | #{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}, 'revision':='nil' | integer(), 'status':='completed' | 'errored' | 'in_process', 'type':='domain_registration' | 'host_create', 'updated_at':='nil' | #{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}}},Vfailed_event@1::#{'__struct__':='Elixir.Dnsimple.Events.HostCreateErrored', 'error':=#{'__struct__':='Elixir.Dnsimple.Errors.ContactNotFound' | 'Elixir.Dnsimple.Errors.ContactNotInRegistry' | 'Elixir.Dnsimple.Errors.DomainAlreadyRegistered' | 'Elixir.Dnsimple.Errors.DomainNotFound' | 'Elixir.Dnsimple.Errors.EppError' | 'Elixir.Dnsimple.Errors.OrderNotFound' | 'Elixir.Dnsimple.Errors.PersistenceError' | 'Elixir.Dnsimple.Errors.UnsupportedEtld' | 'Elixir.Dnsimple.Errors.ValidationFailure', 'code'=>integer(), 'contact_id'=>binary(), 'errors'=>[{atom(),{binary(),[{atom(),_}]}}], 'id'=>binary(), 'msg'=>binary(), 'name'=>binary(), 'raw_response'=>#{'__struct__':='Elixir.Exepp.Response.Command', 'extensions':=_, 'message_queue':=#{'__struct__':='Elixir.Exepp.Response.MessageQueue', 'count':=integer(), 'id':=binary(), 'msg':=binary() | map(), 'qdate':=#{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}}, 'response_data':=[#{'__struct__':='Elixir.Exepp.Response.Domains.Check', 'available':=boolean(), 'lang':='nil' | binary(), 'name':=binary(), 'reason':='nil' | binary()}] | #{'__struct__':='Elixir.Exepp.Response.Contacts.Check' | 'Elixir.Exepp.Response.Contacts.Create' | 'Elixir.Exepp.Response.Domains.Create' | 'Elixir.Exepp.Response.Hosts.Check', 'available'=>boolean(), 'crdate'=>'nil' | #{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}, 'exdate'=>'nil' | #{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}, 'id'=>binary(), 'lang'=>binary(), 'name'=>binary(), 'reason'=>binary()}, 'results':=[#{'__struct__':='Elixir.Exepp.Response.Result', 'code':=integer(), 'extension_values':=map(), 'msg':=map(), 'values':=[map()]}], 'transaction_id':=#{'__struct__':='Elixir.Exepp.Response.TransactionId', 'client_transaction_id':=binary(), 'server_transaction_id':=binary()}}, 'registry_key'=>atom(), 'tld'=>binary()}, 'host_id':=binary(), 'name':=binary(), 'timestamp':=#{'__struct__':='Elixir.DateTime', 'calendar':=atom(), 'day':=integer(), 'hour':=integer(), 'microsecond':={char(),0 | 1 | 2 | 3 | 4 | 5 | 6}, 'minute':=integer(), 'month':=integer(), 'second':=integer(), 'std_offset':=integer(), 'time_zone':=binary(), 'utc_offset':=integer(), 'year':=integer(), 'zone_abbr':=binary()}}) breaks the contract ('nil','Elixir.Dnsimple.Events.HostCreateRequested':t()) -> {'ok',{'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()}}
    ; ({'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()},'Elixir.Dnsimple.Events.HostCreateSucceeded':t()) -> {'ok',{'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()}} in the 2nd argument
________________________________________________________________________________

```

Pull request description:
Fix semicolon contract pretty printing.
Resolves #202 

Patch:
diff --git a/lib/dialyxir/pretty_print.ex b/lib/dialyxir/pretty_print.ex
index 2a98faf..534d3a4 100644
--- a/lib/dialyxir/pretty_print.ex
+++ b/lib/dialyxir/pretty_print.ex
@@ -85,6 +85,33 @@ defmodule Dialyxir.PrettyPrint do
   end
 
   def pretty_print_contract(str) do
+    multiple_heads? =
+      str
+      |> to_string()
+      |> String.contains?(";")
+
+    if multiple_heads? do
+      [head | tail] =
+        str
+        |> to_string()
+        |> String.split(";")
+
+      joiner = "Contract head: "
+
+      pretty =
+        Enum.map_join([head | tail], joiner, fn str ->
+          str
+          |> to_charlist()
+          |> do_pretty_print_contract()
+        end)
+
+      joiner <> pretty
+    else
+      do_pretty_print_contract(str)
+    end
+  end
+
+  def do_pretty_print_contract(str) do
     prefix = "@spec a"
     suffix = "\ndef a() do\n  :ok\nend"
     pretty = pretty_print(str)
diff --git a/lib/dialyxir/warning_helpers.ex b/lib/dialyxir/warning_helpers.ex
index 4b7b456..e4600ee 100644
--- a/lib/dialyxir/warning_helpers.ex
+++ b/lib/dialyxir/warning_helpers.ex
@@ -41,7 +41,7 @@ defmodule Dialyxir.WarningHelpers do
       ) do
     pretty_contract = Dialyxir.PrettyPrint.pretty_print_contract(contract)
 
-    if Enum.empty?(arg_positions) or overloaded? do
+    if Enum.empty?(arg_positions) || overloaded? do
       # We do not know which arguments caused the failure
       """
       breaks the contract


Test patch:
diff --git a/test/pretty_print_test.exs b/test/pretty_print_test.exs
index 86ea9c6..f2212c0 100644
--- a/test/pretty_print_test.exs
+++ b/test/pretty_print_test.exs
@@ -380,4 +380,17 @@ defmodule Dialyxir.Test.PretyPrintTest do
 
     assert pretty_printed == expected_output
   end
+
+  test "contracts with semicolons are pretty printed appropriately" do
+    input = ~S"""
+    ('nil','Elixir.Dnsimple.Events.HostCreateRequested':t()) -> {'ok',{'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()}} ; ({'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()},'Elixir.Dnsimple.Events.HostCreateSucceeded':t()) -> {'ok',{'Elixir.Dnsimple.Models.Host':t(),'Elixir.Dnsimple.Models.Order':t()}}
+    """
+
+    pretty_printed = Dialyxir.PrettyPrint.pretty_print_contract(input)
+
+    expected_output =
+      "Contract head: (nil, Dnsimple.Events.HostCreateRequested.t()) ::\n  {:ok, {Dnsimple.Models.Host.t(), Dnsimple.Models.Order.t()}}Contract head: (\n  {Dnsimple.Models.Host.t(), Dnsimple.Models.Order.t()},\n  Dnsimple.Events.HostCreateSucceeded.t()\n) :: {:ok, {Dnsimple.Models.Host.t(), Dnsimple.Models.Order.t()}}"
+
+    assert pretty_printed == expected_output
+  end
 end