Qqwy / elixir-type_check

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support with multiple arity functions

lamp-town-guy opened this issue · comments

I have trouble with functions with different arity. I added type checks to all of them and now I get this weird error. When I check only for %{} I don't get an error and everything works as expected.

  @doc false
  @spec! validate(%Ingest{} | %{}) :: %Ecto.Changeset{}
  def validate(%Ingest{} = ingest) do
    validate(ingest, %{})
  end

  def validate(attrs) do
    validate(%__MODULE__{}, attrs)
  end

  @spec! validate(%Ingest{} | %__MODULE__{}, %{}) :: %Ecto.Changeset{}
  def validate(%Ingest{} = ingest, attrs) do
    from_model(ingest) |> validate(attrs)
  end

  def validate(%__MODULE__{} = ingest, attrs) do
    {ingest, @types}
    |> cast(attrs, Map.keys(@types))
    |> validate_required(@required)
    |> validate_length(:name, min: 2, max: 100)
  end


  @spec! from_model(%Ingest{} | %__MODULE__{}) :: %__MODULE__{}
  def from_model(%__MODULE__{} = ingest), do: ingest

  def from_model(%Ingest{} = ingest) do
    m = Map.from_struct(ingest)
    struct(__MODULE__, m)
  end

It should be returning Changeset. In another part of my code I'm checking for Changesets and it works as expected.

I'm just testing it after I heard about it on the Thinking Elixir podcast. So far I'm happy how it works.

Thank you for the issue! I will investigate what is going on here.

Could you maybe copy-paste the error which is printed when you try using this code?

Sorry I forgot the most important part. It's a strange thing that I'm getting an error outside my code where the function is being called. It's in a live view.

[error] GenServer #PID<0.1941.0> terminating
** (MatchError) no match of right hand side value: %Inspect.Error{message: "got FunctionClauseError with message \"no function clause matching in Inspect.Ecto.Changeset.inspect/2\" while inspecting %{__struct__: Ecto.Changeset}"}
    (phoenix_ecto 4.4.0) lib/phoenix_ecto/html.ex:4: Phoenix.HTML.FormData.Ecto.Changeset.to_form/2
    (phoenix_live_view 0.16.4) lib/phoenix_live_view/helpers.ex:935: Phoenix.LiveView.Helpers.form/1
    (phoenix_live_view 0.16.4) lib/phoenix_live_view/helpers.ex:508: Phoenix.LiveView.Helpers.__component__/3
    (app 0.1.0) lib/app_web/live/streams/ingest_live/form_component.html.heex:1: anonymous fn/2 in AppWeb.Streams.IngestLive.FormComponent.render/1

I was able to reproduce it with the following example code

defmodule Ingest do
  defstruct name: "Foo", age: 42
  import Ecto.Changeset
end


defmodule Example do
  use TypeCheck
  import Ecto.Changeset

  defstruct name: "Foo", age: 42
  @types %{name: :string, age: :integer}
  @required [:name]

  @doc false
  @spec! validate(%Ingest{} | %{}) :: %Ecto.Changeset{}
  def validate(%Ingest{} = ingest) do
    validate(ingest, %{})
  end

  def validate(attrs) do
    validate(%__MODULE__{}, attrs)
  end

  @spec! validate(%Ingest{} | %__MODULE__{}, %{}) :: %Ecto.Changeset{}
  def validate(%Ingest{} = ingest, attrs) do
    from_model(ingest) |> validate(attrs)
  end

  def validate(%__MODULE__{} = ingest, attrs) do
    {ingest, @types}
    |> cast(attrs, Map.keys(@types))
    |> validate_required(@required)
    |> validate_length(:name, min: 2, max: 100)
  end


  @spec! from_model(%Ingest{} | %__MODULE__{}) :: %__MODULE__{}
  def from_model(%__MODULE__{} = ingest), do: ingest

  def from_model(%Ingest{} = ingest) do
    m = Map.from_struct(ingest)
    struct(__MODULE__, m)
  end
end

and then running

iex> ingest = %Ingest{}
iex> Example.validate(ingest)

My current working theory is that the spec fails and that while printing the error(?), some internal Ecto type (probably some kind of Ecto.Changeset) does not have the structure that is expected, resulting in inspect instead returning an %Inspect.Error{}.

When this struct is then returned to the calling code, it of course is something completely different than from what it had expected, resulting in a crash further down the line.


Debugging continues...

Another odd thing. As written:

iex> ingest = %Ingest{name: "Foo", age: 42}
iex> Example.from_model(ingest)
%{__struct__: Example} # Clearly a map with only the struct key. The name and age keys are now missing.

With the @spec! commented it creates the expected result:

iex> ingest = %Ingest{name: "Foo", age: 42}
iex> Example.from_model(ingest)
%Example{name: "Foo", age: 42}

It is clear there is a bug as to how %__MODULE__{} (or types of structs without keys in general) are handled by TypeCheck.
This might very well be the cause of the other problem: The result passing the spec, but then being turned into %{__struct__: Ecto.Changeset}.

I think the problem is that currently TypeCheck considers %{} to be (as per Elixir's Typespec page) an empty map, and %{a: any()) to be a map containing exactly one key (namely: :a).
However, structs are desugared to %{__struct__: StructName} which then is considered the same way. This is not correct; as per Elixir's Typespec page, %StructName{} should match a map with all of the struct's keys (with arbitrary values).

In previous versions of TypeCheck, the value passed to a check was never altered (so it 'happened to work'). However, in the latest minor version, support for type-checking function parameters was added, which meant that we had to alter the passed-in value in some cases. This thus broke inline-written struct types.


The fix is to transform a struct-type not into %{__struct__: StructName} but instead be more clever and actually look up what keys the struct uses (which should be possible by looking at the defstruct).

In the meantime, what you can do as a circumvention for now is to add a type with the explicit values you expect the output to have to the module, and then use that one for the results. For Ecto.Changeset this will require using a type override.

@lamp-town-guy I have made a PR which contains a fix.
Please let me know if this fix also works on your actual codebase 😊 .

(You can use that particular version of TypeCheck by changing the line in your mix.exs to {:type_check, github: "Qqwy/elixir-type_check", branch: "78-struct_fields_bug"},.

Turning stuff into %{__struct__: StructName} was a problem I encountered elsewhere in the code with Ecto.Query. With update everything works as expected.

Thank you for such quick fix of this issue.

Thanks for letting me know!

Feel free to use the version from the PR for now. I will add some regression tests to it and then release a new version soon (hopefully tomorrow), and I'll ping you when I do so.

@lamp-town-guy version 0.10.4 which includes the fixes has been published! 🥳