Current section
Files
Jump to
Current section
Files
lib/mix/tasks.compile.domo_compiler.ex
defmodule Mix.Tasks.Compile.DomoCompiler do
@moduledoc false
use Mix.Task.Compiler
alias Domo.TypeEnsurerFactory.Alias
alias Domo.TypeEnsurerFactory.Cleaner
alias Domo.TypeEnsurerFactory.DependencyResolver
alias Domo.TypeEnsurerFactory.Generator
alias Domo.TypeEnsurerFactory.Error
alias Domo.TypeEnsurerFactory.ResolvePlanner
alias Domo.TypeEnsurerFactory.Resolver
alias Domo.MixProjectHelper
alias Mix.Task.Compiler.Diagnostic
@recursive true
@plan_manifest "type_resolving_plan.domo"
@types_manifest "resolved_types.domo"
@deps_manifest "modules_deps.domo"
@generated_code_directory "/domo_generated_code"
@impl true
def run(args) do
project = MixProjectHelper.global_stub() || Mix.Project
plan_path = manifest_path(project, :plan)
types_path = manifest_path(project, :types)
deps_path = manifest_path(project, :deps)
code_path = generated_code_path(project)
prev_ignore_module_conflict = Map.get(Code.compiler_options(), :ignore_module_conflict, false)
Code.compiler_options(ignore_module_conflict: true)
paths = {plan_path, types_path, deps_path, code_path}
verbose? = Enum.member?(args, "--verbose")
result = build_ensurer_modules(paths, verbose?)
Code.compiler_options(ignore_module_conflict: prev_ignore_module_conflict)
maybe_print_errors(result)
result
end
def generated_code_path(mix_project) do
Path.join(mix_project.build_path(), @generated_code_directory)
end
defp build_ensurer_modules(paths, verbose?) do
{plan_path, types_path, deps_path, code_path} = paths
Cleaner.rmdir_if_exists!(code_path)
stop_and_flush_planner(plan_path, verbose?)
with {:ok, {_modules, deps_warns}} <- recompile_depending_structs(deps_path),
stop_and_flush_planner(plan_path, verbose?),
:ok <- resolve_types(plan_path, types_path, deps_path),
{:ok, type_ensurer_paths} <- generate_type_ensurers(types_path, code_path),
{:ok, {modules, ens_warns}} <- compile_type_ensurers(type_ensurer_paths, verbose?) do
Cleaner.rm!([plan_path, types_path])
result = if(Enum.empty?(modules), do: :noop, else: :ok)
warnings = format_warnings([deps_warns, ens_warns])
{result, warnings}
else
{:error, {source, message}} ->
case message do
[%Error{compiler_module: Resolver, message: :no_plan}] -> {:noop, []}
{ex_errors, _ex_warns} -> {:error, Enum.map(ex_errors, &diagnostic(source, &1))}
errors when is_list(errors) -> {:error, Enum.map(errors, &diagnostic(&1))}
error -> {:error, [diagnostic(error)]}
end
end
end
defp stop_and_flush_planner(plan_path, verbose?) do
ResolvePlanner.ensure_flushed_and_stopped(plan_path, verbose?)
end
defp recompile_depending_structs(deps_path) do
case DependencyResolver.maybe_recompile_depending_structs(deps_path) do
{:ok, modules, warnings} -> {:ok, {modules, warnings}}
{:error, ex_errors, ex_warnings} -> {:error, {:deps, {ex_errors, ex_warnings}}}
{:error, message} -> {:error, {:deps, message}}
end
end
defp resolve_types(plan_path, types_path, deps_path) do
case Resolver.resolve(plan_path, types_path, deps_path) do
:ok -> :ok
{:error, message} -> {:error, {:resolve, message}}
end
end
defp generate_type_ensurers(types_path, code_path) do
case Generator.generate(types_path, code_path) do
{:ok, _type_ensurer_paths} = ok -> ok
{:error, message} -> {:error, {:generate, message}}
end
end
defp compile_type_ensurers(type_ensurer_paths, verbose?) do
if verbose? do
IO.write("Compile generated type ensurers:\n")
Enum.each(type_ensurer_paths, &IO.write(" #{&1}\n"))
end
case Generator.compile(type_ensurer_paths) do
{:ok, modules, te_warns} -> {:ok, {modules, te_warns}}
{:error, ex_errors, ex_warnings} -> {:error, {:compile, {ex_errors, ex_warnings}}}
{:error, message} -> {:error, {:compile, message}}
end
end
defp format_warnings(warns) do
warns
|> List.flatten()
|> Enum.map(fn {path, position, message} ->
%Diagnostic{
compiler_name: "elixir",
file: path,
position: position,
message: message,
severity: :warning
}
end)
end
defp diagnostic(%Error{compiler_module: Resolver, struct_module: nil} = error) do
message = """
#{module_to_string(error.compiler_module)} failed due to #{inspect(error.message)}.\
"""
diagnostic(error.file, message)
end
defp diagnostic(%Error{compiler_module: Resolver} = error) do
message = """
#{module_to_string(error.compiler_module)} failed to resolve fields type \
of the #{module_to_string(error.struct_module)} struct due to #{inspect(error.message)}.\
"""
diagnostic(error.file, message)
end
defp diagnostic(%Error{compiler_module: Generator} = error) do
message = """
#{module_to_string(error.compiler_module)} failed to generate \
TypeEnsurer module code due to #{inspect(error.message)}.\
"""
diagnostic(error.file, message)
end
defp diagnostic(:deps, {path, _line, error}) do
message = """
Elixir compiler launched by DependencyResolver failed due to \
#{error}\
"""
diagnostic(path, message)
end
defp diagnostic(:compile, {path, _line, error}) do
message = """
Elixir compiler failed to compile a TypeEnsurer module code due to \
#{error}\
"""
diagnostic(path, message)
end
defp diagnostic(file, message) do
%Diagnostic{
compiler_name: "domo",
file: file,
message: message,
position: 1,
severity: :error
}
end
defp module_to_string(module) do
{:__aliases__, [alias: false], module_parts} = Alias.atom_to_alias(module)
module_parts
|> Enum.map(&Atom.to_string(&1))
|> Enum.join(".")
end
defp maybe_print_errors({:error, diagnostics}) do
diagnostics
|> List.wrap()
|> Enum.each(&print_error(&1.file, &1.message))
end
defp maybe_print_errors(_), do: nil
defp print_error(file, reason) do
IO.write([
"\n== Type ensurer compilation error in file #{Path.relative_to_cwd(file)} ==\n",
["** ", reason, ?\n]
])
end
@impl true
def manifests, do: [@plan_manifest, @types_manifest]
def deps_manifest, do: @deps_manifest
def manifest_path(mix_project, manifest_kind) do
Path.join(mix_project.build_path(), manifest(manifest_kind))
end
defp manifest(kind) do
case kind do
:plan -> @plan_manifest
:types -> @types_manifest
:deps -> @deps_manifest
end
end
@impl true
def clean do
project = MixProjectHelper.global_stub() || Mix.Project
plan_path = manifest_path(project, :plan)
types_path = manifest_path(project, :types)
deps_path = manifest_path(project, :deps)
code_path = generated_code_path(project)
File.rm(plan_path)
File.rm(types_path)
File.rm(deps_path)
Cleaner.rmdir_if_exists!(code_path)
end
end