Remove test marked with @tag :skip

Remove test marked with @tag :skip

The test is implemented in typespec_tesst as an error.

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

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

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

  test "unused variable" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          def hello(arg), do: nil

          if true do
            user = :warning
          else
            :nothing
          end
        end
        """)
      end)

    assert output =~ "variable \"arg\" is unused"
    assert output =~ "variable \"user\" is unused"
  after
    purge(Sample)
  end

  test "unused variable in redefined function in different file" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          defmacro __using__(_) do
            quote location: :keep do
              def function(arg)
            end
          end
        end
        """)

        code = """
          defmodule RedefineSample do
            use Sample
            def function(var123), do: nil
          end
        """

        Code.eval_string(code, [], file: "redefine_sample.ex")
      end)

    assert output =~ "redefine_sample.ex:3"
    assert output =~ "variable \"var123\" is unused"
  after
    purge(Sample)
    purge(RedefineSample)
  end

  test "useless literal" do
    message = "code block contains unused literal \"oops\""

    assert capture_err(fn ->
             Code.eval_string("""
             "oops"
             :ok
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             fn ->
               "oops"
               :ok
             end
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             try do
               "oops"
               :ok
             after
               :ok
             end
             """)
           end) =~ message
  end

  test "useless attr" do
    message =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @foo 1
          @bar 1
          @foo

          def bar do
            @bar
            :ok
          end
        end
        """)
      end)

    assert message =~ "module attribute @foo in code block has no effect as it is never returned "
    assert message =~ "module attribute @bar in code block has no effect as it is never returned "
  after
    purge(Sample)
  end

  test "useless var" do
    message = "variable foo in code block has no effect as it is never returned "

    assert capture_err(fn ->
             Code.eval_string("""
             foo = 1
             foo
             :ok
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             fn ->
               foo = 1
               foo
               :ok
             end
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             try do
               foo = 1
               foo
               :ok
             after
               :ok
             end
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             node()
             :ok
             """)
           end) == ""
  end

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

  test "underscored variable on assign" do
    assert capture_err(fn ->
             Code.eval_string("""
              defmodule Sample do
               def fun(_var) do
                 _var + 1
               end
             end
             """)
           end) =~ "the underscored variable \"_var\" is used after being set"
  after
    purge(Sample)
  end

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

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

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

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

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

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

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

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

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

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

  test "unused import" do
    assert capture_err(fn ->
             Code.compile_string("""
             defmodule Sample do
               import :lists
               def a, do: nil
             end
             """)
           end) =~ "unused import :lists\n"

    assert capture_err(fn ->
             Code.compile_string("""
             import :lists
             """)
           end) =~ "unused import :lists\n"
  after
    purge(Sample)
  end

  test "unused import of one of the functions in :only" do
    output =
      capture_err(fn ->
        Code.compile_string("""
        defmodule Sample do
          import String, only: [upcase: 1, downcase: 1, trim: 1]
          def a, do: upcase("hello")
        end
        """)
      end)

    assert output =~ "unused import String.downcase/1"
    assert output =~ "unused import String.trim/1"
  after
    purge(Sample)
  end

  test "unused import of any of the functions in :only" do
    assert capture_err(fn ->
             Code.compile_string("""
             defmodule Sample do
               import String, only: [upcase: 1, downcase: 1]
               def a, do: nil
             end
             """)
           end) =~ "unused import String\n"
  after
    purge(Sample)
  end

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

  test "unused alias when also import" do
    assert capture_err(fn ->
             Code.compile_string("""
             defmodule Sample do
               alias :lists, as: List
               import MapSet
               new()
             end
             """)
           end) =~ "unused alias List"
  after
    purge(Sample)
  end

  test "unused inside dynamic module" do
    import List, only: [flatten: 1], warn: false

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

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

  # TODO: Remove this check once we depend only on 20
  if :erlang.system_info(:otp_release) >= '20' do
    test "duplicate map keys" do
      output =
        capture_err(fn ->
          defmodule DuplicateMapKeys do
            assert %{a: :b, a: :c} == %{a: :c}
            assert %{1 => 2, 1 => 3} == %{1 => 3}
          end
        end)

      assert output =~ "key :a will be overridden in map"
      assert output =~ "key 1 will be overridden in map"
    end
  end

  test "unused guard" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               def atom_case do
                 v = "bc"
                 case v do
                   _ when is_atom(v) -> :ok
                   _ -> :fail
                 end
               end
             end
             """)
           end) =~ "this check/guard will always yield the same result"
  after
    purge(Sample)
  end

  test "`length(list) == 0` in guard" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               def list_case do
                 v = []
                 case v do
                   _ when length(v) == 0 -> :ok
                   _ -> :fail
                 end
               end
             end
             """)
           end) =~
             "\"length(v) == 0\" is discouraged since it has to traverse the whole list to check if it is empty or not"
  after
    purge(Sample)
  end

  test "previous clause always matches" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               def binary_cond do
                 v = "bc"
                 cond do
                   is_binary(v) -> :bin
                   true -> :ok
                 end
               end
             end
             """)
           end) =~ "this clause cannot match because a previous clause at line 5 always matches"
  after
    purge(Sample)
  end

  test "empty clause" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               def hello
             end
             """)
           end) =~ "implementation not provided for predefined def hello/0"
  after
    purge(Sample1)
  end

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

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

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

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

  test "clause with defaults should be first" do
    assert capture_err(fn ->
             Code.eval_string(~S"""
             defmodule Sample do
               def hello(arg), do: nil
               def hello(arg \\ 0), do: nil
             end
             """)
           end) =~ "definitions with multiple clauses and default values require a header"
  after
    purge(Sample)
  end

  test "clauses with default should use fun head" do
    assert capture_err(fn ->
             Code.eval_string(~S"""
             defmodule Sample do
               def hello(arg \\ 0), do: nil
               def hello(arg), do: nil
             end
             """)
           end) =~ "definitions with multiple clauses and default values require a header"
  after
    purge(Sample)
  end

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

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

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

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

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

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

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

  test "undefined function for behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @callback foo :: term
             end

             defmodule Sample2 do
               @behaviour Sample1
             end
             """)
           end) =~
             "function foo/0 required by behaviour Sample1 is not implemented (in module Sample2)"
  after
    purge([Sample1, Sample2])
  end

  test "undefined macro for behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @macrocallback foo :: Macro.t
             end

             defmodule Sample2 do
               @behaviour Sample1
             end
             """)
           end) =~
             "macro foo/0 required by behaviour Sample1 is not implemented (in module Sample2)"
  after
    purge([Sample1, Sample2])
  end

  test "wrong kind for behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @callback foo :: term
             end

             defmodule Sample2 do
               @behaviour Sample1
               defmacro foo, do: :ok
             end
             """)
           end) =~
             "function foo/0 required by behaviour Sample1 was implemented as \"defmacro\" but should have been \"def\""
  after
    purge([Sample1, Sample2])
  end

  test "conflicting behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @callback foo :: term
             end

             defmodule Sample2 do
               @callback foo :: term
             end

             defmodule Sample3 do
               @behaviour Sample1
               @behaviour Sample2
             end
             """)
           end) =~
             "conflicting behaviours found. function foo/0 is required by Sample1 and Sample2 (in module Sample3)"
  after
    purge([Sample1, Sample2, Sample3])
  end

  test "undefined behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               @behaviour UndefinedBehaviour
             end
             """)
           end) =~ "module UndefinedBehaviour does not exist (in module Sample)"
  after
    purge(Sample)
  end

  test "empty behaviours" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule EmptyBehaviour do
             end
             defmodule Sample do
               @behaviour EmptyBehaviour
             end
             """)
           end) =~ "module EmptyBehaviour is not a behaviour (in module Sample)"
  after
    purge(Sample)
    purge(EmptyBehaviour)
  end

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

  test "undefined function for protocol" do
    assert capture_err(fn ->
             Code.eval_string("""
             defprotocol Sample1 do
               def foo(subject)
             end

             defimpl Sample1, for: Atom do
             end
             """)
           end) =~
             "function foo/1 required by protocol Sample1 is not implemented (in module Sample1.Atom)"
  after
    purge([Sample1, Sample1.Atom])
  end

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

  test "warning with overridden file" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @file "sample"
          def foo(x), do: :ok
        end
        """)
      end)

    assert output =~ "variable \"x\" is unused"
    assert output =~ "sample:3"
  after
    purge(Sample)
  end

  test "with and do clauses emit errors, else clauses do not" do
    assert capture_err(fn ->
             Code.compile_string("""
             with {:first, int} when is_integer(int) <- {:second, Integer.gcd(2, 4)} do
               int
             end
             """)
           end) =~ "this clause cannot match"

    assert capture_err(fn ->
             Code.compile_string("""
             with {:first, int1} when is_integer(int1) <- {:first, Integer.gcd(2, 4)},
                  {:second, int2} when is_integer(int2) <- {:second, Integer.gcd(2, 4)} do
               {:ok, int1 + int2}
             else
               {:first, nil} -> {:error, "first number is not integer"}
               {:second, nil} -> {:error, "second number is not integer"}
             end
             """)
           end) == ""
  after
    purge(Sample1)
    purge(Sample2)
  end

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

  test "duplicated docs" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @doc "Something"
          @doc "Another"
          def foo, do: :ok

          @doc false
          @doc "Doc"
          def bar, do: :ok
        end
        """)
      end)

    assert output =~ "redefining @doc attribute previously set at line 2"
    assert output =~ "nofile:3: Sample (module)"
    refute output =~ "nofile:7"
  after
    purge(Sample)
  end

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

  test "attribute with no use" do
    content =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @at "Something"
        end
        """)
      end)

    assert content =~ "module attribute @at was set but never used"
    assert content =~ "nofile:2"
  after
    purge(Sample)
  end

  test "typedoc with no type" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               @typedoc "Something"
             end
             """)
           end) =~ "module attribute @typedoc was set but no type follows it"
  after
    purge(Sample)
  end

  test "doc with no function" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               @doc "Something"
             end
             """)
           end) =~ "module attribute @doc was set but no definition follows it"
  after
    purge(Sample)
  end

  test "pipe without explicit parentheses" do
    assert capture_err(fn ->
             Code.eval_string("""
             [5, 6, 7, 3]
             |> Enum.map_join "", &(Integer.to_string(&1))
             |> String.to_integer
             """)
           end) =~ "parentheses are required when piping into a function call"
  end

  test "variable is being expanded to function call" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        self
        defmodule Sample do
          def my_node(), do: node
        end
        """)
      end)

    assert output =~ "variable \"self\" does not exist and is being expanded to \"self()\""
    assert output =~ "variable \"node\" does not exist and is being expanded to \"node()\""
  after
    purge(Sample)
  end

  defmodule User do
    defstruct [:name]
  end

  test ":__struct__ is ignored when using structs" do
    assert capture_err(fn ->
             code = """
             assert %Kernel.WarningTest.User{__struct__: Ignored, name: "joe"} ==
                    %Kernel.WarningTest.User{name: "joe"}
             """

             Code.eval_string(code, [], __ENV__)
           end) =~ "key :__struct__ is ignored when using structs"

    assert capture_err(fn ->
             code = """
             user = %Kernel.WarningTest.User{name: "meg"}
             assert %Kernel.WarningTest.User{user | __struct__: Ignored, name: "joe"} ==
                    %Kernel.WarningTest.User{__struct__: Kernel.WarningTest.User, name: "joe"}
             """

             Code.eval_string(code, [], __ENV__)
           end) =~ "key :__struct__ is ignored when using structs"
  end

  test "catch comes before rescue in try block" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        try do
          :trying
        catch
          _ -> :caught
        rescue
          _ -> :error
        end
        """)
      end)

    assert output =~ ~s("catch" should always come after "rescue" in try)
  end

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

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

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

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

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

  test "unused variable" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          def hello(arg), do: nil

          if true do
            user = :warning
          else
            :nothing
          end
        end
        """)
      end)

    assert output =~ "variable \"arg\" is unused"
    assert output =~ "variable \"user\" is unused"
  after
    purge(Sample)
  end

  test "unused variable in redefined function in different file" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          defmacro __using__(_) do
            quote location: :keep do
              def function(arg)
            end
          end
        end
        """)

        code = """
          defmodule RedefineSample do
            use Sample
            def function(var123), do: nil
          end
        """

        Code.eval_string(code, [], file: "redefine_sample.ex")
      end)

    assert output =~ "redefine_sample.ex:3"
    assert output =~ "variable \"var123\" is unused"
  after
    purge(Sample)
    purge(RedefineSample)
  end

  test "useless literal" do
    message = "code block contains unused literal \"oops\""

    assert capture_err(fn ->
             Code.eval_string("""
             "oops"
             :ok
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             fn ->
               "oops"
               :ok
             end
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             try do
               "oops"
               :ok
             after
               :ok
             end
             """)
           end) =~ message
  end

  test "useless attr" do
    message =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @foo 1
          @bar 1
          @foo

          def bar do
            @bar
            :ok
          end
        end
        """)
      end)

    assert message =~ "module attribute @foo in code block has no effect as it is never returned "
    assert message =~ "module attribute @bar in code block has no effect as it is never returned "
  after
    purge(Sample)
  end

  test "useless var" do
    message = "variable foo in code block has no effect as it is never returned "

    assert capture_err(fn ->
             Code.eval_string("""
             foo = 1
             foo
             :ok
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             fn ->
               foo = 1
               foo
               :ok
             end
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             try do
               foo = 1
               foo
               :ok
             after
               :ok
             end
             """)
           end) =~ message

    assert capture_err(fn ->
             Code.eval_string("""
             node()
             :ok
             """)
           end) == ""
  end

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

  test "underscored variable on assign" do
    assert capture_err(fn ->
             Code.eval_string("""
              defmodule Sample do
               def fun(_var) do
                 _var + 1
               end
             end
             """)
           end) =~ "the underscored variable \"_var\" is used after being set"
  after
    purge(Sample)
  end

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

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

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

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

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

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

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

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

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

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

  test "unused import" do
    assert capture_err(fn ->
             Code.compile_string("""
             defmodule Sample do
               import :lists
               def a, do: nil
             end
             """)
           end) =~ "unused import :lists\n"

    assert capture_err(fn ->
             Code.compile_string("""
             import :lists
             """)
           end) =~ "unused import :lists\n"
  after
    purge(Sample)
  end

  test "unused import of one of the functions in :only" do
    output =
      capture_err(fn ->
        Code.compile_string("""
        defmodule Sample do
          import String, only: [upcase: 1, downcase: 1, trim: 1]
          def a, do: upcase("hello")
        end
        """)
      end)

    assert output =~ "unused import String.downcase/1"
    assert output =~ "unused import String.trim/1"
  after
    purge(Sample)
  end

  test "unused import of any of the functions in :only" do
    assert capture_err(fn ->
             Code.compile_string("""
             defmodule Sample do
               import String, only: [upcase: 1, downcase: 1]
               def a, do: nil
             end
             """)
           end) =~ "unused import String\n"
  after
    purge(Sample)
  end

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

  test "unused alias when also import" do
    assert capture_err(fn ->
             Code.compile_string("""
             defmodule Sample do
               alias :lists, as: List
               import MapSet
               new()
             end
             """)
           end) =~ "unused alias List"
  after
    purge(Sample)
  end

  test "unused inside dynamic module" do
    import List, only: [flatten: 1], warn: false

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

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

  # TODO: Remove this check once we depend only on 20
  if :erlang.system_info(:otp_release) >= '20' do
    test "duplicate map keys" do
      output =
        capture_err(fn ->
          defmodule DuplicateMapKeys do
            assert %{a: :b, a: :c} == %{a: :c}
            assert %{1 => 2, 1 => 3} == %{1 => 3}
          end
        end)

      assert output =~ "key :a will be overridden in map"
      assert output =~ "key 1 will be overridden in map"
    end
  end

  test "unused guard" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               def atom_case do
                 v = "bc"
                 case v do
                   _ when is_atom(v) -> :ok
                   _ -> :fail
                 end
               end
             end
             """)
           end) =~ "this check/guard will always yield the same result"
  after
    purge(Sample)
  end

  test "`length(list) == 0` in guard" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               def list_case do
                 v = []
                 case v do
                   _ when length(v) == 0 -> :ok
                   _ -> :fail
                 end
               end
             end
             """)
           end) =~
             "\"length(v) == 0\" is discouraged since it has to traverse the whole list to check if it is empty or not"
  after
    purge(Sample)
  end

  test "previous clause always matches" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               def binary_cond do
                 v = "bc"
                 cond do
                   is_binary(v) -> :bin
                   true -> :ok
                 end
               end
             end
             """)
           end) =~ "this clause cannot match because a previous clause at line 5 always matches"
  after
    purge(Sample)
  end

  test "empty clause" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               def hello
             end
             """)
           end) =~ "implementation not provided for predefined def hello/0"
  after
    purge(Sample1)
  end

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

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

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

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

  test "clause with defaults should be first" do
    assert capture_err(fn ->
             Code.eval_string(~S"""
             defmodule Sample do
               def hello(arg), do: nil
               def hello(arg \\ 0), do: nil
             end
             """)
           end) =~ "definitions with multiple clauses and default values require a header"
  after
    purge(Sample)
  end

  test "clauses with default should use fun head" do
    assert capture_err(fn ->
             Code.eval_string(~S"""
             defmodule Sample do
               def hello(arg \\ 0), do: nil
               def hello(arg), do: nil
             end
             """)
           end) =~ "definitions with multiple clauses and default values require a header"
  after
    purge(Sample)
  end

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

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

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

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

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

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

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

  test "undefined function for behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @callback foo :: term
             end

             defmodule Sample2 do
               @behaviour Sample1
             end
             """)
           end) =~
             "function foo/0 required by behaviour Sample1 is not implemented (in module Sample2)"
  after
    purge([Sample1, Sample2])
  end

  test "undefined macro for behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @macrocallback foo :: Macro.t
             end

             defmodule Sample2 do
               @behaviour Sample1
             end
             """)
           end) =~
             "macro foo/0 required by behaviour Sample1 is not implemented (in module Sample2)"
  after
    purge([Sample1, Sample2])
  end

  test "wrong kind for behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @callback foo :: term
             end

             defmodule Sample2 do
               @behaviour Sample1
               defmacro foo, do: :ok
             end
             """)
           end) =~
             "function foo/0 required by behaviour Sample1 was implemented as \"defmacro\" but should have been \"def\""
  after
    purge([Sample1, Sample2])
  end

  test "conflicting behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample1 do
               @callback foo :: term
             end

             defmodule Sample2 do
               @callback foo :: term
             end

             defmodule Sample3 do
               @behaviour Sample1
               @behaviour Sample2
             end
             """)
           end) =~
             "conflicting behaviours found. function foo/0 is required by Sample1 and Sample2 (in module Sample3)"
  after
    purge([Sample1, Sample2, Sample3])
  end

  test "undefined behaviour" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               @behaviour UndefinedBehaviour
             end
             """)
           end) =~ "module UndefinedBehaviour does not exist (in module Sample)"
  after
    purge(Sample)
  end

  test "empty behaviours" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule EmptyBehaviour do
             end
             defmodule Sample do
               @behaviour EmptyBehaviour
             end
             """)
           end) =~ "module EmptyBehaviour is not a behaviour (in module Sample)"
  after
    purge(Sample)
    purge(EmptyBehaviour)
  end

  @tag :skip
  test "warn on unknown optional callback" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule IllDefinedOptionalBehaviour do
               @callback foo() :: any
               @optional_callbacks foo: 1
             end
             """)
           end) =~
             "The foo/1 optional callback specified in \"IllDefinedOptionalBehaviour\" is not defined"
  after
    purge(IllDefinedOptionalBehaviour)
  end

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

  test "undefined function for protocol" do
    assert capture_err(fn ->
             Code.eval_string("""
             defprotocol Sample1 do
               def foo(subject)
             end

             defimpl Sample1, for: Atom do
             end
             """)
           end) =~
             "function foo/1 required by protocol Sample1 is not implemented (in module Sample1.Atom)"
  after
    purge([Sample1, Sample1.Atom])
  end

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

  test "warning with overridden file" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @file "sample"
          def foo(x), do: :ok
        end
        """)
      end)

    assert output =~ "variable \"x\" is unused"
    assert output =~ "sample:3"
  after
    purge(Sample)
  end

  test "with and do clauses emit errors, else clauses do not" do
    assert capture_err(fn ->
             Code.compile_string("""
             with {:first, int} when is_integer(int) <- {:second, Integer.gcd(2, 4)} do
               int
             end
             """)
           end) =~ "this clause cannot match"

    assert capture_err(fn ->
             Code.compile_string("""
             with {:first, int1} when is_integer(int1) <- {:first, Integer.gcd(2, 4)},
                  {:second, int2} when is_integer(int2) <- {:second, Integer.gcd(2, 4)} do
               {:ok, int1 + int2}
             else
               {:first, nil} -> {:error, "first number is not integer"}
               {:second, nil} -> {:error, "second number is not integer"}
             end
             """)
           end) == ""
  after
    purge(Sample1)
    purge(Sample2)
  end

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

  test "duplicated docs" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @doc "Something"
          @doc "Another"
          def foo, do: :ok

          @doc false
          @doc "Doc"
          def bar, do: :ok
        end
        """)
      end)

    assert output =~ "redefining @doc attribute previously set at line 2"
    assert output =~ "nofile:3: Sample (module)"
    refute output =~ "nofile:7"
  after
    purge(Sample)
  end

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

  test "attribute with no use" do
    content =
      capture_err(fn ->
        Code.eval_string("""
        defmodule Sample do
          @at "Something"
        end
        """)
      end)

    assert content =~ "module attribute @at was set but never used"
    assert content =~ "nofile:2"
  after
    purge(Sample)
  end

  test "typedoc with no type" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               @typedoc "Something"
             end
             """)
           end) =~ "module attribute @typedoc was set but no type follows it"
  after
    purge(Sample)
  end

  test "doc with no function" do
    assert capture_err(fn ->
             Code.eval_string("""
             defmodule Sample do
               @doc "Something"
             end
             """)
           end) =~ "module attribute @doc was set but no definition follows it"
  after
    purge(Sample)
  end

  test "pipe without explicit parentheses" do
    assert capture_err(fn ->
             Code.eval_string("""
             [5, 6, 7, 3]
             |> Enum.map_join "", &(Integer.to_string(&1))
             |> String.to_integer
             """)
           end) =~ "parentheses are required when piping into a function call"
  end

  test "variable is being expanded to function call" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        self
        defmodule Sample do
          def my_node(), do: node
        end
        """)
      end)

    assert output =~ "variable \"self\" does not exist and is being expanded to \"self()\""
    assert output =~ "variable \"node\" does not exist and is being expanded to \"node()\""
  after
    purge(Sample)
  end

  defmodule User do
    defstruct [:name]
  end

  test ":__struct__ is ignored when using structs" do
    assert capture_err(fn ->
             code = """
             assert %Kernel.WarningTest.User{__struct__: Ignored, name: "joe"} ==
                    %Kernel.WarningTest.User{name: "joe"}
             """

             Code.eval_string(code, [], __ENV__)
           end) =~ "key :__struct__ is ignored when using structs"

    assert capture_err(fn ->
             code = """
             user = %Kernel.WarningTest.User{name: "meg"}
             assert %Kernel.WarningTest.User{user | __struct__: Ignored, name: "joe"} ==
                    %Kernel.WarningTest.User{__struct__: Kernel.WarningTest.User, name: "joe"}
             """

             Code.eval_string(code, [], __ENV__)
           end) =~ "key :__struct__ is ignored when using structs"
  end

  test "catch comes before rescue in try block" do
    output =
      capture_err(fn ->
        Code.eval_string("""
        try do
          :trying
        catch
          _ -> :caught
        rescue
          _ -> :error
        end
        """)
      end)

    assert output =~ ~s("catch" should always come after "rescue" in try)
  end

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

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


