Code.require_file "../test_helper.exs", __DIR__

defmodule Kernel.WarningTest do
  use ExUnit.Case
  import ExUnit.CaptureIO

  defp capture_err(fun) do
    capture_io(:stderr, fun)
  end

  test :unused_variable do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def hello(arg), do: nil
      end
      """
    end) =~ "warning: variable arg is unused"
  after
    purge Sample
  end

  test :underscored_variable_on_match do
    assert capture_err(fn ->
      Code.eval_string """
      {_arg, _arg} = {1, 1}
      """
    end) =~ "warning: the underscored variable \"_arg\" appears more than once in a match"
  end

  test :unused_function do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample1 do
        defp hello, do: nil
      end
      """
    end) =~ "warning: function hello/0 is unused"

    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample2 do
        defp hello(0), do: hello(1)
        defp hello(1), do: :ok
      end
      """
    end) =~ "function hello/1 is unused"

    assert capture_err(fn ->
      Code.eval_string ~S"""
      defmodule Sample3 do
        def a, do: nil
        def b, do: d(10)
        defp c(x, y \\ 1), do: [x, y]
        defp d(x), do: x
      end
      """
    end) =~ "warning: function c/2 is unused"
  after
    purge [Sample1, Sample2, Sample3]
  end

  test :unused_cyclic_functions do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        defp a, do: b
        defp b, do: a
      end
      """
    end) =~ "warning: function a/0 is unused"
  after
    purge Sample
  end

  test :unused_macro do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        defmacrop hello, do: nil
      end
      """
    end) =~ "warning: macro hello/0 is unused"
  after
    purge Sample
  end

  test :shadowing do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def test(x) do
          case x do
            {:file, fid} -> fid
            {:path, _}   -> fn(fid) -> fid end
          end
        end
      end
      """
    end) == ""
  after
    purge Sample
  end

  test :unused_default_args do
    assert capture_err(fn ->
      Code.eval_string ~S"""
      defmodule Sample1 do
        def a, do: b(1, 2, 3)
        defp b(arg1 \\ 1, arg2 \\ 2, arg3 \\ 3), do: [arg1, arg2, arg3]
      end
      """
    end) =~ "warning: default arguments in b/3 are never used"

    assert capture_err(fn ->
      Code.eval_string ~S"""
      defmodule Sample2 do
        def a, do: b(1, 2)
        defp b(arg1 \\ 1, arg2 \\ 2, arg3 \\ 3), do: [arg1, arg2, arg3]
      end
      """
    end) =~ "warning: the first 2 default arguments in b/3 are never used"

    assert capture_err(fn ->
      Code.eval_string ~S"""
      defmodule Sample3 do
        def a, do: b(1)
        defp b(arg1 \\ 1, arg2 \\ 2, arg3 \\ 3), do: [arg1, arg2, arg3]
      end
      """
    end) =~ "warning: the first default argument in b/3 is never used"

    assert capture_err(fn ->
      Code.eval_string ~S"""
      defmodule Sample4 do
        def a, do: b(1)
        defp b(arg1 \\ 1, arg2, arg3 \\ 3), do: [arg1, arg2, arg3]
      end
      """
    end) == ""
  after
    purge [Sample1, Sample2, Sample3, Sample4]
  end

  test :unused_import do
    assert capture_err(fn ->
      Code.compile_string """
      defmodule Sample do
        import :lists, only: [flatten: 1]
        def a, do: nil
      end
      """
    end) =~ "warning: unused import :lists"

    assert capture_err(fn ->
      Code.compile_string """
      import :lists, only: [flatten: 1]
      """
    end) =~ "warning: unused import :lists"
  after
    purge Sample
  end

  test :unused_alias do
    assert capture_err(fn ->
      Code.compile_string """
      defmodule Sample do
        alias :lists, as: List
        def a, do: nil
      end
      """
    end) =~ "warning: unused alias List"
  after
    purge Sample
  end

  test :unused_inside_dynamic_module do
    import List, only: [flatten: 1], warn: false

    assert capture_err(fn ->
      defmodule Sample do
        import String, only: [downcase: 1]

        def world do
          flatten([1,2,3])
        end
      end
    end) =~ "warning: unused import String"
  after
    purge Sample
  end

  test :unused_guard do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample1 do
        def is_atom_case do
          v = "bc"
          case v do
            _ when is_atom(v) -> :ok
            _ -> :fail
          end
        end
      end
      """
    end) =~ "nofile:5: warning: this check/guard will always yield the same result"

    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample2 do
        def is_binary_cond do
          v = "bc"
          cond do
            is_binary(v) -> :bin
            true -> :ok
          end
        end
      end
      """
    end) =~ "nofile:6: warning: this clause cannot match because a previous clause at line 5 always matches"
  after
    purge [Sample1, Sample2]
  end

  test :empty_clause do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample1 do
        def hello
      end
      """
    end) =~ "warning: bodyless clause provided for nonexistent def hello/0"
  after
    purge Sample1
  end

  test :used_import_via_alias do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample1 do
        import List, only: [flatten: 1]

        defmacro generate do
          List.duplicate(quote(do: flatten([1,2,3])), 100)
        end
      end

      defmodule Sample2 do
        import Sample1
        generate
      end
      """
    end) == ""
  after
    purge [Sample1, Sample2]
  end

  test :clause_not_match do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def hello, do: nil
        def hello, do: nil
      end
      """
    end) =~ "warning: this clause cannot match because a previous clause at line 2 always matches"
  after
    purge Sample
  end

  test :clause_with_defaults_should_be_first do
    assert capture_err(fn ->
      Code.eval_string ~S"""
      defmodule Sample do
        def hello(arg), do: nil
        def hello(arg \\ 0), do: nil
      end
      """
    end) =~ "warning: clause with defaults should be the first clause in def hello/1"
  after
    purge Sample
  end

  test :unused_with_local_with_overridable do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def hello, do: world
        defp world, do: :ok
        defoverridable [hello: 0]
        def hello, do: :ok
      end
      """
    end) =~ "warning: function world/0 is unused"
  after
    purge Sample
  end

  test :used_with_local_with_reattached_overridable do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def hello, do: world
        defp world, do: :ok
        defoverridable [hello: 0, world: 0]
      end
      """
    end) == ""
  after
    purge Sample
  end

  test :undefined_module_attribute do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        @foo
      end
      """
    end) =~ "warning: undefined module attribute @foo, please remove access to @foo or explicitly set it to nil before access"
  after
    purge Sample
  end

  test :undefined_module_attribute_in_function do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def hello do
          @foo
        end
      end
      """
    end) =~ "warning: undefined module attribute @foo, please remove access to @foo or explicitly set it to nil before access"
  after
    purge Sample
  end

  test :undefined_module_attribute_with_file do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        @foo
      end
      """
    end) =~ "warning: undefined module attribute @foo, please remove access to @foo or explicitly set it to nil before access"
  after
    purge Sample
  end

  test :in_guard_empty_list do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def a(x) when x in [], do: x
      end
      """
    end) =~ "nofile:2: warning: this check/guard will always yield the same result"
  after
    purge Sample
  end

  test :no_effect_operator do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def a(x) do
          x != :foo
          :ok
        end
      end
      """
    end) =~ "warning: use of operator != has no effect"
  after
    purge Sample
  end

  test :badarg_warning do
    assert capture_err(fn ->
      assert_raise ArgumentError, fn ->
        Code.eval_string """
        defmodule Sample do
          Atom.to_string "abc"
        end
        """
      end
    end) =~ "warning: this expression will fail with ArgumentError"
  after
    purge [Sample]
  end

  test :undefined_function_for_behaviour do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample1 do
        use Behaviour
        defcallback foo
      end

      defmodule Sample2 do
        @behaviour Sample1
      end
      """
    end) =~ "warning: undefined behaviour function foo/0 (for behaviour Sample1)"
  after
    purge [Sample1, Sample2, Sample3]
  end

  test :undefined_macro_for_behaviour do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample1 do
        use Behaviour
        defmacrocallback foo
      end

      defmodule Sample2 do
        @behaviour Sample1
      end
      """
    end) =~ "warning: undefined behaviour macro foo/0 (for behaviour Sample1)"
  after
    purge [Sample1, Sample2, Sample3]
  end

  test :undefined_behavior do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        @behavior Hello
      end
      """
    end) =~ "warning: @behavior attribute is not supported, please use @behaviour instead"
  after
    purge Sample
  end

  test :undefined_macro_for_protocol do
    assert capture_err(fn ->
      Code.eval_string """
      defprotocol Sample1 do
        def foo(subject)
      end

      defimpl Sample1, for: Atom do
      end
      """
    end) =~ "warning: undefined protocol function foo/1 (for protocol Sample1)"
  after
    purge [Sample1, Sample1.Atom]
  end

  test :overidden_def do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        def foo(x, 1), do: x + 1
        def bar(), do: nil
        def foo(x, 2), do: x * 2
      end
      """
    end) =~ "nofile:4: warning: clauses for the same def should be grouped together, def foo/2 was previously defined (nofile:2)"
  after
    purge Sample
  end

  test :warning_with_overridden_file do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        @file "sample"
        def foo(x), do: :ok
      end
      """
    end) =~ "sample:3: warning: variable x is unused"
  after
    purge Sample
  end

  test :warning_on_codepoint_escape do
    assert capture_err(fn ->
      Code.eval_string "? "
    end) =~ "nofile:1: warning: found ? followed by codepoint 0x20 (space), please use \\s instead"
  end

  test :typedoc_on_typep do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        @typedoc "Something"
        @typep priv :: any
        @spec foo() :: priv
        def foo(), do: nil
      end
      """
    end) =~ "nofile:3: warning: type priv/0 is private, @typedoc's are always discarded for private types"
  after
    purge Sample
  end

  test :typedoc_with_no_type do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        @typedoc "Something"
      end
      """
    end) =~ "nofile:1: warning: @typedoc provided but no type follows it"
  after
    purge Sample
  end

  test :doc_with_no_function do
    assert capture_err(fn ->
      Code.eval_string """
      defmodule Sample do
        @doc "Something"
      end
      """
    end) =~ "nofile:1: warning: @doc provided but no definition follows it"
  after
    purge Sample
  end

  defp purge(list) when is_list(list) do
    Enum.each list, &purge/1
  end

  defp purge(module) when is_atom(module) do
    :code.delete module
    :code.purge module
  end
end
