Improve test for no import options in DocTest tests (#6094)

It was really confusing that a min function is return a max value.
Additionally it adds a test that defines a function a colliding name in Kernel,
that is that not conflict because it is not being imported.

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

import ExUnit.TestHelpers

defmodule ExUnit.DocTestTest.GoodModule do
  @doc """
  iex> test_fun()
  1
  iex> test_fun() + 1
  2
  """
  def test_fun, do: 1

  @doc ~S"""
  iex> ~S(f#{o}o)
  "f\#{o}o"
  """
  def test_sigil, do: :ok

  @doc """
  iex> a = 1
  iex> b = a + 2
  3
  iex> a + b
  4
  """
  def single_context, do: :ok

  @doc """
  iex> 1 + (fn() -> "" end).()
  ** (ArithmeticError) bad argument in arithmetic expression

  iex> 2 + (fn() -> :a end).()
  ** (ArithmeticError) bad argument in arithmetic expression
  """
  def two_exceptions, do: :ok

  @doc """
  iex> 1 + (fn() -> :a end).()
  ** (ArithmeticError) bad argument in arithmetic expression
  """
  def exception_test, do: :ok

  @doc ~S"""
  iex> raise "foo\nbar"
  ** (RuntimeError) foo
  bar
  """
  def multiline_exception_test, do: :ok

  @doc """
  iex> Enum.into([:a, :b, :c], MapSet.new)
  #MapSet<[:a, :b, :c]>
  """
  def inspect1_test, do: :ok

  @doc """
  iex> x = Enum.into([:a, :b, :c], MapSet.new)
  ...> x
  #MapSet<[:a, :b, :c]>
  """
  def inspect2_test, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.MultipleExceptions do
  @doc """
  iex> 1 + ""
  ** (ArithmeticError) bad argument in arithmetic expression
  iex> 2 + ""
  ** (ArithmeticError) bad argument in arithmetic expression
  """
  def two_exceptions, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.SomewhatGoodModuleWithOnly do
  @doc """
  iex> test_fun1()
  1
  iex> test_fun1() + 1
  2
  """
  def test_fun1, do: 1

  @doc """
  iex> test_fun2()
  1
  iex> test_fun2() + 1
  1
  """
  def test_fun2, do: 1
end |> write_beam

defmodule ExUnit.DocTestTest.SomewhatGoodModuleWithExcept do
  @moduledoc """
  iex> 1 + 1
  1
  """

  @doc """
  iex> test_fun1()
  1
  iex> test_fun1() + 1
  2
  """
  def test_fun1, do: 1

  @doc """
  iex> test_fun2()
  1
  iex> test_fun2() + 1
  1
  """
  def test_fun2, do: 1
end |> write_beam

defmodule ExUnit.DocTestTest.NoImport do
  @doc """
  iex> ExUnit.DocTestTest.NoImport.max(1, 2)
  {:ok, 2}

  iex> max(1, 2)
  2
  """
  def max(a, b), do: {:ok, Kernel.max(a, b)}
end |> write_beam

defmodule ExUnit.DocTestTest.Invalid do
  @moduledoc """

      iex> 1 + * 1
      1

      iex> 1 + hd(List.flatten([1]))
      3

      iex> :oops
      #MapSet<[]>

      iex> Hello.world
      :world

      iex> raise "oops"
      ** (WhatIsThis) oops

      iex> raise "oops"
      ** (RuntimeError) hello

  """

  @doc """
      iex> 1 + * 1
      1
  """
  def a(), do: :ok

  @doc """
      iex> 1 + * 1
      1
  """
  defmacro b(), do: :ok

  @doc """
    ```
    iex> 1 + 2
    3
  ```
  """
  def indented_not_enough, do: :ok

  @doc ~S'''
  ```
  iex> 1 + 2
  3
    ```
  '''
  def indented_too_much, do: :ok

  @doc """
      ```
  iex> 1 + 2
  3
      ```
  """
  def dedented_past_fence, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationHeredocs do
  @doc ~S'''
  Receives a test and formats its failure.

  ## Examples

      iex> "  1\n  2\n"
      """
        1
        2
      """

  '''
  def heredocs, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationMismatchedPrompt do
  @doc ~S'''
    iex> foo = 1
     iex> bar = 2
    iex> foo + bar
    3
  '''
  def mismatched, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationTooMuch do
  @doc ~S'''
    iex> 1 + 2
      3
  '''
  def too_much, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationNotEnough do
  @doc ~S'''
      iex> 1 + 2
    3
  '''
  def test_fun, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.FencedHeredocs do
  @doc ~S'''
  Receives a test and formats its failure.

  ## Examples

  ```
  iex> 1 + 2
  3
  ```

      ```
      iex> 1 + 2
      3
      ```

  ```
      iex> 1 + 2
      3
  ```
  '''
  def heredocs, do: :ok

  @doc ~S'''
  ```
  iex> 1 + 2
  3
  '''
  def incomplete, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.Incomplete do
  @doc ~S'''
      iex> 1 + 2

  '''
  def test_fun, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.FenceIncomplete do
  @doc ~S'''
  ```
  iex> 1 + 2
  3
  '''
  def test_fun, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.Numbered do
  @doc """
  iex(1)> 1 +
  ...(1)> 2
  3
  """
  def test_fun(), do: :ok
end |> write_beam()

defmodule ExUnit.DocTestTest.Host do
  @doc """
      iex(foo@bar)1> 1 +
      ...(foo@bar)1> 2
      3
  """
  def test_fun(), do: :ok
end |> write_beam()

defmodule ExUnit.DocTestTest.Haiku do
  @moduledoc """
  This module describes the ancient Japanese poem form known as Haiku.

  The Inspect protocol has been overriden for `%Haiku{}`
  so that Haikus are shown in a pretty-printed fashion.

  This module is part of the DocTest test suite,
  to ensure that DocTest can handle opaque inspect types
  which contain unicode and possibly consist of multiple lines.
  """

  defstruct [:first_phrase, :second_phrase, :third_phrase, :author]

  @doc """
  Creates a new Haiku.
  Optionally pass in the `author` as fourth argument.

  ## Examples:

      # Simple Haiku, inspect output consists of multiple lines.
      iex> ExUnit.DocTestTest.Haiku.new("Haikus are easy", "But sometimes they don't make sense", "Refrigerator")
      #Haiku<
        Haikus are easy
        But sometimes they don't make sense
        Refrigerator
      >

      # Haiku with Unicode characters (Japanese Kanji, em-dash).
      iex> ExUnit.DocTestTest.Haiku.new("古池や", "蛙飛びこむ", "水の音", "Matsuo Basho")
      #Haiku<
        古池や
        蛙飛びこむ
        水の音
        ― Matsuo Basho
      >

  """
  def new(first, second, third, author \\ "")
      when is_binary(first) and is_binary(second) and is_binary(third) and is_binary(author) do
    %__MODULE__{
      first_phrase: first,
      second_phrase: second,
      third_phrase: third,
      author: author
    }
  end

  defimpl Inspect do
    def inspect(haiku, _opts) do
      author = if haiku.author == "", do: "", else: "\n  ― #{haiku.author}"
      """
      #Haiku<
        #{haiku.first_phrase}
        #{haiku.second_phrase}
        #{haiku.third_phrase}#{author}
      >
      """
      |> String.trim_trailing("\n")
    end
  end
end |> write_beam

defmodule ExUnit.DocTestTest do
  use ExUnit.Case

  # This is intentional. The doctests in DocTest's docs
  # fail for demonstration purposes.
  # doctest ExUnit.DocTest

  doctest ExUnit.DocTestTest.GoodModule, import: true
  doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [test_fun1: 0], import: true
  doctest ExUnit.DocTestTest.SomewhatGoodModuleWithExcept, except: [:moduledoc, test_fun2: 0], import: true
  doctest ExUnit.DocTestTest.NoImport
  doctest ExUnit.DocTestTest.IndentationHeredocs
  doctest ExUnit.DocTestTest.FencedHeredocs
  doctest ExUnit.DocTestTest.Haiku

  import ExUnit.CaptureIO

  test "multiple functions filtered with :only" do
    defmodule MultipleOnly do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [test_fun1: 0, test_fun2: 0], import: true
    end

    ExUnit.Server.cases_loaded()
    assert capture_io(fn -> ExUnit.run end) =~ "2 tests, 1 failure"
  end

  test "doctest failures" do
    # When adding or removing lines above this line, the tests below will
    # fail because we are explicitly asserting some doctest lines from
    # ActuallyCompiled in the format of "test/ex_unit/doc_test_test.exs:<LINE>".
    defmodule ActuallyCompiled do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.Invalid
    end

    ExUnit.configure(seed: 0, colors: [enabled: false])
    ExUnit.Server.cases_loaded()
    output = capture_io(fn -> ExUnit.run end)

    # Test order is not guaranteed, we can't match this as a string for each failing doctest
    assert output =~ ~r/\d+\) test moduledoc at ExUnit\.DocTestTest\.Invalid \(\d+\) \(ExUnit\.DocTestTest\.ActuallyCompiled\)/

    assert output =~ """
      1) test moduledoc at ExUnit.DocTestTest.Invalid (1) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:130: syntax error before: '*'
         code: 1 + * 1
         stacktrace:
           test/ex_unit/doc_test_test.exs:130: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      2) test moduledoc at ExUnit.DocTestTest.Invalid (2) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest failed
         code: 1 + hd(List.flatten([1])) === 3
         left: 2
         stacktrace:
           test/ex_unit/doc_test_test.exs:133: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      3) test moduledoc at ExUnit.DocTestTest.Invalid (3) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest failed
         code: inspect(:oops) === "#MapSet<[]>"
         left: ":oops"
         stacktrace:
           test/ex_unit/doc_test_test.exs:136: ExUnit.DocTestTest.Invalid (module)
    """

    # The stacktrace points to the cause of the error
    assert output =~ """
      4) test moduledoc at ExUnit.DocTestTest.Invalid (4) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest failed: got UndefinedFunctionError with message "function Hello.world/0 is undefined (module Hello is not available)"
         code: Hello.world
         stacktrace:
           Hello.world()
           (for doctest at) test/ex_unit/doc_test_test.exs:139: (test)
    """

    assert output =~ """
      5) test moduledoc at ExUnit.DocTestTest.Invalid (5) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest failed: expected exception WhatIsThis but got RuntimeError with message "oops"
         code: raise "oops"
         stacktrace:
           test/ex_unit/doc_test_test.exs:142: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      6) test moduledoc at ExUnit.DocTestTest.Invalid (6) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest failed: wrong message for RuntimeError
         expected:
           "hello"
         actual:
           "oops"
         code: raise "oops"
         stacktrace:
           test/ex_unit/doc_test_test.exs:145: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      7) test doc at ExUnit.DocTestTest.Invalid.a/0 (7) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:151: syntax error before: '*'
         code: 1 + * 1
         stacktrace:
           test/ex_unit/doc_test_test.exs:151: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      8) test doc at ExUnit.DocTestTest.Invalid.b/0 (8) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:157: syntax error before: '*'
         code: 1 + * 1
         stacktrace:
           test/ex_unit/doc_test_test.exs:157: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      9) test doc at ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (9) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:181: unexpected token: "`" (column 5, codepoint U+0060)
         code: 3
                   ```
         stacktrace:
           test/ex_unit/doc_test_test.exs:180: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
     10) test doc at ExUnit.DocTestTest.Invalid.indented_not_enough/0 (10) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:165: unexpected token: "`" (column 1, codepoint U+0060)
         code: 3
               `
         stacktrace:
           test/ex_unit/doc_test_test.exs:164: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
     11) test doc at ExUnit.DocTestTest.Invalid.indented_too_much/0 (11) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:391
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:173: unexpected token: "`" (column 3, codepoint U+0060)
         code: 3
                 ```
         stacktrace:
           test/ex_unit/doc_test_test.exs:172: ExUnit.DocTestTest.Invalid (module)
    """
  end

  test "IEx prefix contains a number" do
    defmodule NumberedUsage do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.Numbered
    end

    ExUnit.Server.cases_loaded()
    assert capture_io(fn -> ExUnit.run end) =~ "1 test, 0 failures"
  end

  test "IEx prompt contains host" do
    message =
      ~s[unknown IEx prompt: "iex(foo@bar)1> 1 +".\nAccepted formats are: iex>, iex(1)>, ...>, ...(1)>]

    regex = ~r[test/ex_unit/doc_test_test\.exs:\d+: #{Regex.escape(message)}]

    assert_raise ExUnit.DocTest.Error, regex, fn ->
      defmodule HostUsage do
        use ExUnit.Case
        doctest ExUnit.DocTestTest.Host
      end
    end
  end

  test "tags tests as doctests" do
    defmodule DoctestTag do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.NoImport

      setup test do
        assert test.doctest
        :ok
      end
    end

    ExUnit.Server.cases_loaded()
    assert capture_io(fn -> ExUnit.run end) =~ "2 tests, 0 failures"
  end

  test "multiple exceptions in one test case is not supported" do
    assert_raise ExUnit.DocTest.Error, ~r"multiple exceptions in one doctest case are not supported", fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.MultipleExceptions
      end
    end
  end

  test "fails on invalid module" do
    assert_raise CompileError, ~r"module ExUnit\.DocTestTest\.Unknown is not loaded and could not be found", fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.Unknown
      end
    end
  end

  test "fails when there are no docs" do
    assert_raise ExUnit.DocTest.Error, ~r"could not retrieve the documentation for module ExUnit\.DocTestTest", fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest
      end
    end
  end

  test "fails in indentation mismatch" do
    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch: "   iex> bar = 2", should have been 2 spaces], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.IndentationMismatchedPrompt
      end
    end

    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch: "    3", should have been 2 spaces], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.IndentationTooMuch
      end
    end

    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch: \"  3\", should have been 4 spaces], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.IndentationNotEnough
      end
    end
  end

  test "fails with improper termination" do
    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: expected non-blank line to follow iex> prompt], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.Incomplete
      end
    end
  end

  test "fails on invalid use" do
    assert_raise RuntimeError, ~r"cannot define test", fn ->
      defmodule FunctionClashFail do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.Invalid
      end
    end
  end