Preserve even invalid form edits when adding/removing items

The edits were dropped from items that existed before the edit, because we used the `get_field` changeset helper to retrieve the associated items and it only applies valid changes to the data, dropping any invalid ones in the process.

defmodule JumubaseWeb.PerformanceLive.Edit do
  use Phoenix.LiveView
  import Jumubase.Gettext
  import JumubaseWeb.PerformanceController, only: [normalize_params: 1]
  import JumubaseWeb.PerformanceLive.Helpers
  alias Ecto.Changeset
  alias Jumubase.Foundation
  alias Jumubase.Showtime
  alias Jumubase.Showtime.{Appearance, Performance, Piece}
  alias JumubaseWeb.Router.Helpers, as: Routes

  def render(assigns) do
    JumubaseWeb.PerformanceView.render("live_form.html", assigns)
  end

  def mount(_params, assigns, socket) do
    {:ok, prepare(socket, assigns)}
  end

  def handle_event("change", %{"performance" => attrs}, socket) do
    {:noreply, change(socket, attrs)}
  end

  def handle_event("add-appearance", _, socket) do
    {:noreply, add_appearance(socket)}
  end

  def handle_event("remove-appearance", %{"index" => index}, socket) do
    {:noreply, remove_appearance(socket, parse_id(index))}
  end

  def handle_event("toggle-appearance-panel", %{"index" => index}, socket) do
    {:noreply, toggle_appearance_panel(socket, parse_id(index))}
  end

  def handle_event("add-piece", _, socket) do
    {:noreply, add_piece(socket)}
  end

  def handle_event("remove-piece", %{"index" => index}, socket) do
    {:noreply, remove_piece(socket, parse_id(index))}
  end

  def handle_event("toggle-piece-panel", %{"index" => index}, socket) do
    {:noreply, toggle_piece_panel(socket, parse_id(index))}
  end

  def handle_event("submit", %{"performance" => params}, socket) do
    contest = socket.assigns.contest
    performance = socket.assigns.performance

    params = normalize_params(params)

    case Showtime.update_performance(contest, performance, params) do
      {:ok, %Performance{}} ->
        {:noreply,
         socket
         |> put_flash(:success, gettext("Your changes to the registration were saved."))
         |> redirect(to: Routes.page_path(socket, :home))}

      {:error, changeset} ->
        {:noreply, handle_failed_submit(socket, changeset)}
    end
  end

  def prepare(
        socket,
        %{"contest_id" => c_id, "performance_id" => p_id, "submit_title" => submit_title}
      ) do
    contest = Foundation.get_contest!(c_id) |> Foundation.load_contest_categories()
    performance = Showtime.get_performance!(contest, p_id)

    assign(socket,
      changeset: Showtime.change_performance(performance),
      contest: contest,
      performance: performance,
      expanded_appearance_index: nil,
      expanded_piece_index: nil,
      submit_title: submit_title
    )
  end

  def change(socket, attrs) do
    contest = socket.assigns.contest
    performance = socket.assigns.performance

    changeset =
      Performance.changeset(performance, attrs, contest.round)
      |> Map.put(:action, :update)

    assign(socket, changeset: changeset)
  end

  def add_appearance(socket) do
    {changeset, index} = add_item(socket.assigns.changeset, :appearances, %Appearance{})

    assign(socket,
      changeset: changeset,
      expanded_appearance_index: socket.assigns.expanded_appearance_index || index
    )
  end

  def remove_appearance(socket, index) do
    changeset = remove_item(socket.assigns.changeset, :appearances, index)
    assign(socket, changeset: changeset)
  end

  def add_piece(socket) do
    {changeset, index} = add_item(socket.assigns.changeset, :pieces, %Piece{})

    assign(socket,
      changeset: changeset,
      expanded_piece_index: socket.assigns.expanded_piece_index || index
    )
  end

  def remove_piece(socket, index) do
    changeset = remove_item(socket.assigns.changeset, :pieces, index)
    assign(socket, changeset: changeset)
  end

  # Private helpers

  defp add_item(changeset, field_name, item) do
    existing = get_assoc(changeset, field_name)
    changeset = changeset |> Changeset.put_assoc(field_name, existing ++ [item])
    {changeset, length(existing)}
  end

  defp remove_item(changeset, field_name, index) do
    remaining =
      changeset
      |> get_assoc(field_name)
      |> List.delete_at(index)

    Changeset.put_assoc(changeset, field_name, remaining)
  end

  defp get_assoc(changeset, field_name) do
    # This is used as a fallback if the changeset has no changes yet (e.g. right
    # after loading the edit form). Using `get_field` instead of `get_change` won't
    # work here, because it drops the user's "invalid" form edits that we want to keep.
    assoc_from_data = Map.get(changeset.data, field_name, [])

    changeset
    |> Changeset.get_change(field_name, assoc_from_data)
    |> exclude_obsolete()
  end

  defp exclude_obsolete(records_or_changesets) do
    records_or_changesets
    |> Enum.filter(fn
      %Changeset{action: action} -> action in [:insert, :update]
      _ -> true
    end)
  end
