defmodule Mix.Tasks.Arch.Explore.Static do use Mix.Task alias Archeometer.Explore.Project @moduledoc """ Mix Task to run code AST exploration of modules, functions and macros and store its findings into a Archeometer database. Usage: mix arch.explore.static [options] The following options are accepted: * `--include-deps` - Wildcard glob style to filter dependencies For more information see: [Basic usage guide](guides/introduction/basic_usage.md#include-dependencies-in-the-analysis) """ @shortdoc "Runs static code exploration" @impl Mix.Task def run(argv) do case get_args(argv) do [ include_deps: deps_filter ] -> Credo.Application.start(nil, nil) %Credo.Execution{argv: []} |> Credo.Execution.ExecutionTiming.start_server() |> Credo.Execution.ExecutionConfigFiles.start_server() |> Credo.Execution.ExecutionSourceFiles.start_server() |> Credo.Execution.Task.AppendDefaultConfig.call([]) |> Credo.Execution.Task.ParseOptions.call([]) |> Credo.ConfigBuilder.parse() |> append_deps(deps_filter) |> Credo.CLI.Task.LoadAndValidateSourceFiles.call() |> Archeometer.Explore.Credo.SaveStatsTask.call(Project.test_paths(deps_filter)) |> Credo.Execution.get_exit_status() |> ok_status() {:validate_error, error_type} -> Mix.shell().error("Error: #{error_type}") {:error, error_type} {:error, error_type} -> Mix.shell().error("Error: #{error_type}") print_help() {:error, error_type} end end defp get_args(argv) do {opts, _args, invalid} = OptionParser.parse( argv, strict: [include_deps: :keep] ) case invalid do [] -> deps_filter = Keyword.get_values(opts, :include_deps) validate_options(deps_filter) _ -> {:error, :wrong_arguments} end end defp validate_options(deps_filter) do case Project.filter_deps(deps_filter) do [] -> {:validate_error, "Filter doesn't match any dependency"} _ -> [include_deps: deps_filter] end end defp print_help() do Mix.shell().info(""" Usage: mix arch.explore.static [opts] opts: --include-deps 'deps_filter' 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 defp append_deps(exec, deps_filter) do included_deps = Project.filter_deps(deps_filter) if included_deps do deps_dirs = Keyword.values(included_deps) include_suffixes = [ "lib/**/*.{ex,exs}", "src/", "test/**/*.{ex,exs}", "web/", "apps/*/lib/", "apps/*/src/", "apps/*/test/", "apps/*/web/" ] include_dirs = for dep_dir <- deps_dirs, include_suffix <- include_suffixes, do: dep_dir <> "/" <> include_suffix included = Map.get(exec.files, :included, []) ++ include_dirs excluded = [~r/\/_build\//, ~r/\/node_modules\//] ++ Enum.map(deps_dirs, &(&1 <> "/deps")) Map.update!(exec, :files, &%{&1 | included: included}) |> Map.update!(:files, &%{&1 | excluded: excluded}) else exec end end defp ok_status(0), do: :ok defp ok_status(other), do: {:error, other} end