end

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

import ExUnit.TestHelpers

defmodule ExUnit.DocTestTest.GoodModule do
  @doc """
  iex> test_fun()
  1
  iex> test_fun() + 1
  2
  """
  def test_fun, do: 1

  @doc ~S"""
  iex> ~S(f#{o}o)
  "f\#{o}o"
  """
  def test_sigil, do: :ok

  @doc """
  iex> a = 1
  iex> b = a + 2
  3
  iex> a + b
  4
  """
  def single_context, do: :ok

  @doc """
  iex> 1 + (fn() -> "" end).()
  ** (ArithmeticError) bad argument in arithmetic expression

  iex> 2 + (fn() -> :a end).()
  ** (ArithmeticError) bad argument in arithmetic expression
  """
  def two_exceptions, do: :ok

  @doc """
  iex> 1 + (fn() -> :a end).()
  ** (ArithmeticError) bad argument in arithmetic expression
  """
  def exception_test, do: :ok

  @doc ~S"""
  iex> raise "foo\nbar"
  ** (RuntimeError) foo
  bar
  """
  def multiline_exception_test, do: :ok

  @doc """
  iex> Enum.into([:a, :b, :c], MapSet.new)
  #MapSet<[:a, :b, :c]>
  """
  def inspect1_test, do: :ok

  @doc """
  iex> x = Enum.into([:a, :b, :c], MapSet.new)
  ...> x
  #MapSet<[:a, :b, :c]>
  """
  def inspect2_test, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.MultipleExceptions do
  @doc """
  iex> 1 + ""
  ** (ArithmeticError) bad argument in arithmetic expression
  iex> 2 + ""
  ** (ArithmeticError) bad argument in arithmetic expression
  """
  def two_exceptions, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.SomewhatGoodModuleWithOnly do
  @doc """
  iex> test_fun1()
  1
  iex> test_fun1() + 1
  2
  """
  def test_fun1, do: 1

  @doc """
  iex> test_fun2()
  1
  iex> test_fun2() + 1
  1
  """
  def test_fun2, do: 1
end |> write_beam

defmodule ExUnit.DocTestTest.SomewhatGoodModuleWithExcept do
  @moduledoc """
  iex> 1 + 1
  1
  """

  @doc """
  iex> test_fun1()
  1
  iex> test_fun1() + 1
  2
  """
  def test_fun1, do: 1

  @doc """
  iex> test_fun2()
  1
  iex> test_fun2() + 1
  1
  """
  def test_fun2, do: 1
end |> write_beam

defmodule ExUnit.DocTestTest.NoImport do
  @doc """
  iex> ExUnit.DocTestTest.NoImport.min(1, 2)
  2
  """
  def min(a, b), do: max(a, b)
end |> write_beam

defmodule ExUnit.DocTestTest.Invalid do
  @moduledoc """

      iex> 1 + * 1
      1

      iex> 1 + hd(List.flatten([1]))
      3

      iex> :oops
      #MapSet<[]>

      iex> Hello.world
      :world

      iex> raise "oops"
      ** (WhatIsThis) oops

      iex> raise "oops"
      ** (RuntimeError) hello

  """

  @doc """
      iex> 1 + * 1
      1
  """
  def a(), do: :ok

  @doc """
      iex> 1 + * 1
      1
  """
  defmacro b(), do: :ok

  @doc """
    ```
    iex> 1 + 2
    3
  ```
  """
  def indented_not_enough, do: :ok

  @doc ~S'''
  ```
  iex> 1 + 2
  3
    ```
  '''
  def indented_too_much, do: :ok

  @doc """
      ```
  iex> 1 + 2
  3
      ```
  """
  def dedented_past_fence, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationHeredocs do
  @doc ~S'''
  Receives a test and formats its failure.

  ## Examples

      iex> "  1\n  2\n"
      """
        1
        2
      """

  '''
  def heredocs, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationMismatchedPrompt do
  @doc ~S'''
    iex> foo = 1
     iex> bar = 2
    iex> foo + bar
    3
  '''
  def mismatched, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationTooMuch do
  @doc ~S'''
    iex> 1 + 2
      3
  '''
  def too_much, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.IndentationNotEnough do
  @doc ~S'''
      iex> 1 + 2
    3
  '''
  def test_fun, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.FencedHeredocs do
  @doc ~S'''
  Receives a test and formats its failure.

  ## Examples

  ```
  iex> 1 + 2
  3
  ```

      ```
      iex> 1 + 2
      3
      ```

  ```
      iex> 1 + 2
      3
  ```
  '''
  def heredocs, do: :ok

  @doc ~S'''
  ```
  iex> 1 + 2
  3
  '''
  def incomplete, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.Incomplete do
  @doc ~S'''
      iex> 1 + 2

  '''
  def test_fun, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.FenceIncomplete do
  @doc ~S'''
  ```
  iex> 1 + 2
  3
  '''
  def test_fun, do: :ok
end |> write_beam

defmodule ExUnit.DocTestTest.Numbered do
  @doc """
  iex(1)> 1 +
  ...(1)> 2
  3
  """
  def test_fun(), do: :ok
end |> write_beam()

defmodule ExUnit.DocTestTest.Host do
  @doc """
      iex(foo@bar)1> 1 +
      ...(foo@bar)1> 2
      3
  """
  def test_fun(), do: :ok
end |> write_beam()