end

defmodule JumubaseWeb.PerformanceLive.Edit do
  use Phoenix.LiveView
  import Jumubase.Gettext
  import JumubaseWeb.PerformanceController, only: [normalize_params: 1]
  import JumubaseWeb.PerformanceLive.Helpers
  alias Ecto.Changeset
  alias Jumubase.Foundation
  alias Jumubase.Showtime
  alias Jumubase.Showtime.{Appearance, Performance, Piece}
  alias JumubaseWeb.Router.Helpers, as: Routes

  def render(assigns) do
    JumubaseWeb.PerformanceView.render("live_form.html", assigns)
  end

  def mount(_params, assigns, socket) do
    {:ok, prepare(socket, assigns)}
  end

  def handle_event("change", %{"performance" => attrs}, socket) do
    {:noreply, change(socket, attrs)}
  end

  def handle_event("add-appearance", _, socket) do
    {:noreply, add_appearance(socket)}
  end

  def handle_event("remove-appearance", %{"index" => index}, socket) do
    {:noreply, remove_appearance(socket, parse_id(index))}
  end

  def handle_event("toggle-appearance-panel", %{"index" => index}, socket) do
    {:noreply, toggle_appearance_panel(socket, parse_id(index))}
  end

  def handle_event("add-piece", _, socket) do
    {:noreply, add_piece(socket)}
  end

  def handle_event("remove-piece", %{"index" => index}, socket) do
    {:noreply, remove_piece(socket, parse_id(index))}
  end

  def handle_event("toggle-piece-panel", %{"index" => index}, socket) do
    {:noreply, toggle_piece_panel(socket, parse_id(index))}
  end

  def handle_event("submit", %{"performance" => params}, socket) do
    contest = socket.assigns.contest
    performance = socket.assigns.performance

    params = normalize_params(params)

    case Showtime.update_performance(contest, performance, params) do
      {:ok, %Performance{}} ->
        {:noreply,
         socket
         |> put_flash(:success, gettext("Your changes to the registration were saved."))
         |> redirect(to: Routes.page_path(socket, :home))}

      {:error, changeset} ->
        {:noreply, handle_failed_submit(socket, changeset)}
    end
  end

  def prepare(
        socket,
        %{"contest_id" => c_id, "performance_id" => p_id, "submit_title" => submit_title}
      ) do
    contest = Foundation.get_contest!(c_id) |> Foundation.load_contest_categories()
    performance = Showtime.get_performance!(contest, p_id)

    assign(socket,
      changeset: Showtime.change_performance(performance),
      contest: contest,
      performance: performance,
      expanded_appearance_index: nil,
      expanded_piece_index: nil,
      submit_title: submit_title
    )
  end

  def change(socket, attrs) do
    contest = socket.assigns.contest
    performance = socket.assigns.performance

    changeset =
      Performance.changeset(performance, attrs, contest.round)
      |> Map.put(:action, :update)

    assign(socket, changeset: changeset)
  end

  def add_appearance(socket) do
    {changeset, index} = add_item(socket.assigns.changeset, :appearances, %Appearance{})

    assign(socket,
      changeset: changeset,
      expanded_appearance_index: socket.assigns.expanded_appearance_index || index
    )
  end

  def remove_appearance(socket, index) do
    changeset = remove_item(socket.assigns.changeset, :appearances, index)
    assign(socket, changeset: changeset)
  end

  def add_piece(socket) do
    {changeset, index} = add_item(socket.assigns.changeset, :pieces, %Piece{})

    assign(socket,
      changeset: changeset,
      expanded_piece_index: socket.assigns.expanded_piece_index || index
    )
  end

  def remove_piece(socket, index) do
    changeset = remove_item(socket.assigns.changeset, :pieces, index)
    assign(socket, changeset: changeset)
  end

  # Private helpers

  defp add_item(changeset, field_name, item) do
    existing =
      changeset
      |> Changeset.get_field(field_name, [])
      |> exclude_obsolete()

    changeset = changeset |> Changeset.put_assoc(field_name, existing ++ [item])
    {changeset, length(existing)}
  end

  defp remove_item(changeset, field_name, index) do
    remaining =
      changeset
      |> Changeset.get_field(field_name, [])
      |> exclude_obsolete()
      |> List.delete_at(index)

    Changeset.put_assoc(changeset, field_name, remaining)
  end

  defp exclude_obsolete(records_or_changesets) do
    records_or_changesets
    |> Enum.filter(fn
      %Changeset{action: action} -> action in [:insert, :update]
      _ -> true
    end)
  end
