How to use dialyzer with multiple specs for same function

Hi,
I’m trying to integrate dialyzer on a project using Broadway and it seems dialyzer is returning spec error when trying to call functions like Broadway.Message.ack_immediatelly/1:

The function call will not succeed.

Broadway.Message.ack_immediately(_message :: %Broadway.Message{:data => _, _ => _})

will never return since the 1st arguments differ
from the success typing arguments:

(maybe_improper_list())

When calling the function with message structure as list, the dialyzer check succeeds.

The spec on ack_immediately is:

  @spec ack_immediately(message :: Message.t()) :: Message.t()
  @spec ack_immediately(messages :: [Message.t(), ...]) :: [Message.t(), ...]
  def ack_immediately(message_or_messages)

It seems that dialyzer cannot use the new spec system, is there a possible fix for this?

Dialyzer will “rule out” specs that it determines aren’t relevant based on context. For instance, the single Message.t() clause can’t be relevant in this code:

one_message
|> Broadway.Message.ack_immediately()
|> hd()

This doesn’t seem the case.

Calling the ack_immediately with:

Broadway.Message.ack_immediately(message)

Will always result in error, while doing the same call with list:

Broadway.Message.ack_immediately([message])

Will be successful.

@spec ack_immediately(message :: Message.t()) :: Message.t()
  @spec ack_immediately(messages :: [Message.t(), ...]) :: [Message.t(), ...]

is essentially equal to

@spec ack_immediately(Message.t() | [Message.t(), ...]) :: Message.t() | [Message.t(), ...]

Dialyzer can handle the latter, so it should be able to handle the former. The real question here is why dialyzer thinks the success typing of the function should be just a list and not a single value and that’s completely unrelated to what manual spec there is in the code.

2 Likes

For some reason, it seems that the dialyzer error code is not relevant in this case.
Investigating further, the bodies of the functions are:

def ack_immediately(%Message{} = message) do
    [message] = ack_immediately([message])
    message
  end

  def ack_immediately(messages) when is_list(messages) and messages != [] do
    {successful, failed} = Enum.split_with(messages, &(&1.status == :ok))
    _ = Broadway.Acknowledger.ack_messages(successful, failed)

    for message <- messages do
      %{message | acknowledger: {NoopAcknowledger, _ack_ref = nil, _data = nil}}
    end
  end

Commenting the

_ = Broadway.Acknowledger.ack_messages(successful, failed)

solves the dialyzer error.

Running dialyzer on broadway, it also has a few of these type errors, there might be some kind of bugs in the library.