Packages
igniter
0.5.3
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.30
0.6.29
0.6.28
0.6.27
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.52
0.5.51
0.5.50
0.5.49
0.5.48
0.5.47
0.5.46
0.5.45
0.5.44
0.5.43
0.5.42
0.5.41
0.5.40
0.5.39
0.5.38
0.5.37
0.5.36
0.5.35
0.5.34
0.5.33
0.5.32
0.5.31
0.5.30
0.5.29
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.78
0.3.77
0.3.76
0.3.75
0.3.74
0.3.73
0.3.72
0.3.71
0.3.70
0.3.69
0.3.68
0.3.67
0.3.66
0.3.65
0.3.64
0.3.63
0.3.62
0.3.61
0.3.60
0.3.59
0.3.58
0.3.57
0.3.56
0.3.55
0.3.54
0.3.53
0.3.52
0.3.51
0.3.50
0.3.49
0.3.48
0.3.47
0.3.46
0.3.45
0.3.44
0.3.43
0.3.42
0.3.41
0.3.40
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A code generation and project patching framework
Current section
Files
Jump to
Current section
Files
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
path = Igniter.Project.Module.proper_location(igniter, application, :source_folder)
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.update_elixir_file(igniter, path, fn zipper ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_module_using(zipper, Application),
{: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 ->
Zipper.insert_child(zipper, child)
zipper ->
zipper
|> skip_after(opts)
|> case do
{:after, zipper} ->
Zipper.insert_right(zipper, child)
{:before, zipper} ->
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