end


Remove <wohnbundesland> field from exported XML

Based on JMDaten documentation, the possible field values are DSN, DSWM and DSOM – names of regions which strictly speaking no longer exist since the decision to migrate to more flexible "groupings" (Verbund) that are no longer based on geography and whose host memberships may change over the years. We therefore opt to omit the field in the export and leave the decision about what value to select in JMDaten to the person who imports the file.

defmodule JumubaseWeb.XMLEncoder do
  @moduledoc """
  A tool to export data as XML for use in other Jumu software,
  most notably JMDaten.
  """

  alias Jumubase.Showtime
  alias Jumubase.Showtime.{Appearance, Instruments, Performance, Piece}

  @doc """
  Encodes the list of performances to XML.
  """
  def encode(performances) do
    performances =
      performances
      |> Showtime.load_pieces()
      |> Showtime.load_predecessor_contests()

    {:jumu, nil, Enum.map(performances, &to_xml/1)} |> XmlBuilder.generate()
  end

  # Private helpers

  defp to_xml(%Performance{contest_category: cc} = p) do
    appearances = Performance.result_groups(p) |> List.flatten()

    for a <- appearances do
      other_appearances = appearances |> List.delete(a)

      {:teilnehmer, %{id: a.participant.id},
       [
         to_xml(a),
         {:rwkurz, nil, p.predecessor_contest.host.name},
         {:wertung, %{id: p.id},
          [
            {:type, nil, map_role(a.role)},
            {:instrument_stimmlage, nil, Instruments.name(a.instrument)},
            {:kategorie, nil, cc.category.bw_code}
          ]},
         {:spielpartner, nil,
          Enum.map(other_appearances, &{:partner, %{id: &1.participant.id}, to_xml(&1)})},
         {:programme, nil, Enum.map(p.pieces, &to_xml/1)}
       ]}
    end
  end

  defp to_xml(%Appearance{participant: p} = a) do
    [
      {:nachname, nil, p.family_name},
      {:vorname, nil, p.given_name},
      {:geburtstag, nil, format_date(p.birthdate)},
      {:tel, nil, p.phone},
      {:email, nil, p.email},
      {:instrument, nil, Instruments.name(a.instrument)}
    ]
  end

  defp to_xml(%Piece{} = pc) do
    {:programm, %{id: pc.id},
     [
       map_person(pc),
       {:geburtsjahr, nil, pc.composer_born},
       {:sterbejahr, nil, pc.composer_died},
       {:epoche, nil, pc.epoch},
       {:titel_opus, nil, pc.title},
       {:spieldauer, nil, format_duration(pc)}
     ]}
  end

  defp map_role("soloist"), do: "solo"
  defp map_role("ensemblist"), do: "gruppe"
  defp map_role("accompanist"), do: "begleiter"

  defp map_person(%Piece{artist: nil, composer: composer}) do
    {first_names, [last_name]} = String.split(composer) |> Enum.split(-1)

    [
      {:komponist_nachname, nil, last_name},
      {:komponist_vorname1, nil, Enum.at(first_names, 0)},
      {:komponist_vorname2, nil, Enum.at(first_names, 1)},
      {:komponist_vorname3, nil, Enum.at(first_names, 2)}
    ]
  end

  defp map_person(%Piece{composer: nil, artist: artist}) do
    {:komponist_nachname, nil, artist}
  end

  defp format_date(date), do: Timex.format!(date, "{D}.{M}.{YYYY}")

  defp format_duration(%Piece{minutes: min, seconds: sec}) do
    "#{zero_pad(min)}:#{zero_pad(sec)}"
  end

  defp zero_pad(number) do
    number |> Integer.to_string() |> String.pad_leading(2, "0")
  end
