lib/mix/tasks/archeometer.collect.ex

defmodule Mix.Tasks.Arch.Collect do
  use Mix.Task

  @moduledoc """
  Mix task to gather information about the project and store it into the database.
  It currently gathers information about:

  - Inventory of applications, modules, functions and macros.
  - Metrics related to them such as: size and complexity.
  - Information about test coverage.
  - Internal dependencies between modules.

  Usage:

    mix arch.collect [options]

  The following options are accepted:

    * `--no-coverage` avoid running tests and collecting coverage information
    * `--include-deps` wildcard glob style to filter dependencies
  """

  @shortdoc "Runs data collection tasks on current project"

  @impl Mix.Task
  def run(argv) do
    case validate_args(argv) do
      {:ok, _opts, _args} ->
        collect(
          argv -- ["--no-coverage", "coverage"],
          Enum.member?(argv, "--no-coverage")
        )

      {:error, error} ->
        Mix.shell().error("Error: #{error}")
        print_help()
        {:error, error}
    end
  end

  defp collect(argv, skip_coverage) do
    with :ok <- Mix.Task.rerun("arch.collect.static", argv),
         :ok <- Mix.Task.rerun("arch.collect.xrefs", argv),
         :ok <- Mix.Task.rerun("arch.collect.apps", argv),
         :ok <- Mix.Task.rerun("arch.collect.ecto", argv),
         :ok <- Mix.Task.rerun("arch.collect.credo_issues", argv),
         :ok <- explore_coverage(skip: skip_coverage) do
      Mix.shell().info("Data collection completed successfully!")
    else
      {:error, e} ->
        Mix.shell().error("Error: Could not run analysis: #{e}")
    end
  end

  defp explore_coverage(skip: true), do: :ok

  defp explore_coverage(skip: false) do
    # Coverage needs to be called as a command in order to set the mix testing environment
    Mix.Shell.cmd("mix arch.collect.coverage", [env: %{"MIX_ENV" => "test"}], &IO.puts/1)
    :ok
  end

  defp validate_args(argv) do
    {opts, args, invalid} =
      OptionParser.parse(
        argv,
        strict: [
          coverage: :boolean,
          include_deps: :keep
        ]
      )

    case invalid do
      [] ->
        {:ok, opts, args}

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

  defp print_help() do
    Mix.shell().info("""
    Usage: mix arch.collect [opts]

    opts: --include-deps 'deps_filter', --no-coverage

    Where `deps_filter` is a glob Unix-like pattern, matches dependencies name
    --include-deps can be used more than once.

    - `*` matches none or many tokens
    - `?` matches exactly one token
    - `[abc]` matches a set of tokens
    - `[a-z]` matches a range of tokens
    - `[!...]` matches anything but a set of tokens
    """)
  end
end