Current section
Files
Jump to
Current section
Files
lib/mix/tasks/frontier.check.ex
defmodule Mix.Tasks.Frontier.Check do
@shortdoc "Runs Frontier boundary checks on demand."
@moduledoc """
Runs boundary checks without requiring `:frontier` in your compilers list.
Forces a recompile with the tracer enabled, collects cross-module references,
and reports any violations.
$ mix frontier.check
$ mix frontier.check --warnings-as-errors
Use this when you don't want compile-time checking but still want to verify
boundaries in CI or before a PR.
## Options
* `--warnings-as-errors` - Exit with error status if any violations are found
"""
use Mix.Task
alias Frontier.Rules
alias Frontier.Store
alias Frontier.Tracer
alias Frontier.Warning
@impl Mix.Task
def run(argv) do
{opts, _rest, _errors} = OptionParser.parse(argv, strict: [warnings_as_errors: :boolean])
Store.init()
# Register tracer
tracers = Code.get_compiler_option(:tracers)
Code.put_compiler_option(:tracers, [Tracer | tracers])
# Force recompile to collect references
Mix.Task.run("compile", ["--force"])
# Unregister tracer
updated_tracers = Enum.reject(Code.get_compiler_option(:tracers), &(&1 == Tracer))
Code.put_compiler_option(:tracers, updated_tracers)
# Load state from compiled modules
Store.ensure_loaded()
# Check references
errors = check_references()
validation_errors = check_validation()
all_errors = errors ++ validation_errors
print_results(all_errors)
if all_errors != [] and Keyword.get(opts, :warnings_as_errors, false) do
System.halt(1)
end
end
defp check_references do
Store.references()
|> Enum.flat_map(&check_reference/1)
|> Enum.uniq_by(&{&1.file, &1.line, &1.message})
|> Enum.sort_by(&{&1.file, &1.line})
end
defp check_reference(reference) do
case Rules.check(reference.from, reference.to) do
:ok ->
check_external(reference)
{:violation, _type, _details} = violation ->
[to_error(violation, reference)]
end
end
defp check_external(reference) do
caller_context = Frontier.Classifier.owning_context(reference.from)
to_app = if caller_context, do: Application.get_application(reference.to)
own_app = Keyword.fetch!(Mix.Project.config(), :app)
if caller_context && to_app && to_app != own_app do
case Rules.check_external(caller_context, to_app) do
:ok -> []
{:violation, _type, _details} = violation -> [to_error(violation, reference)]
end
else
[]
end
end
defp check_validation do
Frontier.Validation.validate()
|> Enum.map(fn error ->
%{file: nil, line: 0, message: error.message}
end)
end
defp to_error(violation, reference) do
%{
file: reference.file,
line: reference.line,
message: Warning.format(violation)
}
end
defp print_results([]) do
Mix.shell().info([:green, "No boundary violations found."])
end
defp print_results(errors) do
Mix.shell().info("")
Enum.each(errors, fn error ->
pos = if is_integer(error.line) and error.line > 0, do: ":#{error.line}", else: ""
file = error.file || ""
Mix.shell().info([
:bright,
:yellow,
"warning: ",
:reset,
error.message,
"\n #{file}#{pos}\n"
])
end)
count = length(errors)
Mix.shell().info([
:red,
"Found #{count} boundary violation#{if count > 1, do: "s", else: ""}."
])
end
end