elixir-lang / elixir

Elixir is a dynamic, functional language for building scalable and maintainable applications
https://elixir-lang.org/
Apache License 2.0
24.55k stars 3.38k forks source link

Dialyzer warning on `ExUnit.Assertions.assert/1` #11869

Closed sega-yarkin closed 2 years ago

sega-yarkin commented 2 years ago

Environment

Current behavior

ExUnit.Assertions.assert/1 produces Dialyzer warning when Dialyzer can figure out a type of right or left-hand side expressions, and it cannot be nil or false.

It looks like the reason is a check for falsy value here. Not sure if there is an easy way to do something with it, maybe the warning can be suppressed in ExUnit, so users don't have to deal with it.

Example

defmodule AssertBugTest do
  import ExUnit.Assertions

  # Known type
  def something do
    if Process.alive?(self()), do: :ok, else: "nope"
  end

  def test do
    assert ok = something()
    ok
  end

  # Unknown type
  @spec something2() :: any()
  def something2, do: UnknownModule.something()

  def test2 do
    assert :ok = something2()
  end
end

Dialyzer's warnings:

________________________________________________________________________________
lib/assert_bug_test.ex:10:guard_fail
The guard clause:

when _ :: :ok | <<_::32>> === false

can never succeed.
________________________________________________________________________________
lib/assert_bug_test.ex:19:guard_fail
The guard clause:

when _ :: :ok === false

can never succeed.

Generated Erlang code (truncated):

something() ->
    case erlang:is_process_alive(erlang:self()) of
        false -> <<"nope">>;
        true -> ok
    end.

something2() -> 'Elixir.UnknownModule':something().

test() ->
    _@1 = something(),
    _@2 = ...,
    [_ok@2] = case _@1 of
                  _ok@1 ->
                      case _@1 of
                          %% Dialyzer can figure out that _@1 cannot be nil or false
                          _@3 when _@3 =:= nil orelse _@3 =:= false ->
                              erlang:error('Elixir.ExUnit.AssertionError':exception(...)); % Expected truthy, got 
                          _ -> ok
                      end,
                      [_ok@1];
                  _ ->
                      _@4 = {ok, [{line, 10}], nil},
                      erlang:error('Elixir.ExUnit.AssertionError':exception(...)) % match (=) failed
              end,
    _@1,
    _ok@2.

test2() ->
    _@1 = something2(),
    _@2 = ...,
    [] = case _@1 of
             ok ->
                 case _@1 of
                     %% Dialyzer can figure out that _@1 cannot be nil or false (as it already matched `ok`?)
                     _@3 when _@3 =:= nil orelse _@3 =:= false ->
                         erlang:error('Elixir.ExUnit.AssertionError':exception(...)); % Expected truthy, got 
                     _ -> ok
                 end,
                 [];
             _ ->
                 _@4 = ok,
                 erlang:error('Elixir.ExUnit.AssertionError':exception(...)) % match (=) failed
         end,
    _@1.

Expected behavior

No warnings

Ljzn commented 2 years ago

It looks like the reason is a check for falsy value here.

The generated: true in meta seems can not ignore these warnings. I tried changing the code with the pash2 trick, which avoid the dialyzer guard_fail warning.

      suppress_warning(
        quote do
          case (case :erlang.phash2(1, 1) do
                  0 -> right
                  1 -> nil
                end) do
            x when x in [nil, false] ->
              raise ExUnit.AssertionError,
                expr: expr,
                message: "Expected truthy, got #{inspect(right)}"

            _ ->
              :ok
          end
        end
      )