Current section
Files
Jump to
Current section
Files
lib/mix/tasks/compile/frontier.ex
defmodule Mix.Tasks.Compile.Frontier do
@moduledoc """
Verifies cross-module function calls according to Frontier boundaries.
Add `:frontier` to your compilers in `mix.exs`:
def project do
[
compilers: [:frontier] ++ Mix.compilers(),
# ...
]
end
"""
use Mix.Task.Compiler
alias Frontier.Rules
alias Frontier.Store
alias Frontier.Warning
@recursive true
@impl Mix.Task.Compiler
def run(argv) do
{opts, _rest, _errors} = OptionParser.parse(argv, strict: [warnings_as_errors: :boolean])
Store.init()
Store.reset_initialized()
Mix.Task.Compiler.after_compiler(:elixir, &after_elixir_compiler/1)
Mix.Task.Compiler.after_compiler(:app, &after_app_compiler(&1, opts))
tracers = Code.get_compiler_option(:tracers)
Code.put_compiler_option(:tracers, [Frontier.Tracer | tracers])
{:ok, []}
end
defp after_elixir_compiler(outcome) do
tracers = Enum.reject(Code.get_compiler_option(:tracers), &(&1 == Frontier.Tracer))
Code.put_compiler_option(:tracers, tracers)
outcome
end
defp after_app_compiler(outcome, opts) do
with {status, diagnostics} when status in [:ok, :noop] <- outcome do
reference_errors = check_references()
validation_errors =
Frontier.Validation.validate()
|> Enum.map(fn error ->
%Mix.Task.Compiler.Diagnostic{
compiler_name: "frontier",
details: nil,
file: nil,
message: error.message,
position: 0,
severity: :warning
}
end)
errors = reference_errors ++ validation_errors
print_diagnostics(errors)
{status(errors, opts), diagnostics ++ errors}
end
end
defp check_references do
Store.references()
|> Enum.flat_map(&check_reference/1)
|> Enum.uniq_by(&{&1.file, &1.position, &1.message})
|> Enum.sort_by(&{&1.file, &1.position})
end
defp check_reference(reference) do
case Rules.check(reference.from, reference.to) do
:ok ->
check_external_reference(reference)
{:violation, _type, _details} = violation ->
[to_diagnostic(violation, reference)]
end
end
defp check_external_reference(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_diagnostic(violation, reference)]
end
else
[]
end
end
defp to_diagnostic(violation, reference) do
%Mix.Task.Compiler.Diagnostic{
compiler_name: "frontier",
details: nil,
file: reference.file,
message: Warning.format(violation),
position: reference.line,
severity: :warning
}
end
defp print_diagnostics([]), do: :ok
defp print_diagnostics(diagnostics) do
Mix.shell().info("")
Enum.each(diagnostics, fn diag ->
pos = if is_integer(diag.position) and diag.position > 0, do: ":#{diag.position}", else: ""
Mix.shell().info([
:bright,
:yellow,
"warning: ",
:reset,
diag.message,
"\n #{diag.file}#{pos}\n"
])
end)
end
defp status([], _opts), do: :ok
defp status([_ | _], opts) do
if Keyword.get(opts, :warnings_as_errors, false), do: :error, else: :ok
end
end