defmodule ExUnit.DocTestTest.Haiku do
  @moduledoc """
  This module describes the ancient Japanese poem form known as Haiku.

  The Inspect protocol has been overriden for `%Haiku{}`
  so that Haikus are shown in a pretty-printed fashion.

  This module is part of the DocTest test suite,
  to ensure that DocTest can handle opaque inspect types
  which contain unicode and possibly consist of multiple lines.
  """

  defstruct [:first_phrase, :second_phrase, :third_phrase, :author]

  @doc """
  Creates a new Haiku.
  Optionally pass in the `author` as fourth argument.

  ## Examples:

      # Simple Haiku, inspect output consists of multiple lines.
      iex> ExUnit.DocTestTest.Haiku.new("Haikus are easy", "But sometimes they don't make sense", "Refrigerator")
      #Haiku<
        Haikus are easy
        But sometimes they don't make sense
        Refrigerator
      >

      # Haiku with Unicode characters (Japanese Kanji, em-dash).
      iex> ExUnit.DocTestTest.Haiku.new("古池や", "蛙飛びこむ", "水の音", "Matsuo Basho")
      #Haiku<
        古池や
        蛙飛びこむ
        水の音
        ― Matsuo Basho
      >

  """
  def new(first, second, third, author \\ "")
      when is_binary(first) and is_binary(second) and is_binary(third) and is_binary(author) do
    %__MODULE__{
      first_phrase: first,
      second_phrase: second,
      third_phrase: third,
      author: author
    }
  end

  defimpl Inspect do
    def inspect(haiku, _opts) do
      author = if haiku.author == "", do: "", else: "\n  ― #{haiku.author}"
      """
      #Haiku<
        #{haiku.first_phrase}
        #{haiku.second_phrase}
        #{haiku.third_phrase}#{author}
      >
      """
      |> String.trim_trailing("\n")
    end
  end
end |> write_beam

defmodule ExUnit.DocTestTest do
  use ExUnit.Case

  # This is intentional. The doctests in DocTest's docs
  # fail for demonstration purposes.
  # doctest ExUnit.DocTest

  doctest ExUnit.DocTestTest.GoodModule, import: true
  doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [test_fun1: 0], import: true
  doctest ExUnit.DocTestTest.SomewhatGoodModuleWithExcept, except: [:moduledoc, test_fun2: 0], import: true
  doctest ExUnit.DocTestTest.NoImport
  doctest ExUnit.DocTestTest.IndentationHeredocs
  doctest ExUnit.DocTestTest.FencedHeredocs
  doctest ExUnit.DocTestTest.Haiku

  import ExUnit.CaptureIO

  test "multiple functions filtered with :only" do
    defmodule MultipleOnly do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [test_fun1: 0, test_fun2: 0], import: true
    end

    ExUnit.Server.cases_loaded()
    assert capture_io(fn -> ExUnit.run end) =~ "2 tests, 1 failure"
  end

  test "doctest failures" do
    # When adding or removing lines above this line, the tests below will
    # fail because we are explicitly asserting some doctest lines from
    # ActuallyCompiled in the format of "test/ex_unit/doc_test_test.exs:<LINE>".
    defmodule ActuallyCompiled do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.Invalid
    end

    ExUnit.configure(seed: 0, colors: [enabled: false])
    ExUnit.Server.cases_loaded()
    output = capture_io(fn -> ExUnit.run end)

    # Test order is not guaranteed, we can't match this as a string for each failing doctest
    assert output =~ ~r/\d+\) test moduledoc at ExUnit\.DocTestTest\.Invalid \(\d+\) \(ExUnit\.DocTestTest\.ActuallyCompiled\)/

    assert output =~ """
      1) test moduledoc at ExUnit.DocTestTest.Invalid (1) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:127: syntax error before: '*'
         code: 1 + * 1
         stacktrace:
           test/ex_unit/doc_test_test.exs:127: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      2) test moduledoc at ExUnit.DocTestTest.Invalid (2) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest failed
         code: 1 + hd(List.flatten([1])) === 3
         left: 2
         stacktrace:
           test/ex_unit/doc_test_test.exs:130: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      3) test moduledoc at ExUnit.DocTestTest.Invalid (3) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest failed
         code: inspect(:oops) === "#MapSet<[]>"
         left: ":oops"
         stacktrace:
           test/ex_unit/doc_test_test.exs:133: ExUnit.DocTestTest.Invalid (module)
    """

    # The stacktrace points to the cause of the error
    assert output =~ """
      4) test moduledoc at ExUnit.DocTestTest.Invalid (4) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest failed: got UndefinedFunctionError with message "function Hello.world/0 is undefined (module Hello is not available)"
         code: Hello.world
         stacktrace:
           Hello.world()
           (for doctest at) test/ex_unit/doc_test_test.exs:136: (test)
    """

    assert output =~ """
      5) test moduledoc at ExUnit.DocTestTest.Invalid (5) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest failed: expected exception WhatIsThis but got RuntimeError with message "oops"
         code: raise "oops"
         stacktrace:
           test/ex_unit/doc_test_test.exs:139: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      6) test moduledoc at ExUnit.DocTestTest.Invalid (6) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest failed: wrong message for RuntimeError
         expected:
           "hello"
         actual:
           "oops"
         code: raise "oops"
         stacktrace:
           test/ex_unit/doc_test_test.exs:142: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      7) test doc at ExUnit.DocTestTest.Invalid.a/0 (7) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:148: syntax error before: '*'
         code: 1 + * 1
         stacktrace:
           test/ex_unit/doc_test_test.exs:148: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      8) test doc at ExUnit.DocTestTest.Invalid.b/0 (8) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:154: syntax error before: '*'
         code: 1 + * 1
         stacktrace:
           test/ex_unit/doc_test_test.exs:154: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
      9) test doc at ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (9) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:178: unexpected token: "`" (column 5, codepoint U+0060)
         code: 3
                   ```
         stacktrace:
           test/ex_unit/doc_test_test.exs:177: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
     10) test doc at ExUnit.DocTestTest.Invalid.indented_not_enough/0 (10) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:162: unexpected token: "`" (column 1, codepoint U+0060)
         code: 3
               `
         stacktrace:
           test/ex_unit/doc_test_test.exs:161: ExUnit.DocTestTest.Invalid (module)
    """

    assert output =~ """
     11) test doc at ExUnit.DocTestTest.Invalid.indented_too_much/0 (11) (ExUnit.DocTestTest.ActuallyCompiled)
         test/ex_unit/doc_test_test.exs:388
         Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:170: unexpected token: "`" (column 3, codepoint U+0060)
         code: 3
                 ```
         stacktrace:
           test/ex_unit/doc_test_test.exs:169: ExUnit.DocTestTest.Invalid (module)
    """
  end

  test "IEx prefix contains a number" do
    defmodule NumberedUsage do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.Numbered
    end

    ExUnit.Server.cases_loaded()
    assert capture_io(fn -> ExUnit.run end) =~ "1 test, 0 failures"
  end

  test "IEx prompt contains host" do
    message =
      ~s[unknown IEx prompt: "iex(foo@bar)1> 1 +".\nAccepted formats are: iex>, iex(1)>, ...>, ...(1)>]

    regex = ~r[test/ex_unit/doc_test_test\.exs:\d+: #{Regex.escape(message)}]

    assert_raise ExUnit.DocTest.Error, regex, fn ->
      defmodule HostUsage do
        use ExUnit.Case
        doctest ExUnit.DocTestTest.Host
      end
    end
  end

  test "tags tests as doctests" do
    defmodule DoctestTag do
      use ExUnit.Case
      doctest ExUnit.DocTestTest.NoImport

      setup test do
        assert test.doctest
        :ok
      end
    end

    ExUnit.Server.cases_loaded()
    assert capture_io(fn -> ExUnit.run end) =~ "1 test, 0 failures"
  end

  test "multiple exceptions in one test case is not supported" do
    assert_raise ExUnit.DocTest.Error, ~r"multiple exceptions in one doctest case are not supported", fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.MultipleExceptions
      end
    end
  end

  test "fails on invalid module" do
    assert_raise CompileError, ~r"module ExUnit\.DocTestTest\.Unknown is not loaded and could not be found", fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.Unknown
      end
    end
  end

  test "fails when there are no docs" do
    assert_raise ExUnit.DocTest.Error, ~r"could not retrieve the documentation for module ExUnit\.DocTestTest", fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest
      end
    end
  end

  test "fails in indentation mismatch" do
    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch: "   iex> bar = 2", should have been 2 spaces], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.IndentationMismatchedPrompt
      end
    end

    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch: "    3", should have been 2 spaces], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.IndentationTooMuch
      end
    end

    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: indentation level mismatch: \"  3\", should have been 4 spaces], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.IndentationNotEnough
      end
    end
  end

  test "fails with improper termination" do
    assert_raise ExUnit.DocTest.Error,
      ~r[test/ex_unit/doc_test_test\.exs:\d+: expected non-blank line to follow iex> prompt], fn ->
      defmodule NeverCompiled do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.Incomplete
      end
    end
  end

  test "fails on invalid use" do
    assert_raise RuntimeError, ~r"cannot define test", fn ->
      defmodule FunctionClashFail do
        import ExUnit.DocTest
        doctest ExUnit.DocTestTest.Invalid
      end
    end
  end
end


