gradient
gradient copied to clipboard
Gradient does not recognize type of TypedStruct structures
I have a module that uses TypedStruct
to create structs. This is the code:
defmodule Shared.Data.Authorization do
@moduledoc """
Saves authorization details for a user. It also contains other details.
"""
use TypedStruct
alias Shared.Utils.Structs
@type authorization :: %{
(cookie :: String.t()) => String.t(),
(token :: String.t()) => String.t()
}
@derive Jason.Encoder
typedstruct enforce: true do
@typedoc "Authorization information for a user"
field(:cookie, String.t())
field(:token, String.t())
end
@spec new(authorization()) :: __MODULE__.t()
def new(%{"cookie" => cookie, "token" => token} = auth)
when is_binary(cookie) and is_binary(token) do
Structs.string_map_to_struct(auth, __MODULE__)
end
end
The problem here is that gradient does not seem to understand that t()
is created, so it errors out:
lib/data/authorization.ex: The function call on line 26 is expected to have type t() but it has type struct() 24 def new(%{"cookie" => cookie, "token" => token} = auth) 25 when is_binary(cookie) and is_binary(token) do 26 Structs.string_map_to_struct(auth, MODULE) 27 end
For additional context, here is the string_map_to_struct
code:
@spec string_map_to_struct(map, module | struct) :: struct
def string_map_to_struct(data, target_struct) do
data
|> Morphix.atomorphiform!() # string_map to atom_map
|> data_to_struct(target_struct)
end
@spec data_to_struct(Enumerable.t(), module | struct) :: struct
def data_to_struct(data, target_struct), do: struct(target_struct, data)
I decided to convert that code into its native form using defstruct
:
defmodule Shared.Data.Authorization do
@moduledoc """
Saves authorization details for a user. It also contains other details.
"""
alias Shared.Utils.Structs
@enforce_keys [:cookie, :token]
defstruct [:cookie, :token]
@type authorization :: %{
(cookie :: String.t()) => String.t(),
(token :: String.t()) => String.t()
}
@typedoc "Authorization information for a user"
@type t() :: %__MODULE__{
cookie: String.t(),
token: String.t()
}
@spec new(authorization()) :: t()
def new(%{"cookie" => cookie, "token" => token} = auth)
when is_binary(cookie) and is_binary(token) do
Structs.string_map_to_struct(auth, __MODULE__)
end
end
Gradient does not complain here.
Is there a fix for this? (other than removing typed struct?)
Hi, @Fl4m3Ph03n1x!
Thanks for raising this.
Could you also share the definition of Structs.string_map_to_struct/2
(or the entire Shared.Utils.Structs
module for simplicity's sake)?
Are you running the analysis on source files or beam files?
Thanks to Gradient.Debug.print_erlang()
I can see the type t()
gets generated, so if the analysis is run on the beam files the type should be seen by Gradient.
However, the undefined function Structs.string_map_to_struct/2
prevents the analysis from proceeding further, so it's hard to say what else is going on.
Could you also share the definition of Structs.string_map_to_struct/2 (or the entire Shared.Utils.Structs module for simplicity's sake)?
Sure !
defmodule Shared.Utils.Structs do
@moduledoc """
Set of functions used across the app, for utility purposes, like dealing with
tuples, maps and other data structures.
"""
alias Morphix
@spec string_map_to_struct(
data :: map,
target_struct :: module | struct
) ::
target_struct :: struct
def string_map_to_struct(data, target_struct) do
data
|> Morphix.atomorphiform!() # string maps to atom maps
|> data_to_struct(target_struct)
end
@spec data_to_struct(data :: Enumerable.t(), target_struct :: module | struct) ::
target_struct :: struct
def data_to_struct(data, target_struct), do: struct(target_struct, data)
end
Are you running the analysis on source files or beam files?
I am running mix gradient
in my project. It runs through everything.
Is there something else I can do to help?
I've looked at it a bit more, but it's not clear to me yet where the error comes from. It's certain the type is defined and the error message doesn't say that it's missing either. However, I'm not sure why the specific struct type t()
is not treated as a subtype of the built-in type struct()
.
A workaround would be to use a type assertion:
use Gradient.TypeAnnotation
s = Structs.string_map_to_struct(auth, __MODULE__)
annotate_type(s, any())
any()
is compatible with any type, so it's also compatible with t()
. However, if we do this, we should make sure at runtime that s
really never is anything other than type t()
defines, as we're evidently going against what the typechecker says in this case. The thing is it's not clear yet if the typechecker is wrong or if we are wrong.
If it is of any help, this module has the same issue, t()
not being considered a subtype of struct()
, however in this case, not even the version using native elixir code works:
Version using TypedStruct
:
defmodule Shared.Data.User do
@moduledoc """
Represents relevant User information for clients using this AuctionHouse.
"""
use TypedStruct
alias Shared.Utils.Structs
@type user ::
%{
(ingame_name :: String.t()) => String.t(),
(patreon? :: String.t()) => boolean()
}
| [ingame_name: String.t(), patreon?: boolean()]
@derive Jason.Encoder
typedstruct enforce: true do
@typedoc "User information"
field(:ingame_name, String.t())
field(:patreon?, boolean())
end
@spec new(user()) :: __MODULE__.t()
def new(%{"ingame_name" => name, "patreon?" => patreon?} = user)
when is_binary(name) and is_boolean(patreon?) do
Structs.string_map_to_struct(user, __MODULE__)
end
def new([ingame_name: name, patreon?: patreon?] = user)
when is_binary(name) and is_boolean(patreon?),
do: struct(__MODULE__, user)
end
Returns with error:
lib/data/user.ex: The function call on line 28 is expected to have type t() but it has type struct()
26 def new(%{"ingame_name" => name, "patreon?" => patreon?} = user)
27 when is_binary(name) and is_boolean(patreon?) do
28 Structs.string_map_to_struct(user, __MODULE__)
29 end
While the version using native code:
defmodule Shared.Data.User do
@moduledoc """
Represents relevant User information for clients using this AuctionHouse.
"""
alias Shared.Utils.Structs
@enforce_keys [:ingame_name, :patreon?]
defstruct [:ingame_name, :patreon?]
@type user ::
%{
(ingame_name :: String.t()) => String.t(),
(patreon? :: String.t()) => boolean()
}
| [ingame_name: String.t(), patreon?: boolean()]
@typedoc "User information"
@type t() :: %__MODULE__{
ingame_name: String.t(),
patreon?: boolean()
}
@spec new(user()) :: t()
def new(%{"ingame_name" => name, "patreon?" => patreon?} = user)
when is_binary(name) and is_boolean(patreon?),
do: Structs.string_map_to_struct(user, __MODULE__)
def new([ingame_name: name, patreon?: patreon?] = user)
when is_binary(name) and is_boolean(patreon?),
do: struct(__MODULE__, user)
end
Errors with:
lib/data/user.ex: The function call Shared.Utils.Structs.string_map_to_struct(user, Shared.Data.User) on line 62 is expected to have type t() but it has type struct()
Interestingly enough, this will still happen if I inline the code:
@spec new(user()) :: __MODULE__.t()
def new(%{"ingame_name" => name, "patreon?" => patreon?} = user)
when is_binary(name) and is_boolean(patreon?) do
atom_map = Morphix.atomorphiform!(user)
struct(__MODULE__, atom_map)
end
Will complain with:
lib/data/user.ex: The function call on line 36 is expected to have type t() but it has type struct()
34 def new([ingame_name: name, patreon?: patreon?] = user)
35 when is_binary(name) and is_boolean(patreon?),
36 do: struct(__MODULE__, user)
37 end
So at this point, I really do think there is a problem with struct
type and t
type, as there is no extra code here to evaluate.