lib/zig/compile_error.ex

defmodule Zig.CompileError do
  defexception [:command, :code, :error]

  alias Zig.Command

  def message(error) do
    "zig command failed: #{error.command} failed with error #{error.code}: #{error.error}"
  end

  def resolve(error, %{zig_code_path: zig_code_path, manifest_module: manifest_module}) do
    {lines, file_line} =
      error.error
      |> Command.split_on_newline()
      |> Enum.reduce({[], nil}, fn
        error_line, {so_far, nil} ->
          {maybe_line, fileline} = revise_line(error_line, so_far, zig_code_path, manifest_module)

          case List.flatten(maybe_line) do
            [str1, str2 | rest] when is_binary(str1) and is_binary(str2) ->
              {[IO.iodata_to_binary(rest)], fileline}

            _ ->
              {maybe_line, fileline}
          end

        error_line, {so_far, fileline} ->
          {next, _} = revise_line(error_line, so_far, zig_code_path, manifest_module)
          {next, fileline}
      end)

    error =
      lines
      |> Enum.reverse()
      |> IO.iodata_to_binary()
      |> String.trim()

    case file_line do
      nil ->
        %CompileError{description: error}

      {file, line} ->
        %CompileError{description: error, file: file, line: line}
    end
  end

  defp revise_line(error_line, acc, absolute_path, manifest_module) do
    relative_path = Path.relative_to_cwd(absolute_path)

    {replaced_line, fileline} =
      parse_line(error_line, absolute_path, relative_path, manifest_module)

    {[replaced_line, Command.newline() | acc], fileline}
  end

  defp parse_line(error_line, absolute_path, relative_path, manifest_module) do
    cond do
      # if it's the first part of the error message, we must memoize the new file/line
      String.starts_with?(error_line, "#{absolute_path}:") ->
        linerest = String.trim_leading(error_line, "#{absolute_path}:")
        {new_file, new_line, rest} = do_resolution(linerest, absolute_path, manifest_module)

        {[
           new_file,
           colonline(new_line),
           rest
           |> just_replace(absolute_path, relative_path, manifest_module)
           |> remove_column()
         ], {new_file, new_line}}

      String.starts_with?(error_line, "#{relative_path}:") ->
        linerest = String.trim_leading(error_line, "#{relative_path}:")
        {new_file, new_line, rest} = do_resolution(linerest, absolute_path, manifest_module)

        {[
           new_file,
           colonline(new_line),
           rest
           |> just_replace(absolute_path, relative_path, manifest_module)
           |> remove_column()
         ], {new_file, new_line}}

      :else ->
        {just_replace(error_line, absolute_path, relative_path, manifest_module), nil}
    end
  end

  defp remove_column(charlist_line) do
    with ~c':' ++ rest <- charlist_line,
         {_, rest} <- Integer.parse("#{rest}"),
         ": " <> rest <- rest do
      rest
    else
      _ -> charlist_line
    end
  end

  defp colonline(line) do
    if line, do: ":#{line}", else: []
  end

  defp do_resolution(linerest, absolute_path, manifest_module) do
    with {lineno, rest} <- Integer.parse(linerest),
         {file, line} <-
           manifest_module.__resolve(%{file_name: absolute_path, line: lineno}) do
      {file, line, rest}
    else
      _ ->
        {file, _} = manifest_module.__resolve(%{file_name: absolute_path, line: 1})
        {file, nil, linerest}
    end
  end

  defp just_replace("", _, _, _), do: []

  defp just_replace(error_rest, absolute_path, relative_path, manifest_module) do
    cond do
      # if it's the first part of the error message, we must memoize the new file/line
      String.starts_with?(error_rest, "#{absolute_path}:") ->
        linerest = String.trim_leading(error_rest, "#{absolute_path}:")
        {new_file, new_line, rest} = do_resolution(linerest, absolute_path, manifest_module)

        [
          new_file,
          colonline(new_line),
          just_replace(rest, absolute_path, relative_path, manifest_module)
        ]

      String.starts_with?(error_rest, "#{relative_path}:") ->
        linerest = String.trim_leading(error_rest, "#{relative_path}:")
        {new_file, new_line, rest} = do_resolution(linerest, absolute_path, manifest_module)

        [
          new_file,
          colonline(new_line),
          just_replace(rest, absolute_path, relative_path, manifest_module)
        ]

      :else ->
        <<one_char, rest::binary>> = error_rest
        [one_char | just_replace(rest, absolute_path, relative_path, manifest_module)]
    end
  end
end