Skip to content

Commit

Permalink
improvement: support non-literal/non-standard deps lists
Browse files Browse the repository at this point in the history
  • Loading branch information
zachdaniel committed Feb 11, 2025
1 parent ce57f9c commit 543fbdd
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 57 deletions.
8 changes: 8 additions & 0 deletions lib/igniter/code/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ defmodule Igniter.Code.Common do
end
end

@doc "Returns true if the node represents a variable assignment"
@spec variable_assignment?(zipper :: Zipper.t(), name :: atom) :: boolean()
def variable_assignment?(%{node: {:=, _, [{name, _, ctx}, _]}}, name) when is_atom(ctx) do
true
end

def variable_assignment?(_, _), do: false

@doc """
Moves to the last node that matches the predicate.
Expand Down
171 changes: 118 additions & 53 deletions lib/igniter/project/deps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,26 @@ defmodule Igniter.Project.Deps do
add_dependency(igniter, name, version, opts)

{name, version, version_opts} ->
if Keyword.keyword?(version) do
add_dependency(igniter, name, version ++ version_opts, opts)
else
add_dependency(igniter, name, version, Keyword.put(opts, :dep_opts, version_opts))
end
new_igniter =
if Keyword.keyword?(version) do
add_dependency(igniter, name, version ++ version_opts, opts)
else
add_dependency(igniter, name, version, Keyword.put(opts, :dep_opts, version_opts))
end

new_igniter =
if Enum.count(igniter.issues) != Enum.count(new_igniter.issues) ||
Enum.count(igniter.warnings) != Enum.count(new_igniter.warnings) do
Igniter.assign(
new_igniter,
:failed_to_add_deps,
[name | igniter.assigns[:failed_to_add_deps] || []]
)
else
new_igniter
end

new_igniter

other ->
raise ArgumentError, "Invalid dependency: #{inspect(other)}"
Expand Down Expand Up @@ -197,73 +212,123 @@ defmodule Igniter.Project.Deps do
end

defp do_add_dependency(igniter, name, version, opts) do
error_tag =
if opts[:error?] do
:error
else
:warning
end

quoted =
if opts[:dep_opts] do
quote do
{unquote(name), unquote(version), unquote(opts[:dep_opts])}
end
else
{:__block__, [],
[
{{:__block__, [], [name]}, {:__block__, [], [version]}}
]}
end

igniter
|> Igniter.update_elixir_file("mix.exs", fn zipper ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, :deps, 0),
true <- Igniter.Code.List.list?(zipper) do
match =
Igniter.Code.List.move_to_list_item(zipper, fn zipper ->
if Igniter.Code.Tuple.tuple?(zipper) do
case Igniter.Code.Tuple.tuple_elem(zipper, 0) do
{:ok, first_elem} ->
Common.nodes_equal?(first_elem, name)

:error ->
false
end
{:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, :deps, 0) do
case igniter.assigns[:igniter_exs][:deps_location] || :last_list_literal do
{m, f, a} ->
with {:ok, zipper} <- apply(m, f, [a] ++ [igniter, zipper]) do
add_to_deps_list(zipper, name, quoted, opts)
else
_ ->
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
#{inspect(m)}.#{f}/#{Enum.count(a) + 2} did not find a deps location
Please add the dependency manually.
"""}
end
end)

