Current section

Files

Jump to
igniter lib igniter project application.ex
Raw

lib/igniter/project/application.ex

defmodule Igniter.Project.Application do
@moduledoc "Codemods and tools for working with Application modules."
require Igniter.Code.Common
require Igniter.Code.Function
alias Igniter.Code.Common
alias Sourceror.Zipper
@doc "Returns the name of the current application."
@spec app_name() :: atom()
@deprecated "Use `app_name/1` instead."
def app_name do
Mix.Project.config()[:app]
end
@doc "Returns the name of the application."
@spec app_name(Igniter.t()) :: atom()
def app_name(igniter) do
zipper =
igniter
|> Igniter.include_existing_file("mix.exs")
|> Map.get(:rewrite)
|> Rewrite.source!("mix.exs")
|> Rewrite.Source.get(:quoted)
|> Sourceror.Zipper.zip()
with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :project, 0),
zipper <- Igniter.Code.Common.rightmost(zipper),
true <- Igniter.Code.List.list?(zipper),
{:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :app),
{:ok, app_name} when is_atom(app_name) <- expand_app_name(zipper) do
app_name
else
_ ->
raise """
Failed to parse the application name from mix.exs.
Please ensure that the `project` function in your mix.exs
file returns a keyword list with an `app` key.
"""
end
end
defp expand_app_name(zipper) do
case Common.expand_literal(zipper) do
:error ->
expand_attribute(zipper)
other ->
other
end
end
defp expand_attribute(zipper) do
with {:@, _, [{attr, _, nil}]} <- zipper.node,
{:ok, zipper} <- Common.find_prev(zipper, &match?({:@, _, [{^attr, _, [_]}]}, &1.node)) do
zipper
|> Zipper.down()
|> Zipper.down()
|> Common.expand_literal()
else
_ ->
:error
end
end
@doc "Returns the path of the application's priv directory."
@spec priv_dir(Igniter.t(), [String.t()]) :: String.t()
def priv_dir(igniter, subpath \\ []) do
igniter
|> app_name()
|> Application.app_dir(["priv"] ++ subpath)
end
@doc "Returns the name of the application module."
def app_module(igniter) do
zipper =
igniter
|> Igniter.include_existing_file("mix.exs")
|> Map.get(:rewrite)
|> Rewrite.source!("mix.exs")
|> Rewrite.Source.get(:quoted)
|> Sourceror.Zipper.zip()
with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :application, 0),
zipper <- Sourceror.Zipper.down(zipper),
zipper <- Igniter.Code.Common.rightmost(zipper),
true <- Igniter.Code.List.list?(zipper),
{:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :mod) do
case Igniter.Code.Common.expand_literal(zipper) do
{:ok, {app_module, _}} ->
app_module
{:ok, app_module} ->
app_module
:error ->
try do
case Code.eval_quoted(zipper.node) do
{{module, _}, _} -> module
{module, _} when is_atom(module) -> module
end
rescue
_ ->
reraise """
An application module was configured, but we could not determine its name, cannot continue.
""",
__STACKTRACE__
end
end
else
_ ->
nil
end
end
@doc """
Adds a new child to the `children` list in the application file
To pass quoted code as the options, use the following format:
{module, {:code, quoted_code}}
i.e
{MyApp.Supervisor, {:code, quote do
Application.fetch_env!(:app, :config)
end}}
## Options
- `:after` - A list of other modules that this supervisor should appear after,
or a function that takes a module and returns `true` if this module should be placed after it.
- `:opts_updater` - A function that takes the current options (second element of the child tuple),
and returns a new value. If the existing value of the module is not a tuple, the value passed into
your function will be `[]`. Your function *must* return `{:ok, zipper}` or
`{:error | :warning, "error_or_warning"}`.
- `:force?` - If `true`, forces adding a new child, even if an existing child uses the
same child module. Defaults to `false`.
## Ordering
We will put the new child as the earliest item in the list that we can, skipping any modules
in the `after` option.
## Examples
Given an application `start/2` that looks like this:
def start(_type, _args) do
children = [
ChildOne,
{ChildTwo, opt: 1}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
Add a new child that isn't currently present:
Igniter.Project.Application.add_new_child(igniter, NewChild)
# =>
children = [
NewChild,
ChildOne,
{ChildTwo, opt: 1}
]
Add a new child after some existing ones:
Igniter.Project.Application.add_new_child(igniter, NewChild, after: [ChildOne, ChildTwo])
# =>
children = [
ChildOne,
{ChildTwo, opt: 1},
NewChild
]
If the given child module is already present, `add_new_child/3` is a no-op by default:
Igniter.Project.Application.add_new_child(igniter, {ChildOne, opt: 1})
# =>
children = [
ChildOne,
{ChildTwo, opt: 1}
]
You can explicitly handle module conflicts by passing an `:opts_updater`:
Igniter.Project.Application.add_new_child(igniter, {ChildOne, opt: 1},
opts_updater: fn opts ->
{:ok, Sourceror.Zipper.replace(opts, [opt: 1])}
end
)
# =>
children = [
{ChildOne, opt: 1},
{ChildTwo, opt: 1}
]
Using `force?: true`, you can force a child to be added, even if the module
conflicts with an existing one:
Igniter.Project.Application.add_new_child(igniter, {ChildOne, opt: 1}, force?: true)
# =>
children = [
{ChildOne, opt: 1},
ChildOne,
{ChildTwo, opt: 1}
]
"""
@spec add_new_child(
Igniter.t(),
module() | {module, {:code, term()}} | {module, term()},
opts :: Keyword.t()
) ::
Igniter.t()
def add_new_child(igniter, to_supervise, opts \\ []) do
opts =
opts
|> Keyword.update(:after, fn _ -> false end, fn
fun when is_function(fun, 1) -> fun
list -> fn item -> item in List.wrap(list) end
end)
|> Keyword.put_new(:force?, false)
to_perform =
case app_module(igniter) do
nil -> {:create_an_app, Igniter.Project.Module.module_name(igniter, "Application")}
{mod, _} -> {:modify, mod}
mod -> {:modify, mod}
end
case to_perform do
{:create_an_app, mod} ->
igniter
|> create_app(mod)
|> do_add_child(mod, to_supervise, opts)
{:modify, mod} ->
do_add_child(igniter, mod, to_supervise, opts)
end
end
def create_app(igniter, application) do
igniter
|> point_to_application_in_mix_exs(application)
|> create_application_file(application)
end
defp do_add_child(igniter, application, to_supervise, opts) do
to_supervise =
case to_supervise do
module when is_atom(module) -> module
{module, {:code, contents}} when is_atom(module) -> {module, contents}
{module, contents} -> {module, Macro.escape(contents)}
end
to_supervise_module =
case to_supervise do
{module, _} -> module
module -> module
end
Igniter.Project.Module.find_and_update_module!(igniter, application, fn zipper ->
with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :start, 2),
{:ok, zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:=,
[2],
fn call ->
Igniter.Code.Function.argument_matches_pattern?(
call,
0,
{:children, _, context} when is_atom(context)
) &&
Igniter.Code.Function.argument_matches_pattern?(call, 1, v when is_list(v))
end
),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1) do
if opts[:force?] do
insert_child(zipper, to_supervise, opts)
else
case Igniter.Code.List.move_to_list_item(zipper, fn item ->
case extract_child_module(item) do
{:ok, module} -> Igniter.Code.Common.nodes_equal?(module, to_supervise_module)
:error -> false
end
end) do
{:ok, zipper} ->
if updater = opts[:opts_updater] do
zipper =
if Igniter.Code.Tuple.tuple?(zipper) do
zipper
else
Zipper.replace(zipper, {zipper.node, []})
end
{:ok, zipper} = Igniter.Code.Tuple.tuple_elem(zipper, 1)
updater.(zipper)
else
{:ok, zipper}
end
:error ->
insert_child(zipper, to_supervise, opts)
end
end
else
_ ->
{:warning,
"""
Could not find a `children = [...]` assignment in the `start` function of the `#{application}` module.
Please ensure that #{inspect(to_supervise)} is added started by the application `#{application}` manually.
"""}
end
end)
end
defp insert_child(zipper, child, opts) do
zipper
|> Zipper.down()
|> then(fn zipper ->
case Zipper.down(zipper) do
nil ->
{:ok, Zipper.insert_child(zipper, child)}
zipper ->
zipper
|> skip_after(opts)
|> case do
{:after, zipper} ->
{:ok, Zipper.insert_right(zipper, child)}
{:before, zipper} ->
{:ok, Zipper.insert_left(zipper, child)}
end
end
end)
end
defp extract_child_module(zipper) do
if Igniter.Code.Tuple.tuple?(zipper) do
with {:ok, elem} <- Igniter.Code.Tuple.tuple_elem(zipper, 0) do
{:ok, Igniter.Code.Common.expand_alias(elem)}
end
else
{:ok, Igniter.Code.Common.expand_alias(zipper)}
end
end
def skip_after(zipper, opts) do
Igniter.Code.List.do_move_to_list_item(zipper, fn item ->
with {:is_tuple, true} <- {:is_tuple, Igniter.Code.Tuple.tuple?(item)},
{:ok, item} <- Igniter.Code.Tuple.tuple_elem(item, 0),
item <- Igniter.Code.Common.expand_alias(item),
module when is_atom(module) <- alias_to_mod(item.node),
true <- opts[:after].(module) do
true
else
{:is_tuple, false} ->
with item <- Igniter.Code.Common.expand_alias(item),
module when is_atom(module) <- alias_to_mod(item.node),
true <- opts[:after].(module) do
true
else
_ ->
false
end
_ ->
false
end
end)
|> case do
{:ok, zipper} ->
case Zipper.right(zipper) do
nil ->
{:after, zipper}
zipper ->
skip_after(zipper, Keyword.put(opts, :nested?, true))
end
:error ->
if Keyword.get(opts, :nested?) do
{:after, zipper}
else
{:before, zipper}
end
end
end
def create_application_file(igniter, application) do
path = Igniter.Project.Module.proper_location(igniter, application)
supervisor = Igniter.Project.Module.module_name(igniter, "Supervisor")
contents = """
defmodule #{inspect(application)} do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = []
opts = [strategy: :one_for_one, name: #{inspect(supervisor)}]
Supervisor.start_link(children, opts)
end
end
"""
Igniter.create_new_file(igniter, path, contents)
end
defp point_to_application_in_mix_exs(igniter, application) do
Igniter.update_elixir_file(igniter, "mix.exs", fn zipper ->
case Igniter.Code.Module.move_to_module_using(zipper, Mix.Project) do
{:ok, zipper} ->
case Igniter.Code.Function.move_to_def(zipper, :application, 0) do
{:ok, zipper} ->
zipper
|> Igniter.Code.Common.rightmost()
|> Igniter.Code.Keyword.set_keyword_key(:mod, {application, []}, fn z ->
code =
{application, []}
|> Sourceror.to_string()
|> Sourceror.parse_string!()
{:ok, Common.replace_code(z, code)}
end)
_ ->
Common.add_code(zipper, """
def application do
[
mod: {#{inspect(application)}, []}
]
end
""")
end
_ ->
{:warning,
"""
No module in `mix.exs` using `Mix.Project` was found. Please update your `mix.exs`
to point to the application module `#{inspect(application)}`.
For example:
def application do
[
mod: {#{inspect(application)}, []}
]
end
"""}
end
end)
end
defp alias_to_mod({:__aliases__, _, parts}), do: Module.concat(parts)
defp alias_to_mod(v) when is_atom(v), do: v
defp alias_to_mod(other), do: other
end