esl / gradient

Gradient is a static typechecker for Elixir
Apache License 2.0
437 stars 13 forks source link

Not detecting types from structs correctly #41

Open Fl4m3Ph03n1x opened 2 years ago

Fl4m3Ph03n1x commented 2 years ago

Background

Let's imagine I have the following struct in a file called book.ex:

defmodule Book do
  @enforce_keys [:title, :authors]
  defstruct [:title, :authors]

  @type t :: %__MODULE__{
          title: String.t(),
          authors: [String.t()]
        }
end

Now let's define another module that actually uses this:

defmodule DealingWithListsOfLists do
  alias Book

  @spec recommended_books(String.t()) :: [Book]
  def recommended_books(friend) do
    scala = [
      %Book{title: "FP in Scala", authors: ["Chiusano", "Bjarnason"]},
      %Book{title: "Get Programming with Scala", authors: ["Sfregola"]}
    ]

    fiction = [
      %Book{title: "Harry Potter", authors: ["Rowling"]},
      %Book{title: "The Lord of the Rings", authors: ["Tolkien"]}
    ]

    case friend do
      "Alice" -> scala
      "Bob" -> fiction
      _ -> []
    end
  end
end

The careful reader will see that the specification for the recommended_books functions is wrong. It should be returns [Book.t()] and not [Book], which is the struct.

Actual behaviour

$ mix gradient
Typechecking files...
No problems found!

Expected behaviour

It should complain that the specification for the return type is incorrect. Dialyzer does this (although, in quite a cryptic manner):

lib/cb_5.7/dealing_with_lists_of_lists.ex:4:invalid_contract
The @spec for the function does not match the success typing of the function.

Function:
DealingWithListsOfLists.recommended_books/1

Success typing:
@spec recommended_books(_) :: [%Book{:authors => [<<_::56, _::size(8)>>, ...], :title => <<_::64, _::size(8)>>}]

________________________________________________________________________________
done (warnings were emitted)
Halting VM with exit status 2
erszcz commented 2 years ago

Thanks, @Fl4m3Ph03n1x! This is an interesting example of a false negative :+1: Marking it as a bug.

erszcz commented 2 years ago

It should complain that the specification for the return type is incorrect.

Or that the returned value is of a wrong type if we assume that specs are the source of truth (they are for Gradient / Gradualizer). In any case, we should get a warning hinting at the discrepancy between the spec and the actual value.

I have a hunch that [Book] in

  @spec recommended_books(String.t()) :: [Book]

is interpreted as a list of atoms Book which is a valid spec, since a singleton atom is a valid type. However, Gradient should then report a warning that the returned list [Book.t()] does not match the declared type [Book]. This needs further investigation.