Does dialyzer check behaviours and callbacks

I’ve just started putting dialyzer into my project in conjunction with writing behaviours for the call back modules.

Up until this point the callbacks that need implementing have only been defined in the documentation.

In my Ace.TCP.Server module I have the following types and callbacks specified.

defmodule Ace.TCP.server do
  @type connection :: %{peer: term}
  @type state :: term

  @callback init(connection, state) ::
    {:send, term, state} |
    {:send, term, state, timeout} |
    {:nosend, state} |
    {:nosend, state, timeout}

  @callback handle_packet(any, state) ::
    {:send, term, state} |
    {:send, term, state, timeout} |
    {:nosend, state} |
    {:nosend, state, timeout}
end
# The rest of the functionality

I have a Forwarder module that uses this behaviour. Forwarder is part of my test suite.

defmodule Forwarder do
  @behaviour Ace.TCP.Server

  def init(conn, pid) do
    send(pid, {:conn, conn})
    {:nosend, pid}
  end

  def handle_packet(_p, _s) do
    
  end
end

When handle_packet/2 is missing I get a warning when running my tests. yet when it is present as above. I.e. returning nil I get no error from dialyzer. So my question is why is dialyzer missing that this callback doesn’t implement the correct types?

As counter-intuitive as it seems you need to add a @spec to each method in the implementation also in order for it to be picked up by dialyzer.

@callback specifications are not automatically applied to their respective implementations. There may be a good reason for this but I don’t know it :stuck_out_tongue:

dialyzer does know about @callback and you don’t need to use @spec.

You should get a warning like “The inferred return type … has nothing in common with … which is expected by the behaviour”.

I may be running on out of date information - a lot has changed since R15 but it was a memory of this “test” that led me to believe this was the case.

I think the problem was the fact that dialyzer does not run over test files. I guess this is deliberate.

Ah! So there are two things going on here. In the top example the callback implementation can never return a value that adheres to the behaviour, i.e. the callback implementation is breaking the contract of the behaviour. Whereas in the linked example the callback implementation is being called directly, and the behaviour contract does not need to be honoured in this case, instead success typing is used and the inferred argument is any term so the call contract is being honoured.

Edit: In general it does not make sense to infer the callback as the spec for explicit calls because the callback implementation will likely have a subset of the callback definition. For example with GenServer the state can be term but it is likely required to be a struct.

Normally for callback implementations the code is never called directly and so dialyzer can’t infer it is called. Often it will be called using a variable: mod.callback_fun(arg)

1 Like