end

defmodule JumubaseWeb.XMLEncoder do
  @moduledoc """
  A tool to export data as XML for use in other Jumu software,
  most notably JMDaten.
  """

  alias Jumubase.Showtime
  alias Jumubase.Showtime.{Appearance, Instruments, Performance, Piece}

  @doc """
  Encodes the list of performances to XML.
  """
  def encode(performances) do
    performances =
      performances
      |> Showtime.load_pieces()
      |> Showtime.load_predecessor_contests()

    {:jumu, nil, Enum.map(performances, &to_xml/1)} |> XmlBuilder.generate()
  end

  # Private helpers

  defp to_xml(%Performance{contest_category: cc} = p) do
    appearances = Performance.result_groups(p) |> List.flatten()

    for a <- appearances do
      other_appearances = appearances |> List.delete(a)

      {:teilnehmer, %{id: a.participant.id},
       [
         to_xml(a),
         {:rwkurz, nil, p.predecessor_contest.host.name},
         {:wertung, %{id: p.id},
          [
            {:type, nil, map_role(a.role)},
            {:instrument_stimmlage, nil, Instruments.name(a.instrument)},
            {:kategorie, nil, cc.category.bw_code}
          ]},
         {:spielpartner, nil,
          Enum.map(other_appearances, &{:partner, %{id: &1.participant.id}, to_xml(&1)})},
         {:programme, nil, Enum.map(p.pieces, &to_xml/1)}
       ]}
    end
  end

  defp to_xml(%Appearance{participant: p} = a) do
    [
      {:nachname, nil, p.family_name},
      {:vorname, nil, p.given_name},
      {:geburtstag, nil, format_date(p.birthdate)},
      {:wohnbundesland, nil, "DSN"},
      {:tel, nil, p.phone},
      {:email, nil, p.email},
      {:instrument, nil, Instruments.name(a.instrument)}
    ]
  end

  defp to_xml(%Piece{} = pc) do
    {:programm, %{id: pc.id},
     [
       map_person(pc),
       {:geburtsjahr, nil, pc.composer_born},
       {:sterbejahr, nil, pc.composer_died},
       {:epoche, nil, pc.epoch},
       {:titel_opus, nil, pc.title},
       {:spieldauer, nil, format_duration(pc)}
     ]}
  end

  defp map_role("soloist"), do: "solo"
  defp map_role("ensemblist"), do: "gruppe"
  defp map_role("accompanist"), do: "begleiter"

  defp map_person(%Piece{artist: nil, composer: composer}) do
    {first_names, [last_name]} = String.split(composer) |> Enum.split(-1)

    [
      {:komponist_nachname, nil, last_name},
      {:komponist_vorname1, nil, Enum.at(first_names, 0)},
      {:komponist_vorname2, nil, Enum.at(first_names, 1)},
      {:komponist_vorname3, nil, Enum.at(first_names, 2)}
    ]
  end

  defp map_person(%Piece{composer: nil, artist: artist}) do
    {:komponist_nachname, nil, artist}
  end

  defp format_date(date), do: Timex.format!(date, "{D}.{M}.{YYYY}")

  defp format_duration(%Piece{minutes: min, seconds: sec}) do
    "#{zero_pad(min)}:#{zero_pad(sec)}"
  end

  defp zero_pad(number) do
    number |> Integer.to_string() |> String.pad_leading(2, "0")
  end
end