A lot of strange Dialyzer warnings after upgrading from Elixir 1.10.4 to 1.11.2

None of these warnings were output by Dialyzer before the upgrade and none of the relevant code for the warnings has changed.

One set of warnings is covered by this GitHub issue:

The Dialyzer warning:

The function call will not succeed.

:pot.valid_totp(_token :: binary(), binary(), [{:window, 1}])

will never return since it differs in arguments with
positions 3rd from the success typing arguments:

(binary(), any(), [{:token_length, pos_integer()}])

Basically, this function throws the warning:

defmodule PotDialyzerWarningRepro do

  def totp_valid?(token, secret) do
    :pot.valid_totp(token, secret, [{:window, 1}])
  end

end

But this Erlang code – which seems functionally equivalent, doesn’t:

-module(pot_dialyzer_warning_repro_erl).

-export([is_totp_valid/2]).

is_totp_valid(Token, Secret) ->
    pot:valid_totp(Token, Secret, [{window, 1}]).

Changing the 3rd argument in the Elixir code to include the :token_length option does resolve the warning.

Here’s an example of another set of (pairs of) interesting warnings:

web/templates/merchant/freshbooks/_scripts.invoices.html.eex:1:no_return
Function _scripts.invoices.html/1 has no local return.
________________________________________________________________________________
web/templates/merchant/freshbooks/_scripts.invoices.html.eex:1:call
The function call will not succeed.
Partially.Merchant.FreshbooksView.render(
  Partially.Merchant.QuickbooksView,
  <<95, 115, 99, 114, 105, 112, 116, 115, 46, 105, 110, 118, 111, 105, 99, 101, 115, 46,
    104, 116, 109, 108>>
)
will never return since it differs in arguments with
positions 1st from the success typing arguments:
(
  binary()
  | maybe_improper_list(
      binary() | maybe_improper_list(any(), binary() | []) | char(),
      binary() | []
    ),
  any()
)
________________________________________________________________________________

Here’s the contents of web/templates/merchant/freshbooks/_scripts.invoices.html.eex:

<%= render Partially.Merchant.QuickbooksView, "_scripts.invoices.html" %>

and here’s the corresponding view (web/views/merchant/freshbooks_view.ex):

defmodule Partially.Merchant.FreshbooksView do
  use Partially.Web, :view
end

The use Partially.Web, :view itself (generates code that) calls use Phoenix.View.

Based on the Phoenix.View source the generated render/2 function, the template code should be handled by this clause:

      def render(module, template) when is_atom(module) do
        Phoenix.View.render(module, template, %{})
      end

I’m note sure where Dialyzer is getting the “success typing arguments”. It looks like maybe from the render_template/2 function(s) generated by Phoenix.Template perhaps.

Both warnings can be resolved by changing the template code to this:

<%= render Partially.Merchant.QuickbooksView, "_scripts.invoices.html", [] %>

But beyond resolving the warnings, I’m curious to know what they mean and why they seem to only have suddenly appeared after our recent Elixir upgrade.

As part of the Elixir upgrade, we upgraded the appsignal package too and that included changes that “reimplements the render/2 function to add instrumentation”.

I think that refers to the render/2 function generated by use Phoenix.View.

I created an issue in the GitHub project for the AppSignal package:

The Dialyxir (not Dialyzer) maintainer commented on the pot issue on the Dialyxir GitHub project:

… A better minimal example would be one without pot. That would clarify exactly where in the definition or implementation the issue lies. When you say you can’t reproduce the same error with the Erlang code, are you running dialyzer the same way in both cases? Can you reproduce it against the compiled Elixir beams by just running the dialyzer CLI?

I just replied:

A better minimal example would be one without pot. That would clarify exactly where in the definition or implementation the issue lies.

I’m not sure I understand what you’re suggesting. Copy the pot code to the minimal repro repo? I’m a little confused because I’m not sure whether the error is because the pot code is a dependency, nor do I know, or even suspect, what in particular is causing the problem.

Having written the preceding though, I do think I might understand your reasons (and wisdom) for offering that suggestion. I’ll try to replace the pot dependency in the repro code and test whether I can still repro the Dialyzer warnings.

When you say you can’t reproduce the same error with the Erlang code, are you running dialyzer the same way in both cases? Can you reproduce it against the compiled Elixir beams by just running the dialyzer CLI?

No and I don’t know yet how to do either of those things.

In the repro-Erlang code, I’m running rebar3 dialyzer in the root directory of the repo. For the Elixir code, I’m using Dialyxir via mix dialyzer.

How can I run Dialyzer the same way for both repro-repos?

Also, how can I run the Dialyzer CLI against the compiled Elixir BEAM files?

If those questions are ‘too big’ to easily answer, can you suggest search terms to hopefully point me in the right direction? I’ll try to search anyways myself when I have some time.

The ‘view’ warnings seem to be because of a new function generated by the new version of the AppSignal package:

… It does look like maybe the render/2 function generated by Appsignal.Phoenix.View might be what Dialyzer is finding for its “success typing”:

      def render(template, assigns) do
        {root, _pattern, _names} = __templates__()
        path = Path.join(root, template)

        Appsignal.instrument("Render #{path}", fn span ->
          @span.set_attribute(span, "title", path)
          @span.set_attribute(span, "appsignal:category", "render.phoenix_template")
          super(template, assigns)
        end)
      end

It looks like maybe the success typing is coming from Path.join/2; it’s spec:

@spec join(t, t) :: binary

Where t() is:

t() :: IO.chardata()

And IO.chardata() is:

chardata() ::
  String.t() | maybe_improper_list(char() | chardata(), String.t() | [])

There’s a third set of strange Dialyzer warnings; an example:

web/views/admin/affiliate_view.ex:2:guard_fail
The guard test:

is_atom(
  _ ::
    binary()
    | maybe_improper_list(
        binary() | maybe_improper_list(any(), binary() | []) | char(),
        binary() | []
      )
)

can never succeed.

The file web/views/admin/affiliate_view.ex:

defmodule Partially.Admin.AffiliateView do
    use Partially.Web, :view
end

Partially.Web.__using__/1:

  @doc """
  When used, dispatch to the appropriate controller/view/etc.
  """
  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end

I did think it odd that a guard was being used with a macro – I couldn’t find any info about whether that is allowed (or not) and couldn’t find anything in a few cursory web searches. (That seemed to be mostly because results for ‘elixir macro guard’ were mostly about generating functions with guards using macros.)

Or maybe Dialyzer is complaining about some specific code generated by use Partially.Web, :view?

The success typing in the warning does look (somewhat suspiciously) the same as in the warnings for the render/2 calls.