Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradient does not recognize type of TypedStruct structures #165

Open
Fl4m3Ph03n1x opened this issue Mar 28, 2023 · 5 comments
Open

Gradient does not recognize type of TypedStruct structures #165

Fl4m3Ph03n1x opened this issue Mar 28, 2023 · 5 comments
Labels
triage Needs assessment whether a bug, enhancement, duplicate, ...

Comments

@Fl4m3Ph03n1x
Copy link
Contributor

Fl4m3Ph03n1x commented Mar 28, 2023

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?)

@erszcz
Copy link
Member

erszcz commented Mar 28, 2023

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.

@erszcz erszcz added the triage Needs assessment whether a bug, enhancement, duplicate, ... label Mar 28, 2023
@Fl4m3Ph03n1x
Copy link
Contributor Author

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.

erszcz added a commit that referenced this issue Mar 28, 2023
@Fl4m3Ph03n1x
Copy link
Contributor Author

Is there something else I can do to help?

@erszcz
Copy link
Member

erszcz commented Mar 29, 2023

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.

@Fl4m3Ph03n1x
Copy link
Contributor Author

Fl4m3Ph03n1x commented Mar 29, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
triage Needs assessment whether a bug, enhancement, duplicate, ...
Projects
None yet
Development

No branches or pull requests

2 participants