lib/mix/tasks/sonarqube.ex

defmodule Mix.Tasks.Sonarqube do
  @moduledoc """
  Converts Excoveralls JSON to the SonarQube XML.

  ## Usage

      mix sonarqube [--input INPUT_FILE] [--output OUTPUT_FILE]

  By default `INPUT_FILE` is `cover/excoveralls.json`
  and `OUTPUT_FILE` is `cover/sonarqube.xml`

  """

  @shortdoc "Convert Excoveralls JSON to the SonarQube XML"

  use Mix.Task

  def run(args) do
    args
    |> parse_options()
    |> do_run()
  end

  def parse_options(args) do
    {parsed, _, _} = OptionParser.parse(args, strict: [input: :string, output: :string])

    defaults = %{
      input: "cover/excoveralls.json",
      output: "cover/sonarqube.xml"
    }

    Map.merge(defaults, Enum.into(parsed, %{}))
  end

  def do_run(%{input: input_file, output: output_file}) do
    IO.puts("Convert '#{input_file}' to '#{output_file}'")

    xml =
      input_file
      |> File.read!()
      |> Jason.decode!(keys: :atoms)
      |> to_xml()

    output_file
    |> create_dir()
    |> File.write!(xml)
  end

  def create_dir(output_file) do
    output_file
    |> Path.dirname()
    |> File.mkdir_p()

    output_file
  end

  def to_xml(json) do
    stats = json.source_files

    files =
      Enum.map(stats, fn s ->
        lines =
          s.coverage
          |> Enum.with_index(1)
          |> Enum.map(fn
            {nil, _ln} -> nil
            {0, ln} -> {:lineToCover, %{lineNumber: ln, covered: false}, []}
            {_hits, ln} -> {:lineToCover, %{lineNumber: ln, covered: true}, []}
          end)
          |> Enum.filter(& &1)

        {:file, %{path: s.name}, lines}
      end)

    {:coverage, %{version: "1"}, files} |> XmlBuilder.generate()
  end
end