Document what exception impl_for!/1 raises (#10619)

Related: https://github.com/elixir-lang/elixir/pull/10617

defmodule Protocol do
  @moduledoc ~S"""
  Reference and functions for working with protocols.

  A protocol specifies an API that should be defined by its
  implementations. A protocol is defined with `Kernel.defprotocol/2`
  and its implementations with `Kernel.defimpl/3`.

  ## A real case

  In Elixir, we have two nouns for checking how many items there
  are in a data structure: `length` and `size`.  `length` means the
  information must be computed. For example, `length(list)` needs to
  traverse the whole list to calculate its length. On the other hand,
  `tuple_size(tuple)` and `byte_size(binary)` do not depend on the
  tuple and binary size as the size information is precomputed in
  the data structure.

  Although Elixir includes specific functions such as `tuple_size`,
  `binary_size` and `map_size`, sometimes we want to be able to
  retrieve the size of a data structure regardless of its type.
  In Elixir we can write polymorphic code, i.e. code that works
  with different shapes/types, by using protocols. A size protocol
  could be implemented as follows:

      defprotocol Size do
        @doc "Calculates the size (and not the length!) of a data structure"
        def size(data)
      end

  Now that the protocol can be implemented for every data structure
  the protocol may have a compliant implementation for:

      defimpl Size, for: BitString do
        def size(binary), do: byte_size(binary)
      end

      defimpl Size, for: Map do
        def size(map), do: map_size(map)
      end

      defimpl Size, for: Tuple do
        def size(tuple), do: tuple_size(tuple)
      end

  Note that we didn't implement it for lists as we don't have the
  `size` information on lists, rather its value needs to be
  computed with `length`.

  The data structure you are implementing the protocol for
  must be the first argument to all functions defined in the
  protocol.

  It is possible to implement protocols for all Elixir types:

    * Structs (see the "Protocols and Structs" section below)
    * `Tuple`
    * `Atom`
    * `List`
    * `BitString`
    * `Integer`
    * `Float`
    * `Function`
    * `PID`
    * `Map`
    * `Port`
    * `Reference`
    * `Any` (see the "Fallback to `Any`" section below)

  ## Protocols and Structs

  The real benefit of protocols comes when mixed with structs.
  For instance, Elixir ships with many data types implemented as
  structs, like `MapSet`. We can implement the `Size` protocol
  for those types as well:

      defimpl Size, for: MapSet do
        def size(map_set), do: MapSet.size(map_set)
      end

  When implementing a protocol for a struct, the `:for` option can
  be omitted if the `defimpl/3` call is inside the module that defines
  the struct:

      defmodule User do
        defstruct [:email, :name]

        defimpl Size do
          # two fields
          def size(%User{}), do: 2
        end
      end

  If a protocol implementation is not found for a given type,
  invoking the protocol will raise unless it is configured to
  fall back to `Any`. Conveniences for building implementations
  on top of existing ones are also available, look at `defstruct/1`
  for more information about deriving
  protocols.

  ## Fallback to `Any`

  In some cases, it may be convenient to provide a default
  implementation for all types. This can be achieved by setting
  the `@fallback_to_any` attribute to `true` in the protocol
  definition:

      defprotocol Size do
        @fallback_to_any true
        def size(data)
      end

  The `Size` protocol can now be implemented for `Any`:

      defimpl Size, for: Any do
        def size(_), do: 0
      end

  Although the implementation above is arguably not a reasonable
  one. For example, it makes no sense to say a PID or an integer
  have a size of `0`. That's one of the reasons why `@fallback_to_any`
  is an opt-in behaviour. For the majority of protocols, raising
  an error when a protocol is not implemented is the proper behaviour.

  ## Multiple implementations

  Protocols can also be implemented for multiple types at once:

      defprotocol Reversible do
        def reverse(term)
      end

      defimpl Reversible, for: [Map, List] do
        def reverse(term), do: Enum.reverse(term)
      end

  Inside `defimpl/3`, you can use `@protocol` to access the protocol
  being implemented and `@for` to access the module it is being
  defined for.

  ## Types

  Defining a protocol automatically defines a zero-arity type named `t`, which
  can be used as follows:

      @spec print_size(Size.t()) :: :ok
      def print_size(data) do
        result =
          case Size.size(data) do
            0 -> "data has no items"
            1 -> "data has one item"
            n -> "data has #{n} items"
          end

        IO.puts(result)
      end

  The `@spec` above expresses that all types allowed to implement the
  given protocol are valid argument types for the given function.

  ## Reflection

  Any protocol module contains three extra functions:

    * `__protocol__/1` - returns the protocol information. The function takes
      one of the following atoms:

      * `:consolidated?` - returns whether the protocol is consolidated

      * `:functions` - returns a keyword list of protocol functions and their arities

      * `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
        implementing the protocol, otherwise `:not_consolidated`

      * `:module` - the protocol module atom name

    * `impl_for/1` - returns the module that implements the protocol for the given argument,
      `nil` otherwise

    * `impl_for!/1` - same as above but raises `Protocol.UndefinedError` if an implementation is
      not found

  For example, for the `Enumerable` protocol we have:

      iex> Enumerable.__protocol__(:functions)
      [count: 1, member?: 2, reduce: 3, slice: 1]

      iex> Enumerable.impl_for([])
      Enumerable.List

      iex> Enumerable.impl_for(42)
      nil

  In addition, every protocol implementation module contains the `__impl__/1`
  function. The function takes one of the following atoms:

    * `:for` - returns the module responsible for the data structure of the
      protocol implementation

    * `:protocol` - returns the protocol module for which this implementation
    is provided

  For example, the module implementing the `Enumerable` protocol for lists is
  `Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module:

      iex(1)> Enumerable.List.__impl__(:for)
      List

      iex(2)> Enumerable.List.__impl__(:protocol)
      Enumerable

  ## Consolidation

  In order to speed up protocol dispatching, whenever all protocol implementations
  are known up-front, typically after all Elixir code in a project is compiled,
  Elixir provides a feature called *protocol consolidation*. Consolidation directly
  links protocols to their implementations in a way that invoking a function from a
  consolidated protocol is equivalent to invoking two remote functions.

  Protocol consolidation is applied by default to all Mix projects during compilation.
  This may be an issue during test. For instance, if you want to implement a protocol
  during test, the implementation will have no effect, as the protocol has already been
  consolidated. One possible solution is to include compilation directories that are
  specific to your test environment in your mix.exs:

      def project do
        ...
        elixirc_paths: elixirc_paths(Mix.env())
        ...
      end

      defp elixirc_paths(:test), do: ["lib", "test/support"]
      defp elixirc_paths(_), do: ["lib"]

  And then you can define the implementations specific to the test environment
  inside `test/support/some_file.ex`.

  Another approach is to disable protocol consolidation during tests in your
  mix.exs:

      def project do
        ...
        consolidate_protocols: Mix.env() != :test
        ...
      end

  Although doing so is not recommended as it may affect your test suite
  performance.

  Finally, note all protocols are compiled with `debug_info` set to `true`,
  regardless of the option set by the `elixirc` compiler. The debug info is
  used for consolidation and it is removed after consolidation unless
  globally set.
  """

  @doc false
  defmacro def(signature)

  defmacro def({_, _, args}) when args == [] or is_atom(args) do
    raise ArgumentError, "protocol functions expect at least one argument"
  end

  defmacro def({name, _, args}) when is_atom(name) and is_list(args) do
    arity = length(args)

    type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity))
    type_args = [quote(do: t) | type_args]

    varify = fn pos -> Macro.var(String.to_atom("arg" <> Integer.to_string(pos)), __MODULE__) end

    call_args = :lists.map(varify, :lists.seq(2, arity))
    call_args = [quote(do: term) | call_args]

    quote do
      name = unquote(name)
      arity = unquote(arity)

      @functions [{name, arity} | @functions]

      # Generate a fake definition with the user
      # signature that will be used by docs
      Kernel.def(unquote(name)(unquote_splicing(args)))

      # Generate the actual implementation
      Kernel.def unquote(name)(unquote_splicing(call_args)) do
        impl_for!(term).unquote(name)(unquote_splicing(call_args))
      end

      # Copy spec as callback if possible,
      # otherwise generate a dummy callback
      Module.spec_to_callback(__MODULE__, {name, arity}) ||
        @callback unquote(name)(unquote_splicing(type_args)) :: term
    end
  end

  defmacro def(_) do
    raise ArgumentError, "invalid arguments for def inside defprotocol"
  end

  @doc """
  Checks if the given module is loaded and is protocol.

  Returns `:ok` if so, otherwise raises `ArgumentError`.
  """
  @spec assert_protocol!(module) :: :ok
  def assert_protocol!(module) do
    assert_protocol!(module, "")
  end

  defp assert_protocol!(module, extra) do
    case Code.ensure_compiled(module) do
      {:module, ^module} -> :ok
      _ -> raise ArgumentError, "#{inspect(module)} is not available" <> extra
    end

    try do
      module.__protocol__(:module)
    rescue
      UndefinedFunctionError ->
        raise ArgumentError, "#{inspect(module)} is not a protocol" <> extra
    end

    :ok
  end

  @doc """
  Checks if the given module is loaded and is an implementation
  of the given protocol.

  Returns `:ok` if so, otherwise raises `ArgumentError`.
  """
  @spec assert_impl!(module, module) :: :ok
  def assert_impl!(protocol, base) do
    assert_impl!(protocol, base, "")
  end

  defp assert_impl!(protocol, base, extra) do
    impl = Module.concat(protocol, base)

    case Code.ensure_compiled(impl) do
      {:module, ^impl} -> :ok
      _ -> raise ArgumentError, "#{inspect(impl)} is not available" <> extra
    end

    try do
      impl.__impl__(:protocol)
    rescue
      UndefinedFunctionError ->
        raise ArgumentError, "#{inspect(impl)} is not an implementation of a protocol" <> extra
    else
      ^protocol ->
        :ok

      other ->
        raise ArgumentError,
              "expected #{inspect(impl)} to be an implementation of #{inspect(protocol)}" <>
                ", got: #{inspect(other)}" <> extra
    end
  end

  @doc """
  Derives the `protocol` for `module` with the given options.

  If your implementation passes options or if you are generating
  custom code based on the struct, you will also need to implement
  a macro defined as `__deriving__(module, struct, options)`
  to get the options that were passed.

  ## Examples

      defprotocol Derivable do
        def ok(arg)
      end

      defimpl Derivable, for: Any do
        defmacro __deriving__(module, struct, options) do
          quote do
            defimpl Derivable, for: unquote(module) do
              def ok(arg) do
                {:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
              end
            end
          end
        end

        def ok(arg) do
          {:ok, arg}
        end
      end

      defmodule ImplStruct do
        @derive [Derivable]
        defstruct a: 0, b: 0
      end

      Derivable.ok(%ImplStruct{})
      {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}

  Explicit derivations can now be called via `__deriving__`:

      # Explicitly derived via `__deriving__`
      Derivable.ok(%ImplStruct{a: 1, b: 1})

      # Explicitly derived by API via `__deriving__`
      require Protocol
      Protocol.derive(Derivable, ImplStruct, :oops)
      Derivable.ok(%ImplStruct{a: 1, b: 1})

  """
  defmacro derive(protocol, module, options \\ []) do
    quote do
      Protocol.__derive__([{unquote(protocol), unquote(options)}], unquote(module), __ENV__)
    end
  end

  ## Consolidation

  @doc """
  Extracts all protocols from the given paths.

  The paths can be either a charlist or a string. Internally
  they are worked on as charlists, so passing them as lists
  avoid extra conversion.

  Does not load any of the protocols.

  ## Examples

      # Get Elixir's ebin directory path and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.extract_protocols([path])
      iex> Enumerable in mods
      true

  """
  @spec extract_protocols([charlist | String.t()]) :: [atom]
  def extract_protocols(paths) do
    extract_matching_by_attribute(paths, 'Elixir.', fn module, attributes ->
      case attributes[:__protocol__] do
        [fallback_to_any: _] -> module
        _ -> nil
      end
    end)
  end

  @doc """
  Extracts all types implemented for the given protocol from
  the given paths.

  The paths can be either a charlist or a string. Internally
  they are worked on as charlists, so passing them as lists
  avoid extra conversion.

  Does not load any of the implementations.

  ## Examples

      # Get Elixir's ebin directory path and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.extract_impls(Enumerable, [path])
      iex> List in mods
      true

  """
  @spec extract_impls(module, [charlist | String.t()]) :: [atom]
  def extract_impls(protocol, paths) when is_atom(protocol) do
    prefix = Atom.to_charlist(protocol) ++ '.'

    extract_matching_by_attribute(paths, prefix, fn _mod, attributes ->
      case attributes[:__impl__] do
        [protocol: ^protocol, for: for] -> for
        _ -> nil
      end
    end)
  end

  defp extract_matching_by_attribute(paths, prefix, callback) do
    for path <- paths,
        path = to_charlist(path),
        file <- list_dir(path),
        mod = extract_from_file(path, file, prefix, callback),
        do: mod
  end

  defp list_dir(path) when is_list(path) do
    case :file.list_dir(path) do
      {:ok, files} -> files
      _ -> []
    end
  end

  defp extract_from_file(path, file, prefix, callback) do
    if :lists.prefix(prefix, file) and :filename.extension(file) == '.beam' do
      extract_from_beam(:filename.join(path, file), callback)
    end
  end

  defp extract_from_beam(file, callback) do
    case :beam_lib.chunks(file, [:attributes]) do
      {:ok, {module, [attributes: attributes]}} ->
        callback.(module, attributes)

      _ ->
        nil
    end
  end

  @doc """
  Returns `true` if the protocol was consolidated.
  """
  @spec consolidated?(module) :: boolean
  def consolidated?(protocol) do
    protocol.__protocol__(:consolidated?)
  end

  @doc """
  Receives a protocol and a list of implementations and
  consolidates the given protocol.

  Consolidation happens by changing the protocol `impl_for`
  in the abstract format to have fast lookup rules. Usually
  the list of implementations to use during consolidation
  are retrieved with the help of `extract_impls/2`.

  It returns the updated version of the protocol bytecode.
  If the first element of the tuple is `:ok`, it means
  the protocol was consolidated.

  A given bytecode or protocol implementation can be checked
  to be consolidated or not by analyzing the protocol
  attribute:

      Protocol.consolidated?(Enumerable)

  This function does not load the protocol at any point
  nor loads the new bytecode for the compiled module.
  However each implementation must be available and
  it will be loaded.
  """
  @spec consolidate(module, [module]) ::
          {:ok, binary}
          | {:error, :not_a_protocol}
          | {:error, :no_beam_info}
  def consolidate(protocol, types) when is_atom(protocol) do
    with {:ok, ast_info, specs, compile_info} <- beam_protocol(protocol),
         {:ok, definitions} <- change_debug_info(protocol, ast_info, types),
         do: compile(definitions, specs, compile_info)
  end

  defp beam_protocol(protocol) do
    chunk_ids = [:debug_info, 'Docs', 'ExCk']
    opts = [:allow_missing_chunks]

    case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
      {:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
        {:debug_info_v1, _backend, {:elixir_v1, info, specs}} = debug_info
        %{attributes: attributes, definitions: definitions} = info
        chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
        chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)

        case attributes[:__protocol__] do
          [fallback_to_any: any] ->
            {:ok, {any, definitions}, specs, {info, chunks}}

          _ ->
            {:error, :not_a_protocol}
        end

      _ ->
        {:error, :no_beam_info}
    end
  end

  defp beam_file(module) when is_atom(module) do
    case :code.which(module) do
      [_ | _] = file -> file
      _ -> module
    end
  end

  # Change the debug information to the optimized
  # impl_for/1 dispatch version.
  defp change_debug_info(protocol, {any, definitions}, types) do
    types = if any, do: types, else: List.delete(types, Any)
    all = [Any] ++ for {_guard, mod} <- __built_in__(), do: mod
    structs = types -- all

    case List.keytake(definitions, {:__protocol__, 1}, 0) do
      {protocol_def, definitions} ->
        {impl_for, definitions} = List.keytake(definitions, {:impl_for, 1}, 0)
        {struct_impl_for, definitions} = List.keytake(definitions, {:struct_impl_for, 1}, 0)

        protocol_def = change_protocol(protocol_def, types)
        impl_for = change_impl_for(impl_for, protocol, types)
        struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)

        {:ok, [protocol_def, impl_for, struct_impl_for] ++ definitions}

      nil ->
        {:error, :not_a_protocol}
    end
  end

  defp change_protocol({_name, _kind, meta, clauses}, types) do
    clauses =
      Enum.map(clauses, fn
        {meta, [:consolidated?], [], _} -> {meta, [:consolidated?], [], true}
        {meta, [:impls], [], _} -> {meta, [:impls], [], {:consolidated, types}}
        clause -> clause
      end)

    {{:__protocol__, 1}, :def, meta, clauses}
  end

  defp change_impl_for({_name, _kind, meta, _clauses}, protocol, types) do
    fallback = if Any in types, do: load_impl(protocol, Any)
    line = meta[:line]

    clauses =
      for {guard, mod} <- __built_in__(),
          mod in types,
          do: built_in_clause_for(mod, guard, protocol, meta, line)

    struct_clause = struct_clause_for(meta, line)
    fallback_clause = fallback_clause_for(fallback, protocol, meta)
    clauses = [struct_clause] ++ clauses ++ [fallback_clause]

    {{:impl_for, 1}, :def, meta, clauses}
  end

  defp change_struct_impl_for({_name, _kind, meta, _clauses}, protocol, types, structs) do
    fallback = if Any in types, do: load_impl(protocol, Any)
    clauses = for struct <- structs, do: each_struct_clause_for(struct, protocol, meta)
    clauses = clauses ++ [fallback_clause_for(fallback, protocol, meta)]

    {{:struct_impl_for, 1}, :defp, meta, clauses}
  end

  defp built_in_clause_for(mod, guard, protocol, meta, line) do
    x = {:x, [line: line, version: -1], __MODULE__}
    guard = quote(line: line, do: :erlang.unquote(guard)(unquote(x)))
    body = load_impl(protocol, mod)
    {meta, [x], [guard], body}
  end

  defp struct_clause_for(meta, line) do
    x = {:x, [line: line, version: -1], __MODULE__}
    head = quote(line: line, do: %{__struct__: unquote(x)})
    guard = quote(line: line, do: :erlang.is_atom(unquote(x)))
    body = quote(line: line, do: struct_impl_for(unquote(x)))
    {meta, [head], [guard], body}
  end

  defp each_struct_clause_for(struct, protocol, meta) do
    {meta, [struct], [], load_impl(protocol, struct)}
  end

  defp fallback_clause_for(value, _protocol, meta) do
    {meta, [quote(do: _)], [], value}
  end

  defp load_impl(protocol, for) do
    Module.concat(protocol, for).__impl__(:target)
  end

  # Finally compile the module and emit its bytecode.
  defp compile(definitions, specs, {info, chunks}) do
    info = %{info | definitions: definitions}
    {:ok, :elixir_erl.consolidate(info, specs, chunks)}
  end

  ## Definition callbacks

  @doc false
  def __protocol__(name, do: block) do
    quote do
      defmodule unquote(name) do
        # We don't allow function definition inside protocols
        import Kernel,
          except: [
            def: 1,
            def: 2,
            defp: 1,
            defp: 2,
            defdelegate: 2,
            defexception: 1,
            defguard: 1,
            defguardp: 1,
            defmacro: 1,
            defmacro: 2,
            defmacrop: 1,
            defmacrop: 2
          ]

        # Import the new dsl that holds the new def
        import Protocol, only: [def: 1]

        # Compile with debug info for consolidation
        @compile :debug_info

        # Set up a clear slate to store defined functions
        @functions []
        @fallback_to_any false

        # Invoke the user given block
        _ = unquote(block)

        # Finalize expansion
        unquote(after_defprotocol())
      end
    end
  end

  defp after_defprotocol do
    quote bind_quoted: [built_in: __built_in__()] do
      any_impl_for =
        if @fallback_to_any do
          quote do: unquote(__MODULE__.Any).__impl__(:target)
        else
          nil
        end

      # Disable Dialyzer checks - before and after consolidation
      # the types could be more strict
      @dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}

      @doc false
      @spec impl_for(term) :: atom | nil
      Kernel.def(impl_for(data))

      # Define the implementation for structs.
      #
      # It simply delegates to struct_impl_for which is then
      # optimized during protocol consolidation.
      Kernel.def impl_for(%struct{}) do
        struct_impl_for(struct)
      end

      # Define the implementation for built-ins
      :lists.foreach(
        fn {guard, mod} ->
          target = Module.concat(__MODULE__, mod)

          Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
            try do
              unquote(target).__impl__(:target)
            rescue
              UndefinedFunctionError ->
                unquote(any_impl_for)
            end
          end
        end,
        built_in
      )

      # Define a catch-all impl_for/1 clause to pacify Dialyzer (since
      # destructuring opaque types is illegal, Dialyzer will think none of the
      # previous clauses matches opaque types, and without this clause, will
      # conclude that impl_for can't handle an opaque argument). This is a hack
      # since it relies on Dialyzer not being smart enough to conclude that all
      # opaque types will get the any_impl_for/0 implementation.
      Kernel.def impl_for(_) do
        unquote(any_impl_for)
      end

      @doc false
      @spec impl_for!(term) :: atom
      if any_impl_for do
        Kernel.def impl_for!(data) do
          impl_for(data)
        end
      else
        Kernel.def impl_for!(data) do
          impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
        end
      end

      # Internal handler for Structs
      Kernel.defp struct_impl_for(struct) do
        target = Module.concat(__MODULE__, struct)

        try do
          target.__impl__(:target)
        rescue
          UndefinedFunctionError ->
            unquote(any_impl_for)
        end
      end

      # Inline struct implementation for performance
      @compile {:inline, struct_impl_for: 1}

      unless Module.defines_type?(__MODULE__, {:t, 0}) do
        @type t :: term
      end

      # Store information as an attribute so it
      # can be read without loading the module.
      Module.register_attribute(__MODULE__, :__protocol__, persist: true)
      @__protocol__ [fallback_to_any: !!@fallback_to_any]

      @doc false
      @spec __protocol__(:module) :: __MODULE__
      @spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@functions))
      @spec __protocol__(:consolidated?) :: boolean
      @spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}
      Kernel.def(__protocol__(:module), do: __MODULE__)
      Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@functions)))
      Kernel.def(__protocol__(:consolidated?), do: false)
      Kernel.def(__protocol__(:impls), do: :not_consolidated)
    end
  end

  @doc false
  def __functions_spec__([]), do: []

  def __functions_spec__([head | tail]),
    do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)]

  @doc false
  def __impl__(protocol, opts) do
    do_defimpl(protocol, :lists.keysort(1, opts))
  end

  defp do_defimpl(protocol, do: block, for: for) when is_list(for) do
    for f <- for, do: do_defimpl(protocol, do: block, for: f)
  end

  defp do_defimpl(protocol, do: block, for: for) do
    # Unquote the implementation just later
    # when all variables will already be injected
    # into the module body.
    impl =
      quote unquote: false do
        @doc false
        @spec __impl__(:for) :: unquote(for)
        @spec __impl__(:target) :: __MODULE__
        @spec __impl__(:protocol) :: unquote(protocol)
        def __impl__(:for), do: unquote(for)
        def __impl__(:target), do: __MODULE__
        def __impl__(:protocol), do: unquote(protocol)
      end

    quote do
      protocol = unquote(protocol)
      for = unquote(for)
      name = Module.concat(protocol, for)

      Protocol.assert_protocol!(protocol)
      Protocol.__ensure_defimpl__(protocol, for, __ENV__)

      defmodule name do
        @behaviour protocol
        @protocol protocol
        @for for

        unquote(block)

        Module.register_attribute(__MODULE__, :__impl__, persist: true)
        @__impl__ [protocol: @protocol, for: @for]

        unquote(impl)
      end
    end
  end

  @doc false
  def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
    struct = Macro.struct!(for, env)

    foreach = fn
      proto when is_atom(proto) ->
        derive(proto, for, struct, [], env)

      {proto, opts} when is_atom(proto) ->
        derive(proto, for, struct, opts, env)
    end

    :lists.foreach(foreach, :lists.flatten(derives))

    :ok
  end

  defp derive(protocol, for, struct, opts, env) do
    extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
    assert_protocol!(protocol, extra)
    __ensure_defimpl__(protocol, for, env)
    assert_impl!(protocol, Any, extra)

    # Clean up variables from eval context
    env = :elixir_env.reset_vars(env)
    args = [for, struct, opts]
    impl = Module.concat(protocol, Any)

    :elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn mod, fun, args ->
      if function_exported?(mod, fun, length(args)) do
        apply(mod, fun, args)
      else
        quoted =
          quote do
            Module.register_attribute(__MODULE__, :__impl__, persist: true)
            @__impl__ [protocol: unquote(protocol), for: unquote(for)]

            @doc false
            @spec __impl__(:target) :: unquote(impl)
            @spec __impl__(:protocol) :: unquote(protocol)
            @spec __impl__(:for) :: unquote(for)
            def __impl__(:target), do: unquote(impl)
            def __impl__(:protocol), do: unquote(protocol)
            def __impl__(:for), do: unquote(for)
          end

        Module.create(Module.concat(protocol, for), quoted, Macro.Env.location(env))
      end
    end)
  end

  @doc false
  def __ensure_defimpl__(protocol, for, env) do
    if Protocol.consolidated?(protocol) do
      message =
        "the #{inspect(protocol)} protocol has already been consolidated, an " <>
          "implementation for #{inspect(for)} has no effect. If you want to " <>
          "implement protocols after compilation or during tests, check the " <>
          "\"Consolidation\" section in the Protocol module documentation"

      IO.warn(message, Macro.Env.stacktrace(env))
    end

    :ok
  end

  ## Helpers

  @doc false
  def __built_in__ do
    [
      is_tuple: Tuple,
      is_atom: Atom,
      is_list: List,
      is_map: Map,
      is_bitstring: BitString,
      is_integer: Integer,
      is_float: Float,
      is_function: Function,
      is_pid: PID,
      is_port: Port,
      is_reference: Reference
    ]
  end
