defmodule Mix.Tasks.Arch.Collect.Static do
use Mix.Task
alias Archeometer.Collect.Project
@moduledoc """
Mix Task to run code AST analysis of modules, functions and macros
and store its findings into a Archeometer database.
Usage:
mix arch.collect.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 analysis"
@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.Collect.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.collect.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