Dialyzer error - Invalid type specification for function error even though it looks correct

I’m trying to write a @spec for this function that was made by mix phx.gen.auth:

# lib/my_app/identity/user_token.ex

@spec build_session_token(MyApp.Identity.User.t()) :: {binary(), t()}
def build_session_token(user) do
  token = :crypto.strong_rand_bytes(@rand_size)
  {token, %UserToken{token: token, context: "session", user_id: user.id}}
end

Unfortunately, I’m getting this error from Dialyxir:

Invalid type specification for function 'Elixir.MyApp.Identity.UserToken':build_session_token/1.
 The success typing is 'Elixir.MyApp.Identity.UserToken':build_session_token(atom() | #{'id':=_, _=>_}) -> {binary(),#{'__meta__':=#{'__struct__':='Elixir.Ecto.Schema.Metadata', 'context':='nil', 'prefix':='nil', 'schema':='Elixir.MyApp.Identity.UserToken', 'source':=<<_:96>>, 'state':='built'}, '__struct__':='Elixir.MyApp.Identity.UserToken', 'context':=<<_:56>>, 'id':='nil', 'inserted_at':='nil', 'sent_to':='nil', 'token':=binary(), 'user':=#{'__cardinality__':='one', '__field__':='user', '__owner__':='Elixir.MyApp.Identity.UserToken', '__struct__':='Elixir.Ecto.Association.NotLoaded'}, 'user_id':=_}}
 But the spec is 'Elixir.MyApp.Identity.UserToken':build_session_token('Elixir.MyApp.Identity.User':t()) -> {binary(),t()}
 The return types do not overlap

I think it looks correct, so I’m confused why I’m getting this error. What should I do to fix this?

EDIT:

# lib/my_app/identity/user_token.ex

defmodule MyApp.Identity.UserToken do

  @type t :: %__MODULE__{
          __meta__: Ecto.Schema.Metadata.t(),
          id: integer() | nil,
          token: binary(),
          context: String.t(),
          sent_to: String.t(),
          user_id: integer(),
          inserted_at: NaiveDateTime.t() | nil
        }


  schema "users_tokens" do
  # ...
  end
end
# lib/my_app/identity/user.ex

defmodule MyApp.Identity.User do

  @type t :: %__MODULE__{
          __meta__: Ecto.Schema.Metadata.t(),
          id: integer() | nil,
          email: String.t() | nil,
          password: String.t() | nil,
          hashed_password: String.t() | nil,
          confirmed_at: NaiveDateTime.t() | nil,
          inserted_at: NaiveDateTime.t() | nil,
          updated_at: NaiveDateTime.t() | nil
        }

  schema "users" do
    # ...
  end
end

Ecto doesn’t generate typespecs, you have to do that yourself or use a library to auto-generate them. I can’t remember the name of the lib but it’s probably pretty easily found!

EDIT:

Oh, I dunno then :sweat_smile:

Sorry, I forgot to include the typespecs that I have. I actually have typespecs for my Ecto schemas. I’ve edited them into my main post.

In the UserToken typespec I see

But in the function I see the return value:

%UserToken{token: token, context: "session", user_id: user.id}

If that means that sent_to is inferred to be nil in the return type, then it doesn’t match the spec that says it must be a string. I’ve only glanced through this, so its possible that I’ve missed a detail, but that’s the one thing I see that doesn’t look quite right.

6 Likes

That fixed it. Thank you!

1 Like

Some issues with this typespec relative to the code in build_session_token/1:

  • as @sbuttgereit called out, sent_to isn’t able to be nil so writing %UserToken{...} without sent_to isn’t going to produce a UserToken.t()

  • user.id is specced as integer() | nil, but it’s assigned to user_id which is only integer().

1 Like

I have to admit… when I’m typespecing Ecto.Schema structs I set all the fields, with the exception of :__meta__, as typed: <some type> | nil without any further consideration.

I end up doing this for two reasons. The first is there are enough intermediate states for the data on its way to being complete and database ready that any attempt to represent the “completed state” of the data in the struct typespec just ends up with the kinds of errors reported here… and having to sort through and trying to reconcile the additional issues you found with the examples in the original question. The second reason is that avoiding the mental overhead of figuring out workable non-nil states has a significantly higher payoff than actually finding those edges where intermediate states could be finessed for Dialyzer.

For really keeping the data integral I’m depending on the database itself anyway. Yeah, that’s runtime and not compile time, but I think that’s the best to be done since the intermediate state problem means really typing these Schema structs more strictly means typing them in a way that’s not going to match the database requirements anyway… its just not feasible (that I can see).

3 Likes

Yeah I fully agree with you here, I do the same 99% of the time. Elixir has no static typing and as you said Ecto is basically a runtime concern, not a compile-time one, so I never even put any specs on anything Ecto-related. There’s really no point. You might catch a bug twice a year but you have spent who knows how many hours adding those specs.

1 Like