end

defmodule Protocol do
  @moduledoc ~S"""
  Reference and functions for working with protocols.

  A protocol specifies an API that should be defined by its
  implementations. A protocol is defined with `Kernel.defprotocol/2`
  and its implementations with `Kernel.defimpl/3`.

  ## A real case

  In Elixir, we have two nouns for checking how many items there
  are in a data structure: `length` and `size`.  `length` means the
  information must be computed. For example, `length(list)` needs to
  traverse the whole list to calculate its length. On the other hand,
  `tuple_size(tuple)` and `byte_size(binary)` do not depend on the
  tuple and binary size as the size information is precomputed in
  the data structure.

  Although Elixir includes specific functions such as `tuple_size`,
  `binary_size` and `map_size`, sometimes we want to be able to
  retrieve the size of a data structure regardless of its type.
  In Elixir we can write polymorphic code, i.e. code that works
  with different shapes/types, by using protocols. A size protocol
  could be implemented as follows:

      defprotocol Size do
        @doc "Calculates the size (and not the length!) of a data structure"
        def size(data)
      end

  Now that the protocol can be implemented for every data structure
  the protocol may have a compliant implementation for:

      defimpl Size, for: BitString do
        def size(binary), do: byte_size(binary)
      end

      defimpl Size, for: Map do
        def size(map), do: map_size(map)
      end

      defimpl Size, for: Tuple do
        def size(tuple), do: tuple_size(tuple)
      end

  Note that we didn't implement it for lists as we don't have the
  `size` information on lists, rather its value needs to be
  computed with `length`.

  The data structure you are implementing the protocol for
  must be the first argument to all functions defined in the
  protocol.

  It is possible to implement protocols for all Elixir types:

    * Structs (see the "Protocols and Structs" section below)
    * `Tuple`
    * `Atom`
    * `List`
    * `BitString`
    * `Integer`
    * `Float`
    * `Function`
    * `PID`
    * `Map`
    * `Port`
    * `Reference`
    * `Any` (see the "Fallback to `Any`" section below)

  ## Protocols and Structs

  The real benefit of protocols comes when mixed with structs.
  For instance, Elixir ships with many data types implemented as
  structs, like `MapSet`. We can implement the `Size` protocol
  for those types as well:

      defimpl Size, for: MapSet do
        def size(map_set), do: MapSet.size(map_set)
      end

  When implementing a protocol for a struct, the `:for` option can
  be omitted if the `defimpl/3` call is inside the module that defines
  the struct:

      defmodule User do
        defstruct [:email, :name]

        defimpl Size do
          # two fields
          def size(%User{}), do: 2
        end
      end

  If a protocol implementation is not found for a given type,
  invoking the protocol will raise unless it is configured to
  fall back to `Any`. Conveniences for building implementations
  on top of existing ones are also available, look at `defstruct/1`
  for more information about deriving
  protocols.

  ## Fallback to `Any`

  In some cases, it may be convenient to provide a default
  implementation for all types. This can be achieved by setting
  the `@fallback_to_any` attribute to `true` in the protocol
  definition:

      defprotocol Size do
        @fallback_to_any true
        def size(data)
      end

  The `Size` protocol can now be implemented for `Any`:

      defimpl Size, for: Any do
        def size(_), do: 0
      end

  Although the implementation above is arguably not a reasonable
  one. For example, it makes no sense to say a PID or an integer
  have a size of `0`. That's one of the reasons why `@fallback_to_any`
  is an opt-in behaviour. For the majority of protocols, raising
  an error when a protocol is not implemented is the proper behaviour.

  ## Multiple implementations

  Protocols can also be implemented for multiple types at once:

      defprotocol Reversible do
        def reverse(term)
      end

      defimpl Reversible, for: [Map, List] do
        def reverse(term), do: Enum.reverse(term)
      end

  Inside `defimpl/3`, you can use `@protocol` to access the protocol
  being implemented and `@for` to access the module it is being
  defined for.

  ## Types

  Defining a protocol automatically defines a zero-arity type named `t`, which
  can be used as follows:

      @spec print_size(Size.t()) :: :ok
      def print_size(data) do
        result =
          case Size.size(data) do
            0 -> "data has no items"
            1 -> "data has one item"
            n -> "data has #{n} items"
          end

        IO.puts(result)
      end

  The `@spec` above expresses that all types allowed to implement the
  given protocol are valid argument types for the given function.

  ## Reflection

  Any protocol module contains three extra functions:

    * `__protocol__/1` - returns the protocol information. The function takes
      one of the following atoms:

      * `:consolidated?` - returns whether the protocol is consolidated

      * `:functions` - returns a keyword list of protocol functions and their arities

      * `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
        implementing the protocol, otherwise `:not_consolidated`

      * `:module` - the protocol module atom name

    * `impl_for/1` - returns the module that implements the protocol for the given argument,
      `nil` otherwise

    * `impl_for!/1` - same as above but raises an error if an implementation is
      not found

  For example, for the `Enumerable` protocol we have:

      iex> Enumerable.__protocol__(:functions)
      [count: 1, member?: 2, reduce: 3, slice: 1]

      iex> Enumerable.impl_for([])
      Enumerable.List

      iex> Enumerable.impl_for(42)
      nil

  In addition, every protocol implementation module contains the `__impl__/1`
  function. The function takes one of the following atoms:

    * `:for` - returns the module responsible for the data structure of the
      protocol implementation

    * `:protocol` - returns the protocol module for which this implementation
    is provided

  For example, the module implementing the `Enumerable` protocol for lists is
  `Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module:

      iex(1)> Enumerable.List.__impl__(:for)
      List

      iex(2)> Enumerable.List.__impl__(:protocol)
      Enumerable

  ## Consolidation

  In order to speed up protocol dispatching, whenever all protocol implementations
  are known up-front, typically after all Elixir code in a project is compiled,
  Elixir provides a feature called *protocol consolidation*. Consolidation directly
  links protocols to their implementations in a way that invoking a function from a
  consolidated protocol is equivalent to invoking two remote functions.

  Protocol consolidation is applied by default to all Mix projects during compilation.
  This may be an issue during test. For instance, if you want to implement a protocol
  during test, the implementation will have no effect, as the protocol has already been
  consolidated. One possible solution is to include compilation directories that are
  specific to your test environment in your mix.exs:

      def project do
        ...
        elixirc_paths: elixirc_paths(Mix.env())
        ...
      end

      defp elixirc_paths(:test), do: ["lib", "test/support"]
      defp elixirc_paths(_), do: ["lib"]

  And then you can define the implementations specific to the test environment
  inside `test/support/some_file.ex`.

  Another approach is to disable protocol consolidation during tests in your
  mix.exs:

      def project do
        ...
        consolidate_protocols: Mix.env() != :test
        ...
      end

  Although doing so is not recommended as it may affect your test suite
  performance.

  Finally, note all protocols are compiled with `debug_info` set to `true`,
  regardless of the option set by the `elixirc` compiler. The debug info is
  used for consolidation and it is removed after consolidation unless
  globally set.
  """

  @doc false
  defmacro def(signature)

  defmacro def({_, _, args}) when args == [] or is_atom(args) do
    raise ArgumentError, "protocol functions expect at least one argument"
  end

  defmacro def({name, _, args}) when is_atom(name) and is_list(args) do
    arity = length(args)

    type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity))
    type_args = [quote(do: t) | type_args]

    varify = fn pos -> Macro.var(String.to_atom("arg" <> Integer.to_string(pos)), __MODULE__) end

    call_args = :lists.map(varify, :lists.seq(2, arity))
    call_args = [quote(do: term) | call_args]

    quote do
      name = unquote(name)
      arity = unquote(arity)

      @functions [{name, arity} | @functions]

      # Generate a fake definition with the user
      # signature that will be used by docs
      Kernel.def(unquote(name)(unquote_splicing(args)))

      # Generate the actual implementation
      Kernel.def unquote(name)(unquote_splicing(call_args)) do
        impl_for!(term).unquote(name)(unquote_splicing(call_args))
      end

      # Copy spec as callback if possible,
      # otherwise generate a dummy callback
      Module.spec_to_callback(__MODULE__, {name, arity}) ||
        @callback unquote(name)(unquote_splicing(type_args)) :: term
    end
  end

  defmacro def(_) do
    raise ArgumentError, "invalid arguments for def inside defprotocol"
  end

  @doc """
  Checks if the given module is loaded and is protocol.

  Returns `:ok` if so, otherwise raises `ArgumentError`.
  """
  @spec assert_protocol!(module) :: :ok
  def assert_protocol!(module) do
    assert_protocol!(module, "")
  end

  defp assert_protocol!(module, extra) do
    case Code.ensure_compiled(module) do
      {:module, ^module} -> :ok
      _ -> raise ArgumentError, "#{inspect(module)} is not available" <> extra
    end

    try do
      module.__protocol__(:module)
    rescue
      UndefinedFunctionError ->
        raise ArgumentError, "#{inspect(module)} is not a protocol" <> extra
    end

    :ok
  end

  @doc """
  Checks if the given module is loaded and is an implementation
  of the given protocol.

  Returns `:ok` if so, otherwise raises `ArgumentError`.
  """
  @spec assert_impl!(module, module) :: :ok
  def assert_impl!(protocol, base) do
    assert_impl!(protocol, base, "")
  end

  defp assert_impl!(protocol, base, extra) do
    impl = Module.concat(protocol, base)

    case Code.ensure_compiled(impl) do
      {:module, ^impl} -> :ok
      _ -> raise ArgumentError, "#{inspect(impl)} is not available" <> extra
    end

    try do
      impl.__impl__(:protocol)
    rescue
      UndefinedFunctionError ->
        raise ArgumentError, "#{inspect(impl)} is not an implementation of a protocol" <> extra
    else
      ^protocol ->
        :ok

      other ->
        raise ArgumentError,
              "expected #{inspect(impl)} to be an implementation of #{inspect(protocol)}" <>
                ", got: #{inspect(other)}" <> extra
    end
  end

  @doc """
  Derives the `protocol` for `module` with the given options.

  If your implementation passes options or if you are generating
  custom code based on the struct, you will also need to implement
  a macro defined as `__deriving__(module, struct, options)`
  to get the options that were passed.

  ## Examples

      defprotocol Derivable do
        def ok(arg)
      end

      defimpl Derivable, for: Any do
        defmacro __deriving__(module, struct, options) do
          quote do
            defimpl Derivable, for: unquote(module) do
              def ok(arg) do
                {:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
              end
            end
          end
        end

        def ok(arg) do
          {:ok, arg}
        end
      end

      defmodule ImplStruct do
        @derive [Derivable]
        defstruct a: 0, b: 0
      end

      Derivable.ok(%ImplStruct{})
      {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}

  Explicit derivations can now be called via `__deriving__`:

      # Explicitly derived via `__deriving__`
      Derivable.ok(%ImplStruct{a: 1, b: 1})

      # Explicitly derived by API via `__deriving__`
      require Protocol
      Protocol.derive(Derivable, ImplStruct, :oops)
      Derivable.ok(%ImplStruct{a: 1, b: 1})

  """
  defmacro derive(protocol, module, options \\ []) do
    quote do
      Protocol.__derive__([{unquote(protocol), unquote(options)}], unquote(module), __ENV__)
    end
  end

  ## Consolidation

  @doc """
  Extracts all protocols from the given paths.

  The paths can be either a charlist or a string. Internally
  they are worked on as charlists, so passing them as lists
  avoid extra conversion.

  Does not load any of the protocols.

  ## Examples

      # Get Elixir's ebin directory path and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.extract_protocols([path])
      iex> Enumerable in mods
      true

  """
  @spec extract_protocols([charlist | String.t()]) :: [atom]
  def extract_protocols(paths) do
    extract_matching_by_attribute(paths, 'Elixir.', fn module, attributes ->
      case attributes[:__protocol__] do
        [fallback_to_any: _] -> module
        _ -> nil
      end
    end)
  end

  @doc """
  Extracts all types implemented for the given protocol from
  the given paths.

  The paths can be either a charlist or a string. Internally
  they are worked on as charlists, so passing them as lists
  avoid extra conversion.

  Does not load any of the implementations.

  ## Examples

      # Get Elixir's ebin directory path and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.extract_impls(Enumerable, [path])
      iex> List in mods
      true

  """
  @spec extract_impls(module, [charlist | String.t()]) :: [atom]
  def extract_impls(protocol, paths) when is_atom(protocol) do
    prefix = Atom.to_charlist(protocol) ++ '.'

    extract_matching_by_attribute(paths, prefix, fn _mod, attributes ->
      case attributes[:__impl__] do
        [protocol: ^protocol, for: for] -> for
        _ -> nil
      end
    end)
  end

  defp extract_matching_by_attribute(paths, prefix, callback) do
    for path <- paths,
        path = to_charlist(path),
        file <- list_dir(path),
        mod = extract_from_file(path, file, prefix, callback),
        do: mod
  end

  defp list_dir(path) when is_list(path) do
    case :file.list_dir(path) do
      {:ok, files} -> files
      _ -> []
    end
  end

  defp extract_from_file(path, file, prefix, callback) do
    if :lists.prefix(prefix, file) and :filename.extension(file) == '.beam' do
      extract_from_beam(:filename.join(path, file), callback)
    end
  end

  defp extract_from_beam(file, callback) do
    case :beam_lib.chunks(file, [:attributes]) do
      {:ok, {module, [attributes: attributes]}} ->
        callback.(module, attributes)

      _ ->
        nil
    end
  end

  @doc """
  Returns `true` if the protocol was consolidated.
  """
  @spec consolidated?(module) :: boolean
  def consolidated?(protocol) do
    protocol.__protocol__(:consolidated?)
  end

  @doc """
  Receives a protocol and a list of implementations and
  consolidates the given protocol.

  Consolidation happens by changing the protocol `impl_for`
  in the abstract format to have fast lookup rules. Usually
  the list of implementations to use during consolidation
  are retrieved with the help of `extract_impls/2`.

  It returns the updated version of the protocol bytecode.
  If the first element of the tuple is `:ok`, it means
  the protocol was consolidated.

  A given bytecode or protocol implementation can be checked
  to be consolidated or not by analyzing the protocol
  attribute:

      Protocol.consolidated?(Enumerable)

  This function does not load the protocol at any point
  nor loads the new bytecode for the compiled module.
  However each implementation must be available and
  it will be loaded.
  """
  @spec consolidate(module, [module]) ::
          {:ok, binary}
          | {:error, :not_a_protocol}
          | {:error, :no_beam_info}
  def consolidate(protocol, types) when is_atom(protocol) do
    with {:ok, ast_info, specs, compile_info} <- beam_protocol(protocol),
         {:ok, definitions} <- change_debug_info(protocol, ast_info, types),
         do: compile(definitions, specs, compile_info)
  end

  defp beam_protocol(protocol) do
    chunk_ids = [:debug_info, 'Docs', 'ExCk']
    opts = [:allow_missing_chunks]

    case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
      {:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
        {:debug_info_v1, _backend, {:elixir_v1, info, specs}} = debug_info
        %{attributes: attributes, definitions: definitions} = info
        chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
        chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)

        case attributes[:__protocol__] do
          [fallback_to_any: any] ->
            {:ok, {any, definitions}, specs, {info, chunks}}

          _ ->
            {:error, :not_a_protocol}
        end

      _ ->
        {:error, :no_beam_info}
    end
  end

  defp beam_file(module) when is_atom(module) do
    case :code.which(module) do
      [_ | _] = file -> file
      _ -> module
    end
  end

  # Change the debug information to the optimized
  # impl_for/1 dispatch version.
  defp change_debug_info(protocol, {any, definitions}, types) do
    types = if any, do: types, else: List.delete(types, Any)
    all = [Any] ++ for {_guard, mod} <- __built_in__(), do: mod
    structs = types -- all

    case List.keytake(definitions, {:__protocol__, 1}, 0) do
      {protocol_def, definitions} ->
        {impl_for, definitions} = List.keytake(definitions, {:impl_for, 1}, 0)
        {struct_impl_for, definitions} = List.keytake(definitions, {:struct_impl_for, 1}, 0)

        protocol_def = change_protocol(protocol_def, types)
        impl_for = change_impl_for(impl_for, protocol, types)
        struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)

        {:ok, [protocol_def, impl_for, struct_impl_for] ++ definitions}

      nil ->
        {:error, :not_a_protocol}
    end
  end

  defp change_protocol({_name, _kind, meta, clauses}, types) do
    clauses =
      Enum.map(clauses, fn
        {meta, [:consolidated?], [], _} -> {meta, [:consolidated?], [], true}
        {meta, [:impls], [], _} -> {meta, [:impls], [], {:consolidated, types}}
        clause -> clause
      end)

    {{:__protocol__, 1}, :def, meta, clauses}
  end

  defp change_impl_for({_name, _kind, meta, _clauses}, protocol, types) do
    fallback = if Any in types, do: load_impl(protocol, Any)
    line = meta[:line]

    clauses =
      for {guard, mod} <- __built_in__(),
          mod in types,
          do: built_in_clause_for(mod, guard, protocol, meta, line)

    struct_clause = struct_clause_for(meta, line)
    fallback_clause = fallback_clause_for(fallback, protocol, meta)
    clauses = [struct_clause] ++ clauses ++ [fallback_clause]

    {{:impl_for, 1}, :def, meta, clauses}
  end

  defp change_struct_impl_for({_name, _kind, meta, _clauses}, protocol, types, structs) do
    fallback = if Any in types, do: load_impl(protocol, Any)
    clauses = for struct <- structs, do: each_struct_clause_for(struct, protocol, meta)
    clauses = clauses ++ [fallback_clause_for(fallback, protocol, meta)]

    {{:struct_impl_for, 1}, :defp, meta, clauses}
  end

  defp built_in_clause_for(mod, guard, protocol, meta, line) do
    x = {:x, [line: line, version: -1], __MODULE__}
    guard = quote(line: line, do: :erlang.unquote(guard)(unquote(x)))
    body = load_impl(protocol, mod)
    {meta, [x], [guard], body}
  end

  defp struct_clause_for(meta, line) do
    x = {:x, [line: line, version: -1], __MODULE__}
    head = quote(line: line, do: %{__struct__: unquote(x)})
    guard = quote(line: line, do: :erlang.is_atom(unquote(x)))
    body = quote(line: line, do: struct_impl_for(unquote(x)))
    {meta, [head], [guard], body}
  end

  defp each_struct_clause_for(struct, protocol, meta) do
    {meta, [struct], [], load_impl(protocol, struct)}
  end

  defp fallback_clause_for(value, _protocol, meta) do
    {meta, [quote(do: _)], [], value}
  end

  defp load_impl(protocol, for) do
    Module.concat(protocol, for).__impl__(:target)
  end

  # Finally compile the module and emit its bytecode.
  defp compile(definitions, specs, {info, chunks}) do
    info = %{info | definitions: definitions}
    {:ok, :elixir_erl.consolidate(info, specs, chunks)}
  end

  ## Definition callbacks

  @doc false
  def __protocol__(name, do: block) do
    quote do
      defmodule unquote(name) do
        # We don't allow function definition inside protocols
        import Kernel,
          except: [
            def: 1,
            def: 2,
            defp: 1,
            defp: 2,
            defdelegate: 2,
            defexception: 1,
            defguard: 1,
            defguardp: 1,
            defmacro: 1,
            defmacro: 2,
            defmacrop: 1,
            defmacrop: 2
          ]

        # Import the new dsl that holds the new def
        import Protocol, only: [def: 1]

        # Compile with debug info for consolidation
        @compile :debug_info

        # Set up a clear slate to store defined functions
        @functions []
        @fallback_to_any false

        # Invoke the user given block
        _ = unquote(block)

        # Finalize expansion
        unquote(after_defprotocol())
      end
    end
  end

  defp after_defprotocol do
    quote bind_quoted: [built_in: __built_in__()] do
      any_impl_for =
        if @fallback_to_any do
          quote do: unquote(__MODULE__.Any).__impl__(:target)
        else
          nil
        end

      # Disable Dialyzer checks - before and after consolidation
      # the types could be more strict
      @dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}

      @doc false
      @spec impl_for(term) :: atom | nil
      Kernel.def(impl_for(data))

      # Define the implementation for structs.
      #
      # It simply delegates to struct_impl_for which is then
      # optimized during protocol consolidation.
      Kernel.def impl_for(%struct{}) do
        struct_impl_for(struct)
      end

      # Define the implementation for built-ins
      :lists.foreach(
        fn {guard, mod} ->
          target = Module.concat(__MODULE__, mod)

          Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
            try do
              unquote(target).__impl__(:target)
            rescue
              UndefinedFunctionError ->
                unquote(any_impl_for)
            end
          end
        end,
        built_in
      )

      # Define a catch-all impl_for/1 clause to pacify Dialyzer (since
      # destructuring opaque types is illegal, Dialyzer will think none of the
      # previous clauses matches opaque types, and without this clause, will
      # conclude that impl_for can't handle an opaque argument). This is a hack
      # since it relies on Dialyzer not being smart enough to conclude that all
      # opaque types will get the any_impl_for/0 implementation.
      Kernel.def impl_for(_) do
        unquote(any_impl_for)
      end

      @doc false
      @spec impl_for!(term) :: atom
      if any_impl_for do
        Kernel.def impl_for!(data) do
          impl_for(data)
        end
      else
        Kernel.def impl_for!(data) do
          impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
        end
      end

      # Internal handler for Structs
      Kernel.defp struct_impl_for(struct) do
        target = Module.concat(__MODULE__, struct)

        try do
          target.__impl__(:target)
        rescue
          UndefinedFunctionError ->
            unquote(any_impl_for)
        end
      end

      # Inline struct implementation for performance
      @compile {:inline, struct_impl_for: 1}

      unless Module.defines_type?(__MODULE__, {:t, 0}) do
        @type t :: term
      end

      # Store information as an attribute so it
      # can be read without loading the module.
      Module.register_attribute(__MODULE__, :__protocol__, persist: true)
      @__protocol__ [fallback_to_any: !!@fallback_to_any]

      @doc false
      @spec __protocol__(:module) :: __MODULE__
      @spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@functions))
      @spec __protocol__(:consolidated?) :: boolean
      @spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}
      Kernel.def(__protocol__(:module), do: __MODULE__)
      Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@functions)))
      Kernel.def(__protocol__(:consolidated?), do: false)
      Kernel.def(__protocol__(:impls), do: :not_consolidated)
    end
  end

  @doc false
  def __functions_spec__([]), do: []

  def __functions_spec__([head | tail]),
    do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)]

  @doc false
  def __impl__(protocol, opts) do
    do_defimpl(protocol, :lists.keysort(1, opts))
  end

  defp do_defimpl(protocol, do: block, for: for) when is_list(for) do
    for f <- for, do: do_defimpl(protocol, do: block, for: f)
  end

  defp do_defimpl(protocol, do: block, for: for) do
    # Unquote the implementation just later
    # when all variables will already be injected
    # into the module body.
    impl =
      quote unquote: false do
        @doc false
        @spec __impl__(:for) :: unquote(for)
        @spec __impl__(:target) :: __MODULE__
        @spec __impl__(:protocol) :: unquote(protocol)
        def __impl__(:for), do: unquote(for)
        def __impl__(:target), do: __MODULE__
        def __impl__(:protocol), do: unquote(protocol)
      end

    quote do
      protocol = unquote(protocol)
      for = unquote(for)
      name = Module.concat(protocol, for)

      Protocol.assert_protocol!(protocol)
      Protocol.__ensure_defimpl__(protocol, for, __ENV__)

      defmodule name do
        @behaviour protocol
        @protocol protocol
        @for for

        unquote(block)

        Module.register_attribute(__MODULE__, :__impl__, persist: true)
        @__impl__ [protocol: @protocol, for: @for]

        unquote(impl)
      end
    end
  end

  @doc false
  def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
    struct = Macro.struct!(for, env)

    foreach = fn
      proto when is_atom(proto) ->
        derive(proto, for, struct, [], env)

      {proto, opts} when is_atom(proto) ->
        derive(proto, for, struct, opts, env)
    end

    :lists.foreach(foreach, :lists.flatten(derives))

    :ok
  end

  defp derive(protocol, for, struct, opts, env) do
    extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
    assert_protocol!(protocol, extra)
    __ensure_defimpl__(protocol, for, env)
    assert_impl!(protocol, Any, extra)

    # Clean up variables from eval context
    env = :elixir_env.reset_vars(env)
    args = [for, struct, opts]
    impl = Module.concat(protocol, Any)

    :elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn mod, fun, args ->
      if function_exported?(mod, fun, length(args)) do
        apply(mod, fun, args)
      else
        quoted =
          quote do
            Module.register_attribute(__MODULE__, :__impl__, persist: true)
            @__impl__ [protocol: unquote(protocol), for: unquote(for)]

            @doc false
            @spec __impl__(:target) :: unquote(impl)
            @spec __impl__(:protocol) :: unquote(protocol)
            @spec __impl__(:for) :: unquote(for)
            def __impl__(:target), do: unquote(impl)
            def __impl__(:protocol), do: unquote(protocol)
            def __impl__(:for), do: unquote(for)
          end

        Module.create(Module.concat(protocol, for), quoted, Macro.Env.location(env))
      end
    end)
  end

  @doc false
  def __ensure_defimpl__(protocol, for, env) do
    if Protocol.consolidated?(protocol) do
      message =
        "the #{inspect(protocol)} protocol has already been consolidated, an " <>
          "implementation for #{inspect(for)} has no effect. If you want to " <>
          "implement protocols after compilation or during tests, check the " <>
          "\"Consolidation\" section in the Protocol module documentation"

      IO.warn(message, Macro.Env.stacktrace(env))
    end

    :ok
  end

  ## Helpers

  @doc false
  def __built_in__ do
    [
      is_tuple: Tuple,
      is_atom: Atom,
      is_list: List,
      is_map: Map,
      is_bitstring: BitString,
      is_integer: Integer,
      is_float: Float,
      is_function: Function,
      is_pid: PID,
      is_port: Port,
      is_reference: Reference
    ]
  end
end