quoted =
if opts[:dep_opts] do
quote do
{unquote(name), unquote(version), unquote(opts[:dep_opts])}
{:variable, name} ->
with {:ok, zipper} <-
Igniter.Code.Common.move_to(
zipper,
&Igniter.Code.Common.variable_assignment?(&1, name)
),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
true <- Igniter.Code.List.list?(zipper) do
add_to_deps_list(zipper, name, quoted, opts)
else
_ ->
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
`deps/0` does not contain an assignment of the `#{name}` variable to a literal list
Please add the dependency manually.
"""}
end
else
{:__block__, [],
[
{{:__block__, [], [name]}, {:__block__, [], [version]}}
]}
end

case match do
{:ok, zipper} ->
Igniter.Code.Common.replace_code(zipper, quoted)
:last_list_literal ->
zipper = Zipper.rightmost(zipper)

_ ->
if Keyword.get(opts, :append?, false) do
Igniter.Code.List.append_to_list(zipper, quoted)
if Igniter.Code.List.list?(zipper) do
add_to_deps_list(zipper, name, quoted, opts)
else
Igniter.Code.List.prepend_to_list(zipper, quoted)
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
`deps/0` does not end in a list literal that can be added to.
Please add the dependency manually.
"""}
end
end
else
_ ->
if opts[:error?] do
{:error,
"""
Could not add dependency #{inspect({name, version})}
`mix.exs` file does not contain a simple list of dependencies in a `deps/0` function.
Please add it manually and run the installer again.
"""}
else
{:warning,
[
"""
Could not add dependency #{inspect({name, version})}
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
`mix.exs` file does not contain a simple list of dependencies in a `deps/0` function.
`mix.exs` file does not contain a `deps/0` function.
Please add it manually.
"""
]}
end
Please add the dependency manually.
"""}
end
end)
end

defp add_to_deps_list(zipper, name, quoted, opts) do
match =
Igniter.Code.List.move_to_list_item(zipper, fn zipper ->
if Igniter.Code.Tuple.tuple?(zipper) do
case Igniter.Code.Tuple.tuple_elem(zipper, 0) do
{:ok, first_elem} ->
Common.nodes_equal?(first_elem, name)

:error ->
false
end
end
end)

case match do
{:ok, zipper} ->
Igniter.Code.Common.replace_code(zipper, quoted)

_ ->
if Keyword.get(opts, :append?, false) do
Igniter.Code.List.append_to_list(zipper, quoted)
else
Igniter.Code.List.prepend_to_list(zipper, quoted)
end
end
end

@doc false
def determine_dep_type_and_version(requirement) do
case String.split(requirement, "@", trim: true, parts: 2) do
Expand Down
39 changes: 39 additions & 0 deletions lib/igniter/project/igniter_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ defmodule Igniter.Project.IgniterConfig do
default: [],
doc: "A list of extensions to use in the project."
],
deps_location: [
type: {:or, [:last_list_literal, {:tagged_tuple, :variable, :atom}, :mfa]},
default: :last_list_literal,
doc: """
The strategy for finding the `deps` list to add new dependencies to, in your `deps/0` function in `mix.exs`
- `:last_list_literal` expects your deps function to return a literal list which will be prepended to
- `{:variable, :name}` expects to find an assignment from the given variable to a list literal, i.e `deps = [...]`, and prepends to that
- `:mfa` will call the given mfa with the igniter and the zipper within the `deps/0` function. It should return `{:ok, zipper}`
at the position where the dep should be prepended, or :error if the location could not be found.
"""
],
source_folders: [
type: {:list, :string},
default: ["lib", "test/support"],
Expand Down Expand Up @@ -114,6 +126,33 @@ defmodule Igniter.Project.IgniterConfig do
end)
end

def configure(igniter, key, value) do
value =
value
|> Macro.escape()
|> Sourceror.to_string()
|> Sourceror.parse_string!()

igniter
|> setup()
|> Igniter.update_elixir_file(".igniter.exs", fn zipper ->
rightmost = Igniter.Code.Common.rightmost(zipper)

if Igniter.Code.List.list?(rightmost) do
Igniter.Code.Keyword.set_keyword_key(
zipper,
key,
[value],
fn zipper ->
{:ok, Igniter.Code.Common.replace_code(zipper, value)}
end
)
else
{:warning, "Failed to modify `.igniter.exs` when configuring #{inspect(key)}"}
end
end)
end

def dont_move_file_pattern(igniter, pattern) do
quoted =
case pattern do
Expand Down
9 changes: 7 additions & 2 deletions lib/igniter/util/info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,15 @@ defmodule Igniter.Util.Info do

defp add_deps(igniter, add_deps, opts) do
Enum.reduce(add_deps, igniter, fn dependency, igniter ->
with {_, _, dep_opts} <- dependency,
with {name, _, dep_opts} <- dependency,
only when not is_nil(only) <- dep_opts[:only],
false <- Mix.env() in only do
Igniter.add_warning(igniter, """
igniter
|> Igniter.assign(
:failed_to_add_deps,
[name | igniter.assigns[:failed_to_add_deps] || []]
)
|> Igniter.add_warning("""
Dependency #{inspect(dependency)} could not be installed,
because it is configured to be installed with `only: #{inspect(only)}`.
Expand Down
17 changes: 17 additions & 0 deletions lib/igniter/util/install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ defmodule Igniter.Util.Install do
append?: Keyword.get(opts, :append?, false)
)

if igniter.assigns[:failed_to_add_deps] do
Mix.shell().error("""
Failed to add dependencies to the `mix.exs` file.
Igniter may not be able to find where to modify your `deps/0` function.
To address this, modify your `.igniter.exs` file's `deps_location` option.
If you don't yet have a `.igniter.exs`, run `mix igniter.setup`.
For more information, see:
https://hexdocs.pm/igniter/Igniter.Project.IgniterConfig.html
""")

exit({:shutdown, 1})
end

installing_names = Enum.join(installing, ", ")

igniter =
Expand Down
Loading

0 comments on commit 543fbdd

Please sign in to comment.