Packages
finitomata
0.24.2
0.41.0
0.40.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.30.3
0.30.2
0.30.1
0.30.0
0.29.10
0.29.9
0.29.8
0.29.7
0.29.6
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.4
0.26.3
0.26.2
0.26.1
0.26.0
0.25.0
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.7
0.23.6
0.23.5
0.23.4
0.23.3
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.1
0.15.0
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.1
0.1.0
The FSM implementation generated from PlantUML textual representation.
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/compile/finitomata.ex
defmodule Mix.Tasks.Compile.Finitomata do
# credo:disable-for-this-file Credo.Check.Readability.Specs
@moduledoc false
use Mix.Task.Compiler
alias Mix.Task.Compiler
alias Finitomata.{Hook, Mix.Events, Transition}
@preferred_cli_env :dev
@manifest_events "finitomata"
@impl Compiler
def run(argv) do
case Events.start_link() do
{:ok, _pid} ->
:ok
{:error, {:already_started, _pid}} ->
# IO.warn("`Events` process has been attempted to start twice")
:ok
{:error, error} ->
raise CompileError,
description: "Cannot start the `Events` process, error: " <> inspect(error)
end
Compiler.after_compiler(:app, &after_compiler(&1, argv))
tracers = Code.get_compiler_option(:tracers)
Code.put_compiler_option(:tracers, [__MODULE__ | tracers])
{:ok, []}
end
@doc false
@impl Compiler
def manifests, do: [manifest_path(@manifest_events)]
@doc false
@impl Compiler
def clean, do: if(GenServer.whereis(Events), do: Events.stop())
@doc false
def trace({remote, meta, Finitomata, :__using__, 1}, env)
when remote in ~w|remote_macro imported_macro|a do
Events.put(
:declarations,
struct(Finitomata.Hook,
env: env,
module: env.module,
kind: :defmacro,
fun: :__using__,
arity: 1,
args: meta
)
)
:ok
end
def trace({:remote_macro, _meta, Finitomata.Hook, :__before_compile__, 1}, %Macro.Env{
module: module
}) do
module
|> Events.hooks()
|> to_diagnostics(module |> Module.get_attribute(:__config__) |> Map.get(:fsm))
|> add_diagnostics()
|> amend_using_info(module)
end
def trace(_event, _env), do: :ok
@spec after_compiler({status, [Mix.Task.Compiler.Diagnostic.t()]}, any()) ::
{status, [Mix.Task.Compiler.Diagnostic.t()]}
when status: Mix.Task.Compiler.status()
defp after_compiler({status, diagnostics}, _argv) do
tracers = Enum.reject(Code.get_compiler_option(:tracers), &(&1 == __MODULE__))
Code.put_compiler_option(:tracers, tracers)
%{diagnostics: finitomata_diagnostics} = Events.all()
:ok = Events.stop()
{finitomata_status, _full, _added, _removed} = manifest(finitomata_diagnostics)
status =
case {status, finitomata_status} do
{:error, _} -> :error
{:noop, status} -> status
{:ok, _} -> :ok
end
{status, diagnostics ++ MapSet.to_list(finitomata_diagnostics)}
end
@spec diagnostic(message :: binary(), opts :: keyword()) :: Mix.Task.Compiler.Diagnostic.t()
defp diagnostic(message, opts) do
%Mix.Task.Compiler.Diagnostic{
compiler_name: "finitomata",
details: nil,
file: "unknown",
message: message,
position: nil,
severity: :information
}
|> Map.merge(Map.new(opts))
end
@type ambiguous ::
{Transition.state(), {Transition.event(), [Transition.state()]}}
@type disambiguated ::
{Transition.state(), {Transition.event(), [Transition.state()], Hook.t()}}
@type diagnostics :: %{
explicit: [disambiguated()],
partial: [disambiguated()],
implicit: [disambiguated()],
unhandled: [ambiguous()]
}
@spec to_diagnostics(Events.hooks(), [Transition.t()]) :: diagnostics()
defp to_diagnostics(hooks, fsm) do
declared = MapSet.to_list(hooks)
initial = %{explicit: [], partial: [], implicit: [], unhandled: []}
fsm
|> Transition.ambiguous()
|> Enum.reduce(initial, fn {from, {event, tos}}, acc ->
{declared, {from, {event, tos}}, acc}
add_diagnostic(declared, {from, {event, tos}}, acc)
end)
end
defmacrop v, do: {:{}, [], [{:_, [], nil}, {:_, [], nil}, nil]}
@spec add_diagnostic([Hook.t()], ambiguous(), diagnostics()) :: diagnostics()
defp add_diagnostic(hooks, {from, {event, tos}}, diagnostics) do
hooks
|> Enum.reduce_while(diagnostics, fn
%Hook{args: [^from, ^event, v(), v()]} = hook, acc ->
{:halt, %{acc | explicit: [{from, {event, tos, hook}} | acc.explicit]}}
%Hook{args: [^from, {e, _, _}, v(), v()], guards: []} = hook, acc when is_atom(e) ->
{:halt, %{acc | partial: [{from, {event, tos, hook}} | acc.partial]}}
%Hook{args: [{f, _, _}, ^event, v(), v()], guards: []} = hook, acc when is_atom(f) ->
{:halt, %{acc | partial: [{from, {event, tos, hook}} | acc.partial]}}
%Hook{args: [{f, _, _}, {e, _, _}, v(), v()], guards: []} = hook, acc
when is_atom(f) and is_atom(e) ->
{:halt, %{acc | implicit: [{from, {event, tos, hook}} | acc.implicit]}}
%Hook{args: args, guards: guards} = hook, acc ->
{:cont, cover(args, {from, {event, tos, hook}}, guards, acc)}
%Hook{}, acc ->
{:cont, acc}
end)
|> case do
^diagnostics -> %{diagnostics | unhandled: [{from, {event, tos}} | diagnostics.unhandled]}
diagnostics -> diagnostics
end
|> then(fn diagnostics ->
handled =
diagnostics
|> Map.take(~w|explicit partial implicit|a)
|> Map.values()
|> Enum.reduce(
[],
&(&2 ++ Enum.map(&1, fn {from, {event, tos, _hook}} -> {from, {event, tos}} end))
)
|> Enum.uniq()
unhandled =
diagnostics.unhandled
|> Enum.uniq()
|> Kernel.--(handled)
%{diagnostics | unhandled: unhandled}
end)
end
@spec add_diagnostics(diagnostics()) :: diagnostics()
defp add_diagnostics(%{explicit: _, partial: _, implicit: _} = diagnostics) do
diagnostics
|> Map.take(~w|explicit partial implicit|a)
|> Enum.each(fn {type, hooks} ->
Enum.each(hooks, fn
{_from, {_event, _tos, %Hook{} = hook}} = info ->
Events.put(
:diagnostics,
type
|> disambiguated_message(info)
|> diagnostic(
severity: disambiguated_security(type),
details: Hook.details(hook),
position: hook.env.line,
file: hook.env.file
)
)
end)
end)
diagnostics
end
@spec cover(
{Transition.state(), Transition.event()}
| [Hook.ast_tuple()],
{Transition.state(), {Transition.event(), [Transition.state()], Hook.t()}},
[Hook.ast_tuple()],
diagnostics()
) :: diagnostics()
defp cover({f, e}, {from, {event, tos, _} = event_tos_hook}, guards, acc)
when is_atom(f) and is_atom(e) do
case covered?({f, e}, {from, event}, guards) do
3 -> %{acc | explicit: [{from, event_tos_hook} | acc.explicit]}
2 -> %{acc | partial: [{from, event_tos_hook} | acc.partial]}
1 -> %{acc | implicit: [{from, event_tos_hook} | acc.implicit]}
0 -> %{acc | unhandled: [{from, {event, tos}} | acc.unhandled]}
end
end
defp cover([f, {e, _, _}, _, _], {from, event}, guards, acc) when is_atom(f),
do: cover({f, e}, {from, event}, guards, acc)
defp cover([{f, _, _}, e, _, _], {from, event}, guards, acc) when is_atom(e),
do: cover({f, e}, {from, event}, guards, acc)
defp cover([{f, _, _}, {e, _, _}, _, _], {from, event}, guards, acc),
do: cover({f, e}, {from, event}, guards, acc)
# TODO [AM] more cases with pattern matching etc
defp cover(_, _, _guards, acc), do: acc
@spec covered?(
{Transition.state(), Transition.event()},
{Transition.state(), Transition.event()},
[Hook.ast_tuple()]
) :: 0 | 1 | 2 | 3
defp covered?({f, e}, {from, event}, guards) do
guards
|> Macro.prewalk(0, fn
{:in, _, [{^f, _, _}, list]} = t, acc ->
{t, if(from in Macro.expand(list, __ENV__), do: Enum.max([acc, 2]), else: acc)}
{:in, _, [{^e, _, _}, list]} = t, acc ->
{t, if(event in Macro.expand(list, __ENV__), do: Enum.max([acc, 2]), else: acc)}
{:==, _, [{^f, _, _}, ^from]} = t, _acc ->
{t, 3}
{:==, _, [^from, {^f, _, _}]} = t, _acc ->
{t, 3}
{:==, _, [{^e, _, _}, ^event]} = t, _acc ->
{t, 3}
{:==, _, [^event, {^e, _, _}]} = t, _acc ->
{t, 3}
t, acc ->
{t, acc}
end)
|> elem(1)
end
@spec disambiguated_security(:explicit | :partial | :implicit) :: :hint | :info
defp disambiguated_security(:explicit), do: :hint
defp disambiguated_security(:partial), do: :hint
defp disambiguated_security(:implicit), do: :information
@spec disambiguated_message(:explicit | :partial | :implicit, disambiguated()) :: String.t()
defp disambiguated_message(:explicit, {from, {event, tos, %Hook{} = _hook}}) do
"Ambiguous transition " <>
inspect(%Transition{from: from, to: tos, event: event}) <>
" seems to be explicitly handled.\n" <>
"Make sure all possible target states are reachable!"
end
defp disambiguated_message(:partial, {from, {event, tos, %Hook{} = _hook}}) do
"Ambiguous transition " <>
inspect(%Transition{from: from, to: tos, event: event}) <>
" seems to be partially handled.\n" <>
"Make sure all possible target states are reachable!"
end
defp disambiguated_message(:implicit, {from, {event, tos, %Hook{} = _hook}}) do
"Ambiguous transition " <>
inspect(%Transition{from: from, to: tos, event: event}) <>
" seems to be implicitly handled.\n" <>
"Make sure all possible target states are reachable!"
end
@spec amend_using_info(diagnostics(), module()) :: :ok
defp amend_using_info(%{unhandled: []}, _module), do: :ok
defp amend_using_info(%{unhandled: unhandled}, module) do
module
|> Events.declaration()
|> case do
%Hook{} = hook ->
pos = if Keyword.keyword?(hook.args), do: Keyword.get(hook.args, :line, hook.env.line)
message =
[
"This FSM declaration contains ambiguous transitions which are not fully handled:"
| Enum.map(unhandled, fn {from, {event, tos}} ->
" " <>
inspect(%Transition{from: from, to: tos, event: event}) <>
" should be handled more strictly (have kinda catch-all clause)"
end)
]
|> Enum.join("\n")
Events.put(
:diagnostics,
diagnostic(message,
severity: :warning,
details: Hook.details(hook),
position: pos,
file: hook.env.file
)
)
nil ->
Mix.shell().info([
[:bright, :yellow, "warning: ", :reset],
"inconsistent FSM data collected from ",
[:bright, :blue, inspect(module), :reset]
])
end
end
@doc experimental: true, todo: true
@spec manifest(MapSet.t(Hook.t())) ::
{Mix.Task.Compiler.status(), MapSet.t(Hook.t()), MapSet.t(Hook.t()), MapSet.t(Hook.t())}
defp manifest(diagnostics) do
{full, added, removed} =
@manifest_events
|> read_manifest()
|> case do
nil ->
{diagnostics, diagnostics, []}
old ->
old_by_module = old |> Enum.filter(&File.exists?(&1.file)) |> Enum.group_by(& &1.file)
diagnostics_by_module = Enum.group_by(diagnostics, & &1.file)
full =
old_by_module
|> Map.merge(diagnostics_by_module)
|> Map.values()
|> List.flatten()
|> MapSet.new()
old =
old_by_module
|> Map.take(Map.keys(diagnostics_by_module))
|> Map.values()
|> List.flatten()
|> MapSet.new()
{full, MapSet.difference(diagnostics, old), MapSet.difference(old, diagnostics)}
end
write_manifest(@manifest_events, full)
status =
[added, removed]
|> Enum.map(fn diagnostics ->
diagnostics
|> Enum.filter(&match?(%Mix.Task.Compiler.Diagnostic{severity: :warning}, &1))
|> Enum.map(fn %Mix.Task.Compiler.Diagnostic{} = diagnostic ->
loc = Enum.join([Path.relative_to_cwd(diagnostic.file), diagnostic.position], ":")
{diagnostic.message, loc}
end)
end)
|> case do
[[], []] -> :noop
[_added, _removed] -> :ok
end
full
|> Enum.filter(&match?(%Mix.Task.Compiler.Diagnostic{severity: :warning}, &1))
|> Enum.each(fn %Mix.Task.Compiler.Diagnostic{} = diagnostic ->
loc = Enum.join([Path.relative_to_cwd(diagnostic.file), diagnostic.position], ":")
Mix.shell().info([
[:bright, :yellow, "warning: ", :reset],
diagnostic.message,
"\n ",
loc
])
end)
{status, full, added, removed}
end
@spec manifest_path(binary()) :: binary()
defp manifest_path(name),
do: Mix.Project.config() |> Mix.Project.manifest_path() |> Path.join("compile.#{name}")
@spec read_manifest(binary()) :: term()
defp read_manifest(name) do
unless Mix.Utils.stale?([Mix.Project.config_mtime()], [manifest_path(name)]) do
name
|> manifest_path()
|> File.read()
|> case do
{:ok, manifest} -> :erlang.binary_to_term(manifest)
_ -> nil
end
end
end
@spec write_manifest(binary(), term()) :: :ok
defp write_manifest(name, data) do
path = manifest_path(name)
File.mkdir_p!(Path.dirname(path))
File.write!(path, :erlang.term_to_binary(data))
end
end