# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec

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

defmodule Kernel.ErrorsTest do
  use ExUnit.Case, async: true

  defmacro hello do
    quote location: :keep do
      def hello, do: :world
    end
  end

  test "no default arguments in fn" do
    assert_compile_error(
      ["nofile:1:1", "anonymous functions cannot have optional arguments"],
      ~c"fn x \\\\ 1 -> x end"
    )

    assert_compile_error(
      ["nofile:1:1", "anonymous functions cannot have optional arguments"],
      ~c"fn x, y \\\\ 1 -> x + y end"
    )
  end

  test "invalid __CALLER__" do
    assert_compile_error(
      ["nofile:1:34", "__CALLER__ is available only inside defmacro and defmacrop"],
      ~c"defmodule Sample do def hello do __CALLER__ end end"
    )
  end

  test "invalid __STACKTRACE__" do
    assert_compile_error(
      [
        "nofile:1:34",
        "__STACKTRACE__ is available only inside catch and rescue clauses of try expressions"
      ],
      ~c"defmodule Sample do def hello do __STACKTRACE__ end end"
    )

    assert_compile_error(
      [
        "nofile:1:66",
        "__STACKTRACE__ is available only inside catch and rescue clauses of try expressions"
      ],
      ~c"defmodule Sample do try do raise \"oops\" rescue _ -> def hello do __STACKTRACE__ end end end"
    )
  end

  test "undefined function" do
    assert_compile_error(
      [
        "hello.ex:4:5: ",
        "undefined function bar/0 (expected Kernel.ErrorsTest.BadForm to define such a function or for it to be imported, but none are available)"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.BadForm do
        @file "hello.ex"
        def foo do
          bar()
        end
      end
      """
    )

    assert_compile_error(
      [
        "nofile:2:16",
        "undefined function module_info/0 (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.module_info/0)"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.Info do
        def foo, do: module_info()
      end
      """
    )

    assert_compile_error(
      [
        "nofile:3:16",
        "undefined function behaviour_info/1 (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.behaviour_info/1)"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.BehaviourInfo do
        @callback dummy() :: :ok
        def foo, do: behaviour_info(:callbacks)
      end
      """
    )

    assert_compile_error(
      [
        "nofile:3:5",
        "undefined function bar/1 (expected Kernel.ErrorsTest.BadForm to define such a function or for it to be imported, but none are available)"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.BadForm do
        def foo do
          bar(
            baz(1, 2)
          )
        end
      end
      """
    )

    assert_compile_error(
      [
        "nofile:8:5",
        "undefined function baz/0 (expected Sample to define such a function or for it to be imported, but none are available)"
      ],
      ~c"""
      defmodule Sample do
        def foo do
          bar()
        end

        defoverridable [foo: 0]
        def foo do
          baz()
        end
      end
      """
    )
  end

  test "undefined function within unused function" do
    assert_compile_error(
      ["nofile:2:8", "undefined function bar/0"],
      ~c"""
      defmodule Sample do
        defp foo, do: bar()
      end
      """
    )
  end

  test "undefined non-local function" do
    assert_compile_error(
      ["nofile:1:1", "undefined function call/2 (there is no such import)"],
      ~c"call foo, do: :foo"
    )
  end

  test "undefined variables" do
    assert_compile_error(
      ["nofile:3:13", "undefined variable \"bar\"", "nofile:4:13", "undefined variable \"baz\""],
      ~c"""
      defmodule Sample do
        def foo do
          IO.puts bar
          IO.puts baz
        end
      end
      """
    )
  end

  test "recursive variables on definition" do
    assert_compile_error(
      [
        "nofile:2:7: ",
        "recursive variable definition in patterns:",
        "foo(x = y, y = z, z = x)",
        "the following variables form a cycle: \"x\", \"y\", \"z\""
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.RecursiveVars do
        def foo(x = y, y = z, z = x), do: {x, y, z}
      end
      """
    )
  end

  test "function without definition" do
    assert_compile_error(
      ["nofile:2:7: ", "implementation not provided for predefined def foo/0"],
      ~c"""
      defmodule Kernel.ErrorsTest.FunctionWithoutDefinition do
        def foo
      end
      """
    )

    assert_compile_error(
      ["nofile:10: ", "implementation not provided for predefined def example/2"],
      ~c"""
      defmodule Kernel.ErrorsTest.FunctionTemplate do
        defmacro __using__(_) do
          quote do
            def example(foo, bar \\\\ [])
          end
        end
      end

      defmodule Kernel.ErrorsTest.UseFunctionTemplate do
        use Kernel.ErrorsTest.FunctionTemplate
      end
      """
    )
  end

  test "guard without definition" do
    assert_compile_error(
      ["nofile:2:12: ", "implementation not provided for predefined defmacro foo/1"],
      ~c"""
      defmodule Kernel.ErrorsTest.GuardWithoutDefinition do
        defguard foo(bar)
      end
      """
    )
  end

  test "literal on map and struct" do
    assert_compile_error(
      ["nofile:1:10", "expected key-value pairs in a map, got: put_in(foo.bar.baz, nil)"],
      ~c"foo = 1; %{put_in(foo.bar.baz, nil), foo}"
    )
  end

  test "struct fields on defstruct" do
    assert_eval_raise ArgumentError, ["struct field names must be atoms, got: 1"], ~c"""
    defmodule Kernel.ErrorsTest.StructFieldsOnDefstruct do
      defstruct [1, 2, 3]
    end
    """
  end

  test "struct access on body" do
    assert_compile_error(
      [
        "nofile:3:3",
        "cannot access struct Kernel.ErrorsTest.StructAccessOnBody, " <>
          "the struct was not yet defined or the struct " <>
          "is being accessed in the same context that defines it"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.StructAccessOnBody do
        defstruct %{name: "Brasilia"}
        %Kernel.ErrorsTest.StructAccessOnBody{}
      end
      """
    )
  end

  describe "struct errors" do
    test "bad errors" do
      assert_compile_error(
        ["nofile:1:1", "BadStruct.__struct__/1 is undefined, cannot expand struct BadStruct"],
        ~c"%BadStruct{}"
      )

      assert_compile_error(
        ["nofile:1:1", "BadStruct.__struct__/1 is undefined, cannot expand struct BadStruct"],
        ~c"%BadStruct{} = %{}"
      )

      bad_struct_type_error =
        ~r"expected Kernel.ErrorsTest.BadStructType.__struct__/(0|1) to return a map.*, got: :invalid"

      defmodule BadStructType do
        def __struct__, do: :invalid
        def __struct__(_), do: :invalid
      end

      assert_compile_error(bad_struct_type_error, ~c"%#{BadStructType}{}")

      assert_raise ArgumentError, bad_struct_type_error, fn ->
        struct(BadStructType)
      end

      assert_raise ArgumentError, bad_struct_type_error, fn ->
        struct(BadStructType, foo: 1)
      end
    end

    test "bad struct on module conflict" do
      Code.put_compiler_option(:ignore_module_conflict, true)

      assert_compile_error(~r'MissingStructOnReload\.__struct__/1 is undefined', ~c'''
      defmodule MissingStructOnReload do
        defstruct [:title]
        def d(), do: %MissingStructOnReload{}
      end

      defmodule MissingStructOnReload do
        def d(), do: %MissingStructOnReload{}
      end
      ''')
    after
      Code.put_compiler_option(:ignore_module_conflict, false)
    end

    test "missing struct key" do
      missing_struct_key_error =
        ~r"expected Kernel.ErrorsTest.MissingStructKey.__struct__/(0|1) to return a map.*, got: %\{\}"

      defmodule MissingStructKey do
        def __struct__, do: %{}
        def __struct__(_), do: %{}
      end

      assert_compile_error(missing_struct_key_error, ~c"%#{MissingStructKey}{}")

      assert_raise ArgumentError, missing_struct_key_error, fn ->
        struct(MissingStructKey)
      end

      assert_raise ArgumentError, missing_struct_key_error, fn ->
        struct(MissingStructKey, foo: 1)
      end

      invalid_struct_key_error =
        ~r"expected Kernel.ErrorsTest.InvalidStructKey.__struct__/(0|1) to return a map.*, got: %\{__struct__: 1\}"

      defmodule InvalidStructKey do
        def __struct__, do: %{__struct__: 1}
        def __struct__(_), do: %{__struct__: 1}
      end

      assert_compile_error(invalid_struct_key_error, ~c"%#{InvalidStructKey}{}")

      assert_raise ArgumentError, invalid_struct_key_error, fn ->
        struct(InvalidStructKey)
      end

      assert_raise ArgumentError, invalid_struct_key_error, fn ->
        struct(InvalidStructKey, foo: 1)
      end
    end

    test "invalid struct" do
      invalid_struct_name_error =
        ~r"expected struct name returned by Kernel.ErrorsTest.InvalidStructName.__struct__/(0|1) to be Kernel.ErrorsTest.InvalidStructName, got: InvalidName"

      defmodule InvalidStructName do
        def __struct__, do: %{__struct__: InvalidName}
        def __struct__(_), do: %{__struct__: InvalidName}
      end

      assert_compile_error(invalid_struct_name_error, ~c"%#{InvalidStructName}{}")

      assert_raise ArgumentError, invalid_struct_name_error, fn ->
        struct(InvalidStructName)
      end

      assert_raise ArgumentError, invalid_struct_name_error, fn ->
        struct(InvalidStructName, foo: 1)
      end
    end

    test "good struct" do
      defmodule GoodStruct do
        defstruct name: "john"
      end

      assert_eval_raise KeyError,
                        ["key :age not found"],
                        ~c"%#{GoodStruct}{age: 27}"

      assert_compile_error(
        ["nofile:1:1", "unknown key :age for struct Kernel.ErrorsTest.GoodStruct"],
        ~c"%#{GoodStruct}{age: 27} = %{}"
      )
    end

    test "enforce @enforce_keys" do
      defmodule EnforceKeys do
        @enforce_keys [:foo]
        defstruct(foo: nil)
      end

      assert_raise ArgumentError,
                   "@enforce_keys required keys ([:fo, :bar]) that are not defined in defstruct: [foo: nil]",
                   fn ->
                     defmodule EnforceKeysError do
                       @enforce_keys [:foo, :fo, :bar]
                       defstruct(foo: nil)
                     end
                   end
    end
  end

  test "invalid unquote" do
    assert_compile_error(["nofile:1:1", "unquote called outside quote"], ~c"unquote 1")
  end

  test "invalid unquote splicing in one-liners" do
    assert_eval_raise ArgumentError,
                      [
                        "unquote_splicing only works inside arguments and block contexts, " <>
                          "wrap it in parens if you want it to work with one-liners"
                      ],
                      ~c"""
                      defmodule Kernel.ErrorsTest.InvalidUnquoteSplicingInOneliners do
                        defmacro oneliner2 do
                          quote do: unquote_splicing 1
                        end

                        def callme do
                          oneliner2
                        end
                      end
                      """
  end

  test "invalid attribute" do
    msg = ~r"cannot inject attribute @foo into function/macro because cannot escape "

    assert_raise ArgumentError, msg, fn ->
      defmodule InvalidAttribute do
        @foo fn -> nil end
        def bar, do: @foo
      end
    end
  end

  test "typespec attributes set via Module.put_attribute/4" do
    message =
      "attributes type, typep, opaque, spec, callback, and macrocallback " <>
        "must be set directly via the @ notation"

    for kind <- [:type, :typep, :opaque, :spec, :callback, :macrocallback] do
      assert_eval_raise ArgumentError,
                        [message],
                        """
                        defmodule PutTypespecAttribute do
                          Module.put_attribute(__MODULE__, #{inspect(kind)}, {})
                        end
                        """
    end
  end

  test "invalid struct field value" do
    msg = ~r"invalid default value for struct field baz, cannot escape "

    assert_raise ArgumentError, msg, fn ->
      defmodule InvalidStructFieldValue do
        defstruct baz: fn -> nil end
      end
    end
  end

  test "invalid case clauses" do
    assert_compile_error(
      ["nofile:1:37", "expected one argument for \"do\" clauses (->) in \"case\""],
      ~c"case nil do 0, z when not is_nil(z) -> z end"
    )
  end

  test "invalid fn args" do
    exception =
      assert_eval_raise TokenMissingError,
                        [
                          "nofile:1:5:",
                          ~r/missing terminator: end/
                        ],
                        ~c"fn 1"

    assert exception.opening_delimiter == :fn
  end

  test "invalid escape" do
    assert_eval_raise TokenMissingError,
                      ["nofile:1:3:", "invalid escape \\ at end of file"],
                      ~c"1 \\"
  end

  test "show snippet on missing tokens" do
    assert_eval_raise TokenMissingError,
                      [
                        "nofile:1:25:",
                        "missing terminator: end",
                        "defmodule ShowSnippet do\n",
                        "└ unclosed delimiter"
                      ],
                      ~c"defmodule ShowSnippet do"
  end

  test "don't show snippet when error line is empty" do
    assert_eval_raise TokenMissingError,
                      ["nofile:1:25:", "missing terminator: end"],
                      ~c"defmodule ShowSnippet do\n\n"
  end

  test "function local conflict" do
    assert_compile_error(
      ["nofile:3:9: ", "imported Kernel.&&/2 conflicts with local function"],
      ~c"""
      defmodule Kernel.ErrorsTest.FunctionLocalConflict do
        def other, do: 1 && 2
        def _ && _, do: :error
      end
      """
    )
  end

  test "macro local conflict" do
    assert_compile_error(
      [
        "nofile:6:20",
        "call to local macro &&/2 conflicts with imported Kernel.&&/2, " <>
          "please rename the local macro or remove the conflicting import"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.MacroLocalConflict do
        def hello, do: 1 || 2
        defmacro _ || _, do: :ok

        defmacro _ && _, do: :error
        def world, do: 1 && 2
      end
      """
    )
  end

  test "macro with undefined local" do
    assert_eval_raise UndefinedFunctionError,
                      [
                        "function Kernel.ErrorsTest.MacroWithUndefinedLocal.unknown/1 is undefined (function not available)"
                      ],
                      ~c"""
                      defmodule Kernel.ErrorsTest.MacroWithUndefinedLocal do
                        defmacrop bar, do: unknown(1)
                        def baz, do: bar()
                      end
                      """
  end

  test "private macro" do
    assert_eval_raise UndefinedFunctionError,
                      [
                        "function Kernel.ErrorsTest.PrivateMacro.foo/0 is undefined (function not available)"
                      ],
                      ~c"""
                      defmodule Kernel.ErrorsTest.PrivateMacro do
                        defmacrop foo, do: 1
                        defmacro bar, do: __MODULE__.foo()
                        defmacro baz, do: bar()
                      end
                      """
  end

  test "macro invoked before its definition" do
    assert_compile_error(
      ["nofile:2:16", "cannot invoke macro bar/0 before its definition"],
      ~c"""
      defmodule Kernel.ErrorsTest.IncorrectMacroDispatch do
        def foo, do: bar()
        defmacro bar, do: :bar
      end
      """
    )

    assert_compile_error(
      ["nofile:2:16", "cannot invoke macro bar/0 before its definition"],
      ~c"""
      defmodule Kernel.ErrorsTest.IncorrectMacropDispatch do
        def foo, do: bar()
        defmacrop bar, do: :ok
      end
      """
    )

    assert_compile_error(
      ["nofile:2:40", "cannot invoke macro bar/1 before its definition"],
      ~c"""
      defmodule Kernel.ErrorsTest.IncorrectMacroDispatch do
        defmacro bar(a) when is_atom(a), do: bar([a])
      end
      """
    )
  end

  test "macro captured before its definition" do
    assert_compile_error(
      ["nofile:3:18", "cannot invoke macro is_ok/1 before its definition"],
      ~c"""
      defmodule Kernel.ErrorsTest.IncorrectMacroDispatch.Capture do
        def foo do
          predicate = &is_ok/1
          Enum.any?([:ok, :error, :foo], predicate)
        end

        defmacro is_ok(atom), do: atom == :ok
      end
      """
    )
  end

  test "function definition with alias" do
    assert_compile_error(
      [
        "nofile:2:7\n",
        "function names should start with lowercase characters or underscore, invalid name Bar"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.FunctionDefinitionWithAlias do
        def Bar do
          :baz
        end
      end
      """
    )
  end

  test "function import conflict" do
    assert_compile_error(
      ["nofile:3:16", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"],
      ~c"""
      defmodule Kernel.ErrorsTest.FunctionImportConflict do
        import :erlang, only: [exit: 1], warn: false
        def foo, do: exit(:test)
      end
      """
    )

    assert_compile_error(
      ["nofile:3:17", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"],
      ~c"""
      defmodule Kernel.ErrorsTest.FunctionImportConflict do
        import :erlang, only: [exit: 1], warn: false
        def foo, do: &exit/1
      end
      """
    )
  end

  test "ensure valid import :only option" do
    assert_compile_error(
      [
        "nofile:3:3",
        "invalid :only option for import, expected value to be an atom :functions, :macros, or a literal keyword list of function names with arity as values, got: x"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.Only do
        x = [flatten: 1]
        import List, only: x
      end
      """
    )
  end

  test "ensure valid import :except option" do
    assert_compile_error(
      [
        "nofile:3:3",
        "invalid :except option for import, expected value to be a literal keyword list of function names " <>
          "with arity as values, got: Module.__get_attribute__(Kernel.ErrorsTest.Only, :x, 3, true)"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.Only do
        @x [flatten: 1]
        import List, except: @x
      end
      """
    )
  end

  test "def defmacro clause change" do
    assert_compile_error(
      ["nofile:3:12\n", "defmacro foo/1 already defined as def in nofile:2"],
      ~c"""
      defmodule Kernel.ErrorsTest.DefDefmacroClauseChange do
        def foo(1), do: 1
        defmacro foo(x), do: x
      end
      """
    )
  end

  test "def defp clause change from another file" do
    assert_compile_error(["nofile:4\n", "def hello/0 already defined as defp"], ~c"""
    defmodule Kernel.ErrorsTest.DefDefmacroClauseChange do
      require Kernel.ErrorsTest
      defp hello, do: :world
      Kernel.ErrorsTest.hello()
    end
    """)
  end

  test "internal function overridden" do
    assert_compile_error(
      ["nofile:2:7\n", "cannot define def __info__/1 as it is automatically defined by Elixir"],
      ~c"""
      defmodule Kernel.ErrorsTest.InternalFunctionOverridden do
        def __info__(_), do: []
      end
      """
    )
  end

  test "invalid macro" do
    assert_compile_error(
      "invalid quoted expression: {:foo, :bar, :baz, :bat}",
      ~c"""
      defmodule Kernel.ErrorsTest.InvalidMacro do
        defmacrop oops do
          {:foo, :bar, :baz, :bat}
        end

        def test, do: oops()
      end
      """
    )
  end

  test "unloaded module" do
    assert_compile_error(
      ["nofile:1:1", "module Certainly.Doesnt.Exist is not loaded and could not be found"],
      ~c"import Certainly.Doesnt.Exist"
    )
  end

  test "module imported from the context it was defined in" do
    assert_compile_error(
      [
        "nofile:4:3",
        "module Kernel.ErrorsTest.ScheduledModule.Hygiene is not loaded but was defined."
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.ScheduledModule do
        defmodule Hygiene do
        end
        import Kernel.ErrorsTest.ScheduledModule.Hygiene
      end
      """
    )
  end

  test "module imported from the same module" do
    assert_compile_error(
      [
        "nofile:3:5",
        "you are trying to use/import/require the module Kernel.ErrorsTest.ScheduledModule.Hygiene which is currently being defined"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.ScheduledModule do
        defmodule Hygiene do
          import Kernel.ErrorsTest.ScheduledModule.Hygiene
        end
      end
      """
    )
  end

  test "invalid @compile inline" do
    assert_compile_error(
      ["nofile:1: ", "undefined function foo/1 given to @compile :inline"],
      ~c"defmodule Test do @compile {:inline, foo: 1} end"
    )

    assert_compile_error(
      ["nofile:1: ", "macro foo/1 given to @compile :inline"],
      ~c"defmodule Test do @compile {:inline, foo: 1}; defmacro foo(_), do: :ok end"
    )
  end

  test "invalid @nifs attribute" do
    assert_compile_error(
      ["nofile:1: ", "undefined function foo/1 given to @nifs"],
      ~c"defmodule Test do @nifs [foo: 1] end"
    )

    assert_compile_error(
      ["nofile:1: ", "undefined function foo/1 given to @nifs"],
      ~c"defmodule Test do @nifs [foo: 1]; defmacro foo(_) end"
    )

    assert_eval_raise ArgumentError,
                      ["@nifs is a built-in module attribute"],
                      ~c"defmodule Test do @nifs :not_an_option end"
  end

  test "invalid @dialyzer options" do
    assert_compile_error(
      ["nofile:1: ", "undefined function foo/1 given to @dialyzer :nowarn_function"],
      ~c"defmodule Test do @dialyzer {:nowarn_function, {:foo, 1}} end"
    )

    assert_compile_error(
      ["nofile:1: ", "macro foo/1 given to @dialyzer :nowarn_function"],
      ~c"defmodule Test do @dialyzer {:nowarn_function, {:foo, 1}}; defmacro foo(_), do: :ok end"
    )

    assert_compile_error(
      ["nofile:1: ", "undefined function foo/1 given to @dialyzer :no_opaque"],
      ~c"defmodule Test do @dialyzer {:no_opaque, {:foo, 1}} end"
    )

    assert_eval_raise ArgumentError,
                      ["invalid value for @dialyzer attribute: :not_an_option"],
                      ~c"defmodule Test do @dialyzer :not_an_option end"
  end

  test "@on_load attribute format" do
    assert_raise ArgumentError, ~r/should be an atom or an {atom, 0} tuple/, fn ->
      defmodule BadOnLoadAttribute do
        Module.put_attribute(__MODULE__, :on_load, "not an atom")
      end
    end
  end

  test "duplicated @on_load attribute" do
    assert_raise ArgumentError, "the @on_load attribute can only be set once per module", fn ->
      defmodule DuplicatedOnLoadAttribute do
        @on_load :foo
        @on_load :bar
      end
    end
  end

  test "@on_load attribute with undefined function" do
    assert_compile_error(
      ["nofile:1: ", "undefined function foo/0 given to @on_load"],
      ~c"defmodule UndefinedOnLoadFunction do @on_load :foo end"
    )
  end

  test "wrong kind for @on_load attribute" do
    assert_compile_error(
      ["nofile:1: ", "macro foo/0 given to @on_load"],
      ~c"""
      defmodule PrivateOnLoadFunction do
        @on_load :foo
        defmacro foo, do: :ok
      end
      """
    )
  end

  test "in definition module" do
    assert_compile_error(
      [
        "nofile:2: ",
        "cannot define module Kernel.ErrorsTest.InDefinitionModule " <>
          "because it is currently being defined in nofile:1"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.InDefinitionModule do
        defmodule Elixir.Kernel.ErrorsTest.InDefinitionModule, do: true
      end
      """
    )
  end

  test "invalid definition" do
    assert_compile_error(
      ["nofile:1: ", "invalid syntax in def 1.(hello)"],
      ~c"defmodule Kernel.ErrorsTest.InvalidDefinition, do: (def 1.(hello), do: true)"
    )
  end

  test "function head with guard" do
    assert_compile_error(["nofile:2:7: ", "missing :do option in \"def\""], ~c"""
    defmodule Kernel.ErrorsTest.BodyessFunctionWithGuard do
      def foo(n) when is_number(n)
    end
    """)

    assert_compile_error(["nofile:2:7: ", "missing :do option in \"def\""], ~c"""
    defmodule Kernel.ErrorsTest.BodyessFunctionWithGuard do
      def foo(n) when is_number(n), true
    end
    """)
  end

  test "invalid args for function head" do
    assert_compile_error(
      [
        "nofile:2:7: ",
        "patterns are not allowed in function head, only variables and default arguments (using \\\\)"
      ],
      ~c"""
      defmodule Kernel.ErrorsTest.InvalidArgsForBodylessClause do
        def foo(nil)
        def foo(_), do: :ok
      end
      """
    )
  end

  test "bad multi-call" do
    assert_compile_error(
      [
        "nofile:1:1",
        "invalid argument for alias, expected a compile time atom or alias, got: 42"
      ],
      ~c"alias IO.{ANSI, 42}"
    )

    assert_compile_error(
      ["nofile:1:1", ":as option is not supported by multi-alias call"],
      ~c"alias Elixir.{Map}, as: Dict"
    )

    assert_eval_raise UndefinedFunctionError,
                      ["function List.\"{}\"/1 is undefined or private"],
                      ~c"[List.{Chars}, \"one\"]"
  end

  test "macros error stacktrace" do
    assert [
             {:erlang, :+, [1, :foo], _},
             {Kernel.ErrorsTest.MacrosErrorStacktrace, :sample, 1, _} | _
           ] =
             rescue_stacktrace("""
             defmodule Kernel.ErrorsTest.MacrosErrorStacktrace do
               defmacro sample(num), do: num + :foo
               def other, do: sample(1)
             end
             """)
  end

  test "macros function clause stacktrace" do
    assert [{__MODULE__, :sample, 1, _} | _] =
             rescue_stacktrace("""
             defmodule Kernel.ErrorsTest.MacrosFunctionClauseStacktrace do
               import Kernel.ErrorsTest
               sample(1)
             end
             """)
  end

  test "macros interpreted function clause stacktrace" do
    assert [{Kernel.ErrorsTest.MacrosInterpretedFunctionClauseStacktrace, :sample, 1, _} | _] =
             rescue_stacktrace("""
             defmodule Kernel.ErrorsTest.MacrosInterpretedFunctionClauseStacktrace do
               defmacro sample(0), do: 0
               def other, do: sample(1)
             end
             """)
  end

  test "macros compiled callback" do
    assert [{Kernel.ErrorsTest, :__before_compile__, [env], _} | _] =
             rescue_stacktrace("""
             defmodule Kernel.ErrorsTest.MacrosCompiledCallback do
               Module.put_attribute(__MODULE__, :before_compile, Kernel.ErrorsTest)
             end
             """)

    assert %Macro.Env{module: Kernel.ErrorsTest.MacrosCompiledCallback} = env
  end

  test "failed remote call stacktrace includes file/line info" do
    try do
      bad_remote_call(Process.get(:unused, 1))
    rescue
      ArgumentError ->
        assert [
                 {:erlang, :apply, [1, :foo, []], _},
                 {__MODULE__, :bad_remote_call, 1, [file: _, line: _]} | _
               ] = __STACKTRACE__
    end
  end

  test "def fails when rescue, else or catch don't have clauses" do
    assert_compile_error(
      ~r"invalid \"rescue\" block in \"def\", it expects \"pattern -> expr\" clauses",
      """
      defmodule Example do
        def foo do
          bar()
        rescue
          baz()
        end
      end
      """
    )
  end

  test "duplicate map keys" do
    assert_compile_error(["nofile:1:3", "key :a will be overridden in map"], """
      %{a: :b, a: :c} = %{a: :c}
    """)

    assert_compile_error(["nofile:1:3", "key :a will be overridden in map"], """
      %{a: :b, a: :c, a: :d} = %{a: :c}
    """)
  end

  test "| outside of cons" do
    assert_compile_error(["nofile:1:3", "misplaced operator |/2"], "1 | 2")

    assert_compile_error(
      ["nofile:1:45", "misplaced operator |/2"],
      "defmodule MisplacedOperator, do: (def bar(1 | 2), do: :ok)"
    )
  end

  defp bad_remote_call(x), do: x.foo()

  defmacro sample(0), do: 0

  defmacro before_compile(_) do
    quote(do: _)
  end

  ## Helpers

  defp assert_eval_raise(given_exception, messages, source) do
    exception =
      assert_raise given_exception, fn ->
        Code.eval_string(source)
      end

    error_msg = Exception.format(:error, exception, [])

    for msg <- messages do
      assert error_msg =~ msg
    end

    exception
  end

  defp assert_compile_error(messages, string) do
    captured =
      ExUnit.CaptureIO.capture_io(:stderr, fn ->
        ast = Code.string_to_quoted!(string, columns: true)
        assert_raise CompileError, fn -> Code.eval_quoted(ast) end
      end)

    for message <- List.wrap(messages) do
      assert captured =~ message
    end
  end

  defp rescue_stacktrace(string) do
    try do
      Code.eval_string(string)
      nil
    rescue
      _ -> __STACKTRACE__
    else
      _ -> flunk("Expected expression to fail")
    end
  end
end
