Elixir: Doctest generates an incompatible type warning

Created on 5 Nov 2020  路  5Comments  路  Source: elixir-lang/elixir

Environment

  • Elixir & Erlang/OTP versions (elixir --version): 1.11.2 / 23.1.1
  • Operating system: Linux 5.9.3-arch1-1 #1 SMP PREEMPT Sun, 01 Nov 2020 12:58:59 +0000 x86_64 unknown unknown GNU/Linux

Current behavior

We have a function for validating a certain struct, but our doctest seems to generate a warning. The warning itself is obvious, but the test is also to show it's wrong.

defmodule Foo do
  defstruct [:bar]

  @doc """
  Returns if the argument is a valid Foo struct or not.

  iex> Foo.is_valid?(%{})
  false
  iex> Foo.is_valid?(%Foo{})
  false
  iex> Foo.is_valid?(%Foo{bar: "baz"})
  true
  """
  @spec is_valid?(any()) :: boolean()
  defguard is_valid?(term)
           when is_struct(term, __MODULE__) and is_map_key(term, :bar) and not is_nil(term.bar)
end

I think that the code and tests are valid, but the doctest generates this warning:

warning: undefined field "bar" in expression:

    # (for doctest at) lib/foo.ex:7
    arg0.bar

expected one of the following fields: 

where "arg0" was given the type map() in:

    # (for doctest at) lib/foo.ex:7
    {arg0} = {%{}}

Conflict found at
  (for doctest at) lib/foo.ex:7: FooTest."doctest Foo.is_valid?/1 (1)"/1

It will show the error for any type other than %Foo{}.

Expected behavior

No warnings when running the doctest.

Elixir (compiler) Discussion

Most helpful comment

I think the warnings is incorrect. The type warning should only be emitted for expressions that will fail at runtime - this is not the case, the expression discussed here will return false because of the earlier checks that the field exists before accessing it.

In other words, is_map_key(x, :foo) and x.foo should never produce a warning, regardless if x is inferred to have a :foo key or not - the expression will work correctly in both cases.

All 5 comments

This is a common warning in guards because guards are expanded and the compiler is smart enough to "see inside". For example, we can't really test is_map(true) because the compiler will warn since true is not a map.

One possible fix for now is for you to replace %{} by Map.new() although in the future the compiler may even be smart enough to see across Map.new. :)

Yeah I was suspecting something like that, but the test still looks valid to me. Is it maybe an option to not emit these warnings for (doc)tests?

For now I don't want to allow to skip warnings because that's a tricky slippery slope. First we start skip warnings for tests then folks will start skip warnings in actual valid code.

The good news though is that: every time you get a warning because of your example, the user will also get the same warning, which means they will know it will fail before they even invoke it.

I think the warnings is incorrect. The type warning should only be emitted for expressions that will fail at runtime - this is not the case, the expression discussed here will return false because of the earlier checks that the field exists before accessing it.

In other words, is_map_key(x, :foo) and x.foo should never produce a warning, regardless if x is inferred to have a :foo key or not - the expression will work correctly in both cases.

Ah, yes, good point about the conditionals.

Was this page helpful?
0 / 5 - 0 ratings