improvement: Support for extensions in igniter config

improvement: Add a phoenix extension to prevent moving modules that may be phoenix-y
fix: reevaluate .igniter.exs when it changes
This commit is contained in:
Zach Daniel 2024-09-13 17:50:19 -04:00
parent 9994dcf5be
commit c8b35915a5
21 changed files with 1260 additions and 529 deletions

View file

@ -86,7 +86,7 @@
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
{Credo.Check.Design.TagTODO, false},
{Credo.Check.Design.TagFIXME, []},
#

View file

@ -15,7 +15,3 @@ if Mix.env() == :dev do
],
version_tag_prefix: "v"
end
if Mix.env() == :test do
config :igniter, :testing?, true
end

View file

@ -167,7 +167,7 @@ defmodule Igniter do
if igniter.assigns[:test_mode?] do
igniter.assigns[:test_files]
|> Map.keys()
|> Enum.filter(&GlobEx.match?(glob, &1))
|> Enum.filter(&GlobEx.match?(glob, Path.expand(&1)))
else
glob
|> GlobEx.ls()
@ -705,19 +705,17 @@ defmodule Igniter do
end
@doc "This function stores in the igniter if its been run before, so it is only run once, which is expensive."
if Application.compile_env(:igniter, :testing?, false) do
def include_all_elixir_files(igniter) do
def include_all_elixir_files(igniter) do
if igniter.assigns[:private][:included_all_elixir_files?] do
igniter
end
else
def include_all_elixir_files(igniter) do
if igniter.assigns[:private][:included_all_elixir_files?] do
igniter
else
igniter
|> include_glob("{lib,test,config}/**/*.{ex,exs}")
|> assign_private(:included_all_elixir_files?, true)
end
else
igniter
|> Igniter.Project.IgniterConfig.get(:source_folders)
|> Enum.reduce(igniter, fn source_folder, igniter ->
include_glob(igniter, Path.join(source_folder, "/**/*.{ex,exs}"))
end)
|> include_glob("{test,config}/**/*.{ex,exs}")
|> assign_private(:included_all_elixir_files?, true)
end
end
@ -992,7 +990,7 @@ defmodule Igniter do
|> Mix.shell().info()
end
defp format(igniter, adding_paths \\ nil) do
defp format(igniter, adding_paths, reevaluate_igniter_config? \\ true) do
igniter =
igniter
|> include_existing_elixir_file("config/config.exs", require?: false)
@ -1000,7 +998,8 @@ defmodule Igniter do
if adding_paths &&
Enum.any?(List.wrap(adding_paths), &(Path.basename(&1) == ".formatter.exs")) do
format(igniter)
format(igniter, nil, false)
|> reevaluate_igniter_config(adding_paths, reevaluate_igniter_config?)
else
igniter =
"**/.formatter.exs"
@ -1077,9 +1076,22 @@ defmodule Igniter do
end)
%{igniter | rewrite: rewrite}
|> reevaluate_igniter_config(adding_paths, reevaluate_igniter_config?)
end
end
defp reevaluate_igniter_config(igniter, adding_paths, true) do
if is_nil(adding_paths) || ".igniter.exs" in List.wrap(adding_paths) do
parse_igniter_config(igniter)
else
igniter
end
end
defp reevaluate_igniter_config(igniter, _adding_paths, false) do
igniter
end
# for now we only eval `config.exs`
defp with_evaled_configs(rewrite, fun) do
[
@ -1331,7 +1343,7 @@ defmodule Igniter do
warnings: Enum.uniq(igniter.warnings),
tasks: Enum.uniq(igniter.tasks)
}
|> Igniter.Code.Module.move_files()
|> Igniter.Project.Module.move_files()
|> remove_unchanged_files()
|> then(fn igniter ->
if needs_test_support? do
@ -1363,7 +1375,20 @@ defmodule Igniter do
{:ok, source} ->
{igniter_exs, _} = Rewrite.Source.get(source, :quoted) |> Code.eval_quoted()
assign(igniter, :igniter_exs, igniter_exs)
assign(
igniter,
:igniter_exs,
Keyword.update(igniter_exs, :extensions, [], fn extensions ->
Enum.map(extensions, fn
{extension, opts} ->
{extension, opts}
extension ->
{extension, []}
end)
end)
)
end
end

View file

@ -102,7 +102,16 @@ defmodule Igniter.Code.Common do
"""
@spec expand_literal(Zipper.t()) :: {:ok, any()} | :error
def expand_literal(zipper) do
if Macro.quoted_literal?(zipper.node) do
quoted_literal? =
case zipper.node do
{:__block__, _, _} = value ->
!extendable_block?(value)
node ->
Macro.quoted_literal?(node)
end
if quoted_literal? do
{v, _} = Code.eval_quoted(zipper.node)
{:ok, v}
else

View file

@ -6,458 +6,6 @@ defmodule Igniter.Code.Module do
require Logger
@doc """
Finds a module and updates its contents wherever it is.
If the module does not yet exist, it is created with the provided contents. In that case,
the path is determined with `Igniter.Code.Module.proper_location/2`, but may optionally be overwritten with options below.
# Options
- `:path` - Path where to create the module, relative to the project root. Default: `nil` (uses `:kind` to determine the path).
"""
def find_and_update_or_create_module(igniter, module_name, contents, updater, opts \\ [])
def find_and_update_or_create_module(
igniter,
module_name,
contents,
updater,
opts
)
when is_list(opts) do
case find_and_update_module(igniter, module_name, updater) do
{:ok, igniter} ->
igniter
{:error, igniter} ->
create_module(igniter, module_name, contents, opts)
end
end
def find_and_update_or_create_module(
igniter,
module_name,
contents,
updater,
path
)
when is_binary(path) do
Logger.warning("You should use `opts` instead of `path` and pass `path` as a keyword.")
find_and_update_or_create_module(igniter, module_name, contents, updater, path: path)
end
@doc "Creates a new file & module in its appropriate location."
def create_module(igniter, module_name, contents, opts \\ []) do
contents =
"""
defmodule #{inspect(module_name)} do
#{contents}
end
"""
location =
case Keyword.get(opts, :path, nil) do
nil ->
proper_location(module_name)
path ->
path
end
Igniter.create_new_file(igniter, location, contents)
end
@doc "Checks if a module is defined somewhere in the project. The returned igniter should not be discarded."
def module_exists?(igniter, module_name) do
case find_module(igniter, module_name) do
{:ok, {igniter, _, _}} -> {true, igniter}
{:error, igniter} -> {false, igniter}
end
end
def find_and_update_module!(igniter, module_name, updater) do
case find_and_update_module(igniter, module_name, updater) do
{:ok, igniter} -> igniter
{:error, _igniter} -> raise "Could not find module #{inspect(module_name)}"
end
end
@doc "Finds a module and updates its contents. Returns `{:error, igniter}` if the module could not be found. Do not discard this igniter."
@spec find_and_update_module(Igniter.t(), module(), (Zipper.t() -> {:ok, Zipper.t()} | :error)) ::
{:ok, Igniter.t()} | {:error, Igniter.t()}
def find_and_update_module(igniter, module_name, updater) do
case find_module(igniter, module_name) do
{:ok, {igniter, source, zipper}} ->
case Common.move_to_do_block(zipper) do
{:ok, zipper} ->
case updater.(zipper) do
{:ok, zipper} ->
new_quoted =
zipper
|> Zipper.topmost()
|> Zipper.node()
new_source = Rewrite.Source.update(source, :quoted, new_quoted)
{:ok, %{igniter | rewrite: Rewrite.update!(igniter.rewrite, new_source)}}
{:error, error} ->
{:ok, Igniter.add_issue(igniter, error)}
{:warning, error} ->
{:ok, Igniter.add_warning(igniter, error)}
end
_ ->
{:error, igniter}
end
{:error, igniter} ->
{:error, igniter}
end
end
@doc """
Finds a module, returning a new igniter, and the source and zipper location. This new igniter should not be discarded.
In general, you should not use the returned source and zipper to update the module, instead, use this to interrogate
the contents or source in some way, and then call `find_and_update_module/3` with a function to perform an update.
"""
@spec find_module(Igniter.t(), module()) ::
{:ok, {Igniter.t(), Rewrite.Source.t(), Zipper.t()}} | {:error, Igniter.t()}
def find_module(igniter, module_name) do
igniter = Igniter.include_all_elixir_files(igniter)
igniter
|> Map.get(:rewrite)
|> Task.async_stream(
fn source ->
{source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
|> move_to_defmodule(module_name), source}
end,
timeout: :infinity
)
|> Enum.find_value({:error, igniter}, fn
{:ok, {{:ok, zipper}, source}} ->
{:ok, {igniter, source, zipper}}
_other ->
false
end)
end
@spec find_all_matching_modules(igniter :: Igniter.t(), (module(), Zipper.t() -> boolean)) ::
{Igniter.t(), [module()]}
def find_all_matching_modules(igniter, predicate) do
igniter =
igniter
|> Igniter.include_all_elixir_files()
matching_modules =
igniter
|> Map.get(:rewrite)
|> Enum.filter(&match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, &1))
|> Task.async_stream(
fn source ->
source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
|> Zipper.traverse([], fn zipper, acc ->
case zipper.node do
{:defmodule, _, [_, _]} ->
{:ok, mod_zipper} = Igniter.Code.Function.move_to_nth_argument(zipper, 0)
module_name =
mod_zipper
|> Igniter.Code.Common.expand_alias()
|> Zipper.node()
|> Igniter.Code.Module.to_module_name()
with module_name when not is_nil(module_name) <- module_name,
{:ok, do_zipper} <- Igniter.Code.Common.move_to_do_block(zipper),
true <- predicate.(module_name, do_zipper) do
{zipper, [module_name | acc]}
else
_ ->
{zipper, acc}
end
_ ->
{zipper, acc}
end
end)
|> elem(1)
end,
timeout: :infinity
)
|> Enum.flat_map(fn {:ok, v} ->
v
end)
|> Enum.uniq()
{igniter, matching_modules}
end
@doc "Given a suffix, returns a module name with the prefix of the current project."
@spec module_name(String.t()) :: module()
@deprecated "Use `module_name/2` instead."
def module_name(suffix) do
Module.concat(module_name_prefix(), suffix)
end
@doc "Given a suffix, returns a module name with the prefix of the current project."
@spec module_name(Igniter.t(), String.t()) :: module()
def module_name(igniter, suffix) do
Module.concat(module_name_prefix(igniter), suffix)
end
@doc """
Returns the idiomatic file location for a given module, starting with "lib/".
Examples:
iex> Igniter.Code.Module.proper_location(MyApp.Hello)
"lib/my_app/hello.ex"
"""
@spec proper_location(module(), source_folder :: String.t()) :: Path.t()
def proper_location(module_name, source_folder \\ "lib") do
do_proper_location(module_name, {:source_folder, source_folder})
end
@doc """
Returns the test file location for a given module, according to
`mix test` expectations, starting with "test/" and ending with "_test.exs".
Examples:
iex> Igniter.Code.Module.proper_test_location(MyApp.Hello)
"test/my_app/hello_test.exs"
iex> Igniter.Code.Module.proper_test_location(MyApp.HelloTest)
"test/my_app/hello_test.exs"
"""
@spec proper_test_location(module()) :: Path.t()
def proper_test_location(module_name) do
do_proper_location(module_name, :test)
end
@doc """
Returns the test support location for a given module, starting with
"test/support/" and dropping the module name prefix in the path.
Examples:
iex> Igniter.Code.Module.proper_test_support_location(MyApp.DataCase)
"test/support/data_case.ex"
"""
@spec proper_test_support_location(module()) :: Path.t()
def proper_test_support_location(module_name) do
do_proper_location(module_name, {:source_folder, "test/support"})
end
@doc false
def move_files(igniter, opts \\ []) do
module_location_config = Igniter.Project.IgniterConfig.get(igniter, :module_location)
dont_move_files = Igniter.Project.IgniterConfig.get(igniter, :dont_move_files)
igniter =
if opts[:move_all?] do
Igniter.include_all_elixir_files(igniter)
else
igniter
end
igniter.rewrite
|> Stream.filter(&(Path.extname(&1.path) in [".ex", ".exs"]))
|> Stream.reject(&non_movable_file?(&1.path, dont_move_files))
|> Enum.reduce(igniter, fn source, igniter ->
zipper =
source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
with {:ok, zipper} <- Igniter.Code.Module.move_to_defmodule(zipper),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0),
module <-
zipper
|> Igniter.Code.Common.expand_alias()
|> Zipper.node(),
module when not is_nil(module) <- to_module_name(module),
new_path when not is_nil(new_path) <-
should_move_file_to(igniter, source, module, module_location_config, opts) do
Igniter.move_file(igniter, source.path, new_path, error_if_exists?: false)
else
_ ->
igniter
end
end)
end
defp non_movable_file?(path, dont_move_files) do
Enum.any?(dont_move_files, fn
exclusion_pattern when is_binary(exclusion_pattern) ->
path == exclusion_pattern
exclusion_pattern when is_struct(exclusion_pattern, Regex) ->
Regex.match?(exclusion_pattern, path)
end)
end
defp should_move_file_to(igniter, source, module, module_location_config, opts) do
paths_created =
igniter.rewrite
|> Enum.filter(fn source ->
Rewrite.Source.from?(source, :string)
end)
|> Enum.map(& &1.path)
split_path =
source.path
|> Path.relative_to_cwd()
|> Path.split()
igniter
|> Igniter.Project.IgniterConfig.get(:source_folders)
|> Enum.filter(fn source_folder ->
List.starts_with?(split_path, Path.split(source_folder))
end)
|> Enum.max_by(
fn source_folder ->
source_folder
|> Path.split()
|> Enum.zip(split_path)
|> Enum.take_while(fn {l, r} -> l == r end)
|> Enum.count()
end,
fn -> nil end
)
|> case do
nil ->
if Enum.at(split_path, 0) == "test" &&
String.ends_with?(source.path, "_test.exs") do
{:ok, proper_test_location(module)}
else
:error
end
source_folder ->
{:ok, proper_location(module, source_folder)}
end
|> case do
:error ->
nil
{:ok, proper_location} ->
case module_location_config do
:inside_matching_folder ->
{[filename, folder], rest} =
proper_location
|> Path.split()
|> Enum.reverse()
|> Enum.split(2)
inside_matching_folder =
[filename, Path.rootname(filename), folder]
|> Enum.concat(rest)
|> Enum.reverse()
|> Path.join()
inside_matching_folder_dirname = Path.dirname(inside_matching_folder)
just_created_folder? =
Enum.any?(paths_created, fn path ->
List.starts_with?(Path.split(path), Path.split(inside_matching_folder_dirname))
end)
should_use_inside_matching_folder? =
if opts[:move_all?] do
dir?(igniter, inside_matching_folder_dirname) || just_created_folder?
else
source.path == proper_location(module) &&
!dir?(igniter, inside_matching_folder_dirname) && just_created_folder?
end
if should_use_inside_matching_folder? do
inside_matching_folder
else
proper_location
end
:outside_matching_folder ->
if opts[:move_all?] || Rewrite.Source.from?(source, :string) do
proper_location
end
end
end
end
defp dir?(igniter, folder) do
if igniter.assigns[:test_mode?] do
igniter.assigns[:test_files]
|> Map.keys()
|> Enum.any?(fn file_path ->
List.starts_with?(Path.split(file_path), Path.split(folder))
end)
else
File.dir?(folder)
end
end
@doc false
def to_module_name({:__aliases__, _, parts}), do: Module.concat(parts)
def to_module_name(value) when is_atom(value) and not is_nil(value), do: value
def to_module_name(_), do: nil
defp do_proper_location(module_name, kind) do
path =
module_name
|> Module.split()
|> case do
["Mix", "Tasks" | rest] ->
suffix =
rest
|> Enum.map(&to_string/1)
|> Enum.map_join(".", &Macro.underscore/1)
["mix", "tasks", suffix]
other ->
other
|> Enum.map(&to_string/1)
|> Enum.map(&Macro.underscore/1)
end
last = List.last(path)
leading = :lists.droplast(path)
case kind do
:test ->
if String.ends_with?(last, "_test") do
Path.join(["test" | leading] ++ ["#{last}.exs"])
else
Path.join(["test" | leading] ++ ["#{last}_test.exs"])
end
{:source_folder, "test/support"} ->
case leading do
[] ->
Path.join(["test/support", "#{last}.ex"])
[_prefix | leading_rest] ->
Path.join(["test/support" | leading_rest] ++ ["#{last}.ex"])
end
{:source_folder, source_folder} ->
Path.join([source_folder | leading] ++ ["#{last}.ex"])
end
end
def module?(zipper) do
Common.node_matches_pattern?(zipper, {:__aliases__, _, [_ | _]})
end
@doc "Parses a string into a module name"
@spec parse(String.t()) :: module()
def parse(module_name) do
@ -467,7 +15,7 @@ defmodule Igniter.Code.Module do
end
@doc "The module name prefix based on the mix project's module name"
@deprecated "Use `module_name_prefix/1` instead"
@deprecated "Use `Igniter.Project.Module.module_name_prefix/1` instead"
@spec module_name_prefix() :: module()
def module_name_prefix do
Mix.Project.get!()
@ -478,37 +26,9 @@ defmodule Igniter.Code.Module do
@doc "The module name prefix based on the mix project's module name"
@spec module_name_prefix(Igniter.t()) :: module()
# TODO: deprecate
def module_name_prefix(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.Module.move_to_defmodule(zipper),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0) do
case Igniter.Code.Common.expand_alias(zipper) do
%Zipper{node: module_name} when is_atom(module_name) ->
module_name
|> Module.split()
|> :lists.droplast()
|> Module.concat()
%Zipper{node: {:__aliases__, _, parts}} ->
parts
|> :lists.droplast()
|> Module.concat()
end
else
_ ->
raise """
Failed to parse the module name from mix.exs.
Please ensure that you are defining a Mix.Project module in your mix.exs file.
"""
end
Igniter.Project.Module.module_name_prefix(igniter)
end
@doc "Moves the zipper to a defmodule call"
@ -596,4 +116,157 @@ defmodule Igniter.Code.Module do
def move_to_def(zipper, fun, arity) do
Igniter.Code.Function.move_to_def(zipper, fun, arity)
end
def module?(zipper) do
Common.node_matches_pattern?(zipper, {:__aliases__, _, [_ | _]})
end
@doc """
Finds a module and updates its contents wherever it is.
If the module does not yet exist, it is created with the provided contents. In that case,
the path is determined with `Igniter.Code.Module.proper_location/2`, but may optionally be overwritten with options below.
# Options
- `:path` - Path where to create the module, relative to the project root. Default: `nil` (uses `:kind` to determine the path).
"""
# TODO: Deprecate
def find_and_update_or_create_module(igniter, module_name, contents, updater, opts \\ []) do
Igniter.Project.Module.find_and_update_or_create_module(
igniter,
module_name,
contents,
updater,
opts
)
end
@doc "Checks if the value is a module that matches a given predicate"
def module_matching?(zipper, pred) do
zipper =
zipper
|> Igniter.Code.Common.maybe_move_to_single_child_block()
|> Igniter.Code.Common.expand_aliases()
case zipper.node do
{:__aliases__, _, parts} ->
pred.(Module.concat(parts))
value when is_atom(value) ->
pred.(value)
_ ->
false
end
end
@doc "Creates a new file & module in its appropriate location."
# TODO: deprecate
def create_module(igniter, module_name, contents, opts \\ []) do
Igniter.Project.Module.create_module(igniter, module_name, contents, opts)
end
@doc "Checks if a module is defined somewhere in the project. The returned igniter should not be discarded."
# TODO: Deprecate
def module_exists?(igniter, module_name) do
Igniter.Project.Module.module_exists?(igniter, module_name)
end
# TODO: deprecate
def find_and_update_module!(igniter, module_name, updater) do
Igniter.Project.Module.find_and_update_module!(igniter, module_name, updater)
end
@doc "Finds a module and updates its contents. Returns `{:error, igniter}` if the module could not be found. Do not discard this igniter."
@spec find_and_update_module(Igniter.t(), module(), (Zipper.t() -> {:ok, Zipper.t()} | :error)) ::
{:ok, Igniter.t()} | {:error, Igniter.t()}
# TODO: deprecate
def find_and_update_module(igniter, module_name, updater) do
Igniter.Project.Module.find_and_update_module(igniter, module_name, updater)
end
@doc """
Finds a module, returning a new igniter, and the source and zipper location. This new igniter should not be discarded.
In general, you should not use the returned source and zipper to update the module, instead, use this to interrogate
the contents or source in some way, and then call `find_and_update_module/3` with a function to perform an update.
"""
@spec find_module(Igniter.t(), module()) ::
{:ok, {Igniter.t(), Rewrite.Source.t(), Zipper.t()}} | {:error, Igniter.t()}
# TODO deprecate
def find_module(igniter, module_name) do
Igniter.Project.Module.find_module(igniter, module_name)
end
@doc """
Finds a module, raising an error if its not found.
See `find_module/2` for more information.
"""
@spec find_module!(Igniter.t(), module()) ::
{Igniter.t(), Rewrite.Source.t(), Zipper.t()} | no_return
# TODO deprecate
def find_module!(igniter, module_name) do
Igniter.Project.Module.find_module!(igniter, module_name)
end
@spec find_all_matching_modules(igniter :: Igniter.t(), (module(), Zipper.t() -> boolean)) ::
{Igniter.t(), [module()]}
# TODO: deprecate
def find_all_matching_modules(igniter, predicate) do
Igniter.Project.Module.find_all_matching_modules(igniter, predicate)
end
@doc "Given a suffix, returns a module name with the prefix of the current project."
@spec module_name(String.t()) :: module()
@deprecated "Use `module_name/2` instead."
def module_name(suffix) do
Module.concat(module_name_prefix(), suffix)
end
@doc "Given a suffix, returns a module name with the prefix of the current project."
@spec module_name(Igniter.t(), String.t()) :: module()
# TODO: deprecate
def module_name(igniter, suffix) do
Igniter.Project.Module.module_name(igniter, suffix)
end
@doc """
Returns the idiomatic file location for a given module, starting with "lib/".
"""
@spec proper_location(module(), source_folder :: String.t()) :: Path.t()
@deprecated "Use `Igniter.Project.Module.proper_location/3`"
def proper_location(module_name, source_folder \\ "lib") do
Igniter.Project.Module.proper_location(
Igniter.new(),
module_name,
{:source_folder, source_folder}
)
end
@doc """
Returns the test file location for a given module, according to
`mix test` expectations, starting with "test/" and ending with "_test.exs".
"""
@spec proper_test_location(module()) :: Path.t()
@deprecated "Use `Igniter.Project.Module.proper_location/3`"
def proper_test_location(module_name) do
Igniter.Project.Module.proper_location(Igniter.new(), module_name, :test)
end
@doc """
Returns the test support location for a given module, starting with
"test/support/" and dropping the module name prefix in the path.
"""
@spec proper_test_support_location(module()) :: Path.t()
@deprecated "Use `Igniter.Project.Module.proper_location/3`"
def proper_test_support_location(module_name) do
Igniter.Project.Module.proper_location(
Igniter.new(),
module_name,
{:source_folder, "test/support"}
)
end
end

29
lib/igniter/extension.ex Normal file
View file

@ -0,0 +1,29 @@
defmodule Igniter.Extension do
@moduledoc """
Alter igniter's behavior by adding new functionality.
This is used to allow frameworks to modify things like
the conventional location of files.
"""
defmacro __using__(_) do
quote do
@behaviour Igniter.Extension
end
end
@doc """
Choose a proper location for any given module.
Possible return values:
- `{:ok, path}`: The path where the module should be located.
- `:error`: It should go in the default place, or according to other extensions.
- `:keep`: Keep the module in the same location, unless another extension has a place for it, or its just been created.
"""
@callback proper_location(
Igniter.t(),
module(),
Keyword.t()
) :: {:ok, Path.t()} | :error
end

View file

@ -0,0 +1,93 @@
defmodule Igniter.Extensions.Phoenix do
@moduledoc """
A phoenix extension for Igniter.
Install with `mix igniter.add_extension phoenix`
"""
use Igniter.Extension
def proper_location(igniter, module, opts) do
case Keyword.get(opts, :location_convention, :phoenix_generators) do
:phoenix_generators ->
phoenix_generators_proper_location(igniter, module)
other ->
raise "Unknown phoenix location convention #{inspect(other)}"
end
end
defp phoenix_generators_proper_location(igniter, module) do
split = Module.split(module)
cond do
String.ends_with?(to_string(module), "Web.Layouts") ->
:keep
String.ends_with?(to_string(module), "Controller") && List.last(split) != "Controller" &&
String.ends_with?(List.first(split), "Web") ->
[base | rest] = split = Module.split(module)
[type] = List.last(split) |> String.split("Controller", trim: true)
rest = :lists.droplast(rest)
{:ok,
base
|> Macro.underscore()
|> Path.join("controllers")
|> then(fn path ->
rest
|> Enum.map(&Macro.underscore/1)
|> case do
[] -> [path]
nested -> Path.join([path | nested])
end
|> Path.join()
end)
|> Path.join(Macro.underscore(type) <> "_controller.ex")}
String.ends_with?(to_string(module), "HTML") && List.last(split) != "HTML" &&
String.ends_with?(List.first(split), "Web") ->
[base | rest] = split = Module.split(module)
[type] = List.last(split) |> String.split("HTML", trim: true)
rest = :lists.droplast(rest)
potential_controller_module =
Module.concat([base | rest] ++ [type <> "Controller"])
{exists?, _} = Igniter.Code.Module.module_exists?(igniter, potential_controller_module)
if exists? && Igniter.Libs.Phoenix.controller?(igniter, potential_controller_module) do
{:ok,
base
|> Macro.underscore()
|> Path.join("controllers")
|> then(fn path ->
rest
|> Enum.map(&Macro.underscore/1)
|> case do
[] -> [path]
nested -> Path.join([path | nested])
end
|> Path.join()
end)
|> Path.join(Macro.underscore(type) <> "_html.ex")}
else
:keep
end
String.ends_with?(to_string(module), "CoreComponents") &&
String.contains?(to_string(module), "Web") ->
:keep
String.ends_with?(to_string(module), "JSON") && List.last(Module.split(module)) != "JSON" &&
String.ends_with?(List.first(Module.split(module)), "Web") ->
:keep
true ->
:error
end
end
end

View file

@ -18,6 +18,56 @@ defmodule Igniter.Libs.Phoenix do
Module.concat([inspect(Igniter.Code.Module.module_name_prefix(igniter)) <> "Web"])
end
@spec html?(Igniter.t(), module()) :: boolean()
def html?(igniter, module) do
zipper = elem(Igniter.Code.Module.find_module!(igniter, module), 2)
case Igniter.Code.Common.move_to(zipper, fn zipper ->
if Igniter.Code.Function.function_call?(zipper, :use, 2) do
using_a_webbish_module?(zipper) &&
Igniter.Code.Function.argument_equals?(zipper, 1, :html)
else
false
end
end) do
{:ok, _} ->
true
_ ->
false
end
end
@spec controller?(Igniter.t(), module()) :: boolean()
def controller?(igniter, module) do
zipper = elem(Igniter.Code.Module.find_module!(igniter, module), 2)
case Igniter.Code.Common.move_to(zipper, fn zipper ->
if Igniter.Code.Function.function_call?(zipper, :use, 2) do
using_a_webbish_module?(zipper) &&
Igniter.Code.Function.argument_equals?(zipper, 1, :controller)
else
false
end
end) do
{:ok, _} ->
true
_ ->
false
end
end
defp using_a_webbish_module?(zipper) do
case Igniter.Code.Function.move_to_nth_argument(zipper, 0) do
{:ok, zipper} ->
Igniter.Code.Module.module_matching?(zipper, &String.ends_with?(to_string(&1), "Web"))
:error ->
false
end
end
@doc """
Generates a module name that lives in the Web directory of the current app.
"""

View file

@ -143,7 +143,7 @@ defmodule Igniter.Project.Application do
end
def do_add_child(igniter, application, to_supervise, opts) do
path = Igniter.Code.Module.proper_location(application)
path = Igniter.Project.Module.proper_location(igniter, application, :source_folder)
to_supervise =
case to_supervise do
@ -274,7 +274,7 @@ defmodule Igniter.Project.Application do
end
def create_application_file(igniter, application) do
path = Igniter.Code.Module.proper_location(application)
path = Igniter.Project.Module.proper_location(igniter, application)
supervisor = Igniter.Code.Module.module_name(igniter, "Supervisor")
contents = """

View file

@ -9,6 +9,17 @@ defmodule Igniter.Project.IgniterConfig do
or moved there if the folder is created.
"""
],
extensions: [
type:
{:list,
{:or,
[
{:behaviour, Igniter.Extension},
{:tuple, [{:behaviour, Igniter.Extension}, :keyword_list]}
]}},
default: [],
doc: "A list of extensions to use in the project."
],
source_folders: [
type: {:list, :string},
default: ["lib", "test/support"],
@ -47,6 +58,58 @@ defmodule Igniter.Project.IgniterConfig do
igniter.assigns[:igniter_exs][config] || @configs[config][:default]
end
def add_extension(igniter, extension) do
extension =
case extension do
{mod, opts} -> {mod, opts}
mod -> {mod, []}
end
quoted =
extension
|> 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,
:extensions,
[quoted],
fn zipper ->
case Igniter.Code.List.move_to_list_item(zipper, fn zipper ->
if Igniter.Code.Tuple.tuple?(zipper) do
with {:ok, item} <- Igniter.Code.Tuple.tuple_elem(zipper, 0),
true <- Igniter.Code.Common.nodes_equal?(item, elem(extension, 0)) do
true
else
_ ->
false
end
else
Igniter.Code.Common.nodes_equal?(zipper, elem(extension, 0))
end
end) do
{:ok, _} ->
{:ok, zipper}
_ ->
Igniter.Code.List.prepend_to_list(zipper, quoted)
end
end
)
else
{:warning,
"Failed to modify `.igniter.exs` when adding the extension #{inspect(extension)} because its last return value is not a list literal."}
end
end)
end
def setup(igniter) do
Igniter.create_or_update_elixir_file(
igniter,

View file

@ -0,0 +1,578 @@
defmodule Igniter.Project.Module do
@moduledoc "Codemods and utilities for interacting with modules"
require Igniter.Code.Common
alias Igniter.Code.Common
alias Sourceror.Zipper
require Logger
@typedoc """
Placement instruction for a module.
- `:source_folder` - The first source folder of the project
- `{:source_folder, path}` - The selected source folder, i.e `"lib"`
- `:test` - Creating a test file
- `:test_support` - Creating a test support file
"""
@type location_type :: :source_folder | {:source_folder, String.t()} | :test | :test_support
@doc """
Determines where a module should be placed in a project.
"""
@spec proper_location(Igniter.t(), module(), location_type()) :: String.t()
def proper_location(igniter, module_name, type \\ :source_folder) do
type =
case type do
:source_folder ->
igniter
|> Igniter.Project.IgniterConfig.get(:source_folders)
|> Enum.at(0)
|> then(&{:source_folder, &1})
:test_support ->
{:source_folder, "test/support"}
type ->
type
end
do_proper_location(igniter, module_name, type)
end
@doc "Given a suffix, returns a module name with the prefix of the current project."
@spec module_name(Igniter.t(), String.t()) :: module()
def module_name(igniter, suffix) do
Module.concat(module_name_prefix(igniter), suffix)
end
@doc "The module name prefix based on the mix project's module name"
@spec module_name_prefix(Igniter.t()) :: module()
def module_name_prefix(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.Module.move_to_defmodule(zipper),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0) do
case Igniter.Code.Common.expand_alias(zipper) do
%Zipper{node: module_name} when is_atom(module_name) ->
module_name
|> Module.split()
|> :lists.droplast()
|> Module.concat()
%Zipper{node: {:__aliases__, _, parts}} ->
parts
|> :lists.droplast()
|> Module.concat()
end
else
_ ->
raise """
Failed to parse the module name from mix.exs.
Please ensure that you are defining a Mix.Project module in your mix.exs file.
"""
end
end
@doc """
Finds a module, raising an error if its not found.
See `find_module/2` for more information.
"""
@spec find_module!(Igniter.t(), module()) ::
{Igniter.t(), Rewrite.Source.t(), Zipper.t()} | no_return
def find_module!(igniter, module_name) do
case find_module(igniter, module_name) do
{:ok, {igniter, source, zipper}} ->
{igniter, source, zipper}
{:error, _igniter} ->
raise "Could not find module `#{inspect(module_name)}`"
end
end
@doc """
Finds a module and updates its contents wherever it is.
If the module does not yet exist, it is created with the provided contents. In that case,
the path is determined with `Igniter.Code.Module.proper_location/2`, but may optionally be overwritten with options below.
# Options
- `:path` - Path where to create the module, relative to the project root. Default: `nil` (uses `:kind` to determine the path).
"""
def find_and_update_or_create_module(igniter, module_name, contents, updater, opts \\ [])
def find_and_update_or_create_module(
igniter,
module_name,
contents,
updater,
opts
)
when is_list(opts) do
case find_and_update_module(igniter, module_name, updater) do
{:ok, igniter} ->
igniter
{:error, igniter} ->
create_module(igniter, module_name, contents, opts)
end
end
def find_and_update_or_create_module(
igniter,
module_name,
contents,
updater,
path
)
when is_binary(path) do
Logger.warning("You should use `opts` instead of `path` and pass `path` as a keyword.")
find_and_update_or_create_module(igniter, module_name, contents, updater, path: path)
end
@doc """
Creates a new file & module in its appropriate location.
## Options
- `:location` - A location type. See `t:location_type` for more.
"""
def create_module(igniter, module_name, contents, opts \\ []) do
contents =
"""
defmodule #{inspect(module_name)} do
#{contents}
end
"""
location =
case Keyword.get(opts, :path, nil) do
nil ->
proper_location(igniter, module_name, opts[:location] || :source_folder)
path ->
path
end
Igniter.create_new_file(igniter, location, contents)
end
@doc "Checks if a module is defined somewhere in the project. The returned igniter should not be discarded."
def module_exists?(igniter, module_name) do
case find_module(igniter, module_name) do
{:ok, {igniter, _, _}} -> {true, igniter}
{:error, igniter} -> {false, igniter}
end
end
@doc "Finds a module and updates its contents. Raises an error if it doesn't exist"
def find_and_update_module!(igniter, module_name, updater) do
case find_and_update_module(igniter, module_name, updater) do
{:ok, igniter} -> igniter
{:error, _igniter} -> raise "Could not find module #{inspect(module_name)}"
end
end
@doc "Finds a module and updates its contents. Returns `{:error, igniter}` if the module could not be found. Do not discard this igniter."
@spec find_and_update_module(Igniter.t(), module(), (Zipper.t() -> {:ok, Zipper.t()} | :error)) ::
{:ok, Igniter.t()} | {:error, Igniter.t()}
def find_and_update_module(igniter, module_name, updater) do
case find_module(igniter, module_name) do
{:ok, {igniter, source, zipper}} ->
case Common.move_to_do_block(zipper) do
{:ok, zipper} ->
case updater.(zipper) do
{:ok, zipper} ->
new_quoted =
zipper
|> Zipper.topmost()
|> Zipper.node()
new_source = Rewrite.Source.update(source, :quoted, new_quoted)
{:ok, %{igniter | rewrite: Rewrite.update!(igniter.rewrite, new_source)}}
{:error, error} ->
{:ok, Igniter.add_issue(igniter, error)}
{:warning, error} ->
{:ok, Igniter.add_warning(igniter, error)}
end
_ ->
{:error, igniter}
end
{:error, igniter} ->
{:error, igniter}
end
end
@spec find_all_matching_modules(igniter :: Igniter.t(), (module(), Zipper.t() -> boolean)) ::
{Igniter.t(), [module()]}
def find_all_matching_modules(igniter, predicate) do
igniter =
igniter
|> Igniter.include_all_elixir_files()
matching_modules =
igniter
|> Map.get(:rewrite)
|> Enum.filter(&match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, &1))
|> Task.async_stream(
fn source ->
source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
|> Zipper.traverse([], fn zipper, acc ->
case zipper.node do
{:defmodule, _, [_, _]} ->
{:ok, mod_zipper} = Igniter.Code.Function.move_to_nth_argument(zipper, 0)
module_name =
mod_zipper
|> Igniter.Code.Common.expand_alias()
|> Zipper.node()
|> Igniter.Project.Module.to_module_name()
with module_name when not is_nil(module_name) <- module_name,
{:ok, do_zipper} <- Igniter.Code.Common.move_to_do_block(zipper),
true <- predicate.(module_name, do_zipper) do
{zipper, [module_name | acc]}
else
_ ->
{zipper, acc}
end
_ ->
{zipper, acc}
end
end)
|> elem(1)
end,
timeout: :infinity
)
|> Enum.flat_map(fn {:ok, v} ->
v
end)
|> Enum.uniq()
{igniter, matching_modules}
end
@doc """
Finds a module, returning a new igniter, and the source and zipper location. This new igniter should not be discarded.
In general, you should not use the returned source and zipper to update the module, instead, use this to interrogate
the contents or source in some way, and then call `find_and_update_module/3` with a function to perform an update.
"""
@spec find_module(Igniter.t(), module()) ::
{:ok, {Igniter.t(), Rewrite.Source.t(), Zipper.t()}} | {:error, Igniter.t()}
def find_module(igniter, module_name) do
igniter = Igniter.include_all_elixir_files(igniter)
igniter
|> Map.get(:rewrite)
|> Enum.filter(&match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, &1))
|> Task.async_stream(
fn source ->
{source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
|> Igniter.Code.Module.move_to_defmodule(module_name), source}
end,
timeout: :infinity
)
|> Enum.find_value({:error, igniter}, fn
{:ok, {{:ok, zipper}, source}} ->
{:ok, {igniter, source, zipper}}
_other ->
false
end)
end
@doc false
def move_files(igniter, opts \\ []) do
module_location_config = Igniter.Project.IgniterConfig.get(igniter, :module_location)
dont_move_files = Igniter.Project.IgniterConfig.get(igniter, :dont_move_files)
igniter =
if opts[:move_all?] do
Igniter.include_all_elixir_files(igniter)
else
igniter
end
igniter.rewrite
|> Stream.filter(&(Path.extname(&1.path) in [".ex", ".exs"]))
|> Stream.reject(&non_movable_file?(&1.path, dont_move_files))
|> Enum.reduce(igniter, fn source, igniter ->
zipper =
source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
with {:ok, zipper} <- Igniter.Code.Module.move_to_defmodule(zipper),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0),
module <-
zipper
|> Igniter.Code.Common.expand_alias()
|> Zipper.node(),
module when not is_nil(module) <- to_module_name(module),
new_path when not is_nil(new_path) <-
should_move_file_to(igniter, source, module, module_location_config, opts) do
Igniter.move_file(igniter, source.path, new_path, error_if_exists?: false)
else
_ ->
igniter
end
end)
end
defp non_movable_file?(path, dont_move_files) do
Enum.any?(dont_move_files, fn
exclusion_pattern when is_binary(exclusion_pattern) ->
path == exclusion_pattern
exclusion_pattern when is_struct(exclusion_pattern, Regex) ->
Regex.match?(exclusion_pattern, path)
end)
end
defp should_move_file_to(igniter, source, module, module_location_config, opts) do
paths_created =
igniter.rewrite
|> Enum.filter(fn source ->
Rewrite.Source.from?(source, :string)
end)
|> Enum.map(& &1.path)
split_path =
source.path
|> Path.relative_to_cwd()
|> Path.split()
igniter
|> Igniter.Project.IgniterConfig.get(:source_folders)
|> Enum.filter(fn source_folder ->
List.starts_with?(split_path, Path.split(source_folder))
end)
|> Enum.max_by(
fn source_folder ->
source_folder
|> Path.split()
|> Enum.zip(split_path)
|> Enum.take_while(fn {l, r} -> l == r end)
|> Enum.count()
end,
fn -> nil end
)
|> case do
nil ->
if Enum.at(split_path, 0) == "test" &&
String.ends_with?(source.path, "_test.exs") do
{:ok, proper_location(igniter, module, :test), :test}
else
:error
end
source_folder ->
{:ok, proper_location(igniter, module, {:source_folder, source_folder}),
{:source_folder, source_folder}}
end
|> case do
:error ->
nil
{:ok, proper_location, location_type} ->
case module_location_config do
:inside_matching_folder ->
{[filename, folder], rest} =
proper_location
|> Path.split()
|> Enum.reverse()
|> Enum.split(2)
inside_matching_folder =
[filename, Path.rootname(filename), folder]
|> Enum.concat(rest)
|> Enum.reverse()
|> Path.join()
inside_matching_folder_dirname = Path.dirname(inside_matching_folder)
just_created_folder? =
Enum.any?(paths_created, fn path ->
List.starts_with?(Path.split(path), Path.split(inside_matching_folder_dirname))
end)
should_use_inside_matching_folder? =
if opts[:move_all?] do
dir?(igniter, inside_matching_folder_dirname) || just_created_folder?
else
source.path == proper_location(igniter, module, location_type) &&
!dir?(igniter, inside_matching_folder_dirname) && just_created_folder?
end
if should_use_inside_matching_folder? do
inside_matching_folder
else
proper_location
end
:outside_matching_folder ->
if opts[:move_all?] || Rewrite.Source.from?(source, :string) do
proper_location
end
end
end
end
defp dir?(igniter, folder) do
if igniter.assigns[:test_mode?] do
igniter.assigns[:test_files]
|> Map.keys()
|> Enum.any?(fn file_path ->
List.starts_with?(Path.split(file_path), Path.split(folder))
end)
else
File.dir?(folder)
end
end
@doc false
def to_module_name({:__aliases__, _, parts}), do: Module.concat(parts)
def to_module_name(value) when is_atom(value) and not is_nil(value), do: value
def to_module_name(_), do: nil
defp do_proper_location(igniter, module_name, kind) do
path =
module_name
|> Module.split()
|> case do
["Mix", "Tasks" | rest] ->
suffix =
rest
|> Enum.map(&to_string/1)
|> Enum.map_join(".", &Macro.underscore/1)
["mix", "tasks", suffix]
_other ->
modified_module_name =
case kind do
:test ->
string_module = to_string(module_name)
if String.ends_with?(string_module, "Test") do
Module.concat([String.slice(string_module, 0..-5//1)])
else
module_name
end
_ ->
module_name
end
module_to_path(igniter, modified_module_name, module_name)
end
last = List.last(path)
leading = :lists.droplast(path)
case kind do
:test ->
if String.ends_with?(last, "_test") do
Path.join(["test" | leading] ++ ["#{last}.exs"])
else
Path.join(["test" | leading] ++ ["#{last}_test.exs"])
end
{:source_folder, "test/support"} ->
case leading do
[] ->
Path.join(["test/support", "#{last}.ex"])
[_prefix | leading_rest] ->
Path.join(["test/support" | leading_rest] ++ ["#{last}.ex"])
end
{:source_folder, source_folder} ->
Path.join([source_folder | leading] ++ ["#{last}.ex"])
end
end
defp module_to_path(igniter, module, original) do
Enum.reduce_while(
igniter.assigns[:igniter_exs][:extensions] || [],
:error,
fn {extension, opts}, status ->
case extension.proper_location(igniter, module, opts) do
:error ->
{:cont, status}
:keep ->
{:cont, :keep}
{:ok, path} ->
{:halt, {:ok, path}}
end
end
)
|> case do
:keep ->
case find_module(igniter, original) do
{:ok, {_, source, _}} ->
split_path =
source.path
|> Path.rootname(".ex")
|> Path.rootname(".exs")
|> Path.split()
igniter
|> Igniter.Project.IgniterConfig.get(:source_folders)
|> Enum.concat(["test"])
|> Enum.uniq()
|> Enum.map(&Path.split/1)
|> Enum.sort_by(&(-length(&1)))
|> Enum.find(fn source_folder ->
List.starts_with?(split_path, Path.split(source_folder))
end)
|> case do
nil ->
default_location(module)
source_path ->
Enum.drop(split_path, Enum.count(source_path))
end
_ ->
default_location(module)
end
:error ->
default_location(module)
{:ok, path} ->
path
|> Path.rootname(".ex")
|> Path.rootname(".exs")
|> Path.split()
end
end
defp default_location(module) do
module
|> Module.split()
|> Enum.map(&to_string/1)
|> Enum.map(&Macro.underscore/1)
end
end

View file

@ -44,11 +44,19 @@ defmodule Igniter.Test do
## Options
* `label` - A label to put before the diff.
* `only` - File path(s) to only show the diff for
"""
@spec puts_diff(Igniter.t(), opts :: Keyword.t()) :: Igniter.t()
def puts_diff(igniter, opts \\ []) do
igniter.rewrite.sources
|> Map.values()
|> then(fn sources ->
if opts[:only] do
Enum.filter(sources, &(&1.path in List.wrap(opts[:only])))
else
sources
end
end)
|> Igniter.diff()
|> String.trim()
|> case do
@ -211,19 +219,56 @@ defmodule Igniter.Test do
defmacro assert_creates(igniter, path, content \\ nil) do
quote bind_quoted: [igniter: igniter, path: path, content: content] do
assert source = igniter.rewrite.sources[path],
"Expected #{inspect(path)} to have been created, but it was not."
"""
Expected #{inspect(path)} to have been created, but it was not.
#{Igniter.Test.created_files(igniter)}
"""
assert source.from == :string,
"Expected #{inspect(path)} to have been created, but it already existed."
"""
Expected #{inspect(path)} to have been created, but it already existed.
#{Igniter.Test.created_files(igniter)}
"""
if content do
assert Rewrite.Source.get(source, :content) == content
actual_content = Rewrite.Source.get(source, :content)
if actual_content != content do
flunk("""
Expected created file #{inspect(path)} to have the following contents:
#{content}
But it actually had the following contents:
#{actual_content}
Diff, showing your assertion against the actual contents:
#{Rewrite.TextDiff.format(actual_content, content)}
""")
end
end
igniter
end
end
@doc false
def created_files(igniter) do
igniter.rewrite
|> Rewrite.sources()
|> Enum.filter(&(&1.from == :string))
|> Enum.map(& &1.path)
|> case do
[] ->
"\nNo files were created."
modules ->
"\nThe following files were created:\n\n#{Enum.map_join(modules, "\n", &"* #{&1}")}"
end
end
defp simulate_write(igniter) do
igniter.rewrite
|> Rewrite.sources()
@ -239,7 +284,11 @@ defmodule Igniter.Test do
|> Map.put(:warnings, [])
|> Map.put(:notices, [])
|> Map.put(:issues, [])
|> Map.put(:assigns, %{test_mode?: true, test_files: test_files})
|> Map.put(:assigns, %{
test_mode?: true,
test_files: test_files,
igniter_exs: igniter.assigns[:igniter_exs]
})
end)
end
@ -276,24 +325,24 @@ defmodule Igniter.Test do
end
""")
|> Map.put_new("lib/#{app_name}.ex", """
defmodule #{module_name} do
@moduledoc \"\"\"
Documentation for `#{module_name}`.
\"\"\"
defmodule #{module_name} do
@moduledoc \"\"\"
Documentation for `#{module_name}`.
\"\"\"
@doc \"\"\"
Hello world.
@doc \"\"\"
Hello world.
## Examples
## Examples
iex> #{module_name}.hello()
:world
\"\"\"
def hello do
iex> #{module_name}.hello()
:world
end
\"\"\"
def hello do
:world
end
end
""")
|> Map.put_new("README.md", """
# #{module_name}

View file

@ -0,0 +1,41 @@
defmodule Mix.Tasks.Igniter.AddExtension do
use Igniter.Mix.Task
@example "mix igniter.add_extension phoenix"
@shortdoc "Adds an extension to your `.igniter.exs` configuration file."
@moduledoc """
#{@shortdoc}
The extension can be the module name of an extension,
or the string `phoenix`, which maps to `Igniter.Extensions.Phoenix`.
## Example
```bash
#{@example}
```
"""
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
example: @example,
positional: [:extension]
}
end
def igniter(igniter, argv) do
{%{extension: extension}, _argv} = positional_args!(argv)
extension =
if extension == "phoenix" do
Igniter.Extensions.Phoenix
else
Igniter.Code.Module.parse(extension)
end
igniter
|> Igniter.Project.IgniterConfig.add_extension(extension)
end
end

View file

@ -5,6 +5,6 @@ defmodule Mix.Tasks.Igniter.MoveFiles do
def igniter(igniter, _argv) do
Mix.shell().info("Finding all modules and determining proper locations...")
Igniter.Code.Module.move_files(igniter, move_all?: true)
Igniter.Project.Module.move_files(igniter, move_all?: true)
end
end

View file

@ -130,7 +130,7 @@ defmodule Igniter.Code.ModuleTest do
""")
|> Igniter.create_new_file("test.txt", "Foo")
assert {_igniter, [Foo]} =
assert {_igniter, [Foo, Test, TestTest]} =
Igniter.Code.Module.find_all_matching_modules(igniter, fn _module, _zipper ->
true
end)

View file

@ -0,0 +1,80 @@
defmodule Igniter.Extensions.PhoenixTest do
use ExUnit.Case
import Igniter.Test
describe "proper_location/2" do
test "extensions are honored even if the extension is added in the same check" do
test_project()
|> Igniter.Project.IgniterConfig.add_extension(Igniter.Extensions.Phoenix)
|> Igniter.Project.Module.create_module(TestWeb.FooController, """
use TestWeb, :controller
""")
|> assert_creates("lib/test_web/controllers/foo_controller.ex")
end
test "returns a controller location" do
igniter =
test_project()
|> Igniter.create_new_file("lib/test_web/controllers/foo_controller.ex", """
defmodule TestWeb.FooController do
use TestWeb, :controller
end
""")
assert {:ok, "test_web/controllers/foo_controller.ex"} =
Igniter.Extensions.Phoenix.proper_location(igniter, TestWeb.FooController, [])
end
test "when belonging to a controller, it returns an html location" do
igniter =
test_project()
|> Igniter.create_new_file("lib/test_web/controllers/foo_controller.ex", """
defmodule TestWeb.FooController do
use TestWeb, :controller
end
""")
|> Igniter.create_new_file("lib/test_web/controllers/foo_html.ex", """
defmodule TestWeb.FooHTML do
use TestWeb, :html
end
""")
assert {:ok, "test_web/controllers/foo_html.ex"} =
Igniter.Extensions.Phoenix.proper_location(igniter, TestWeb.FooHTML, [])
end
test "when not belonging to a controller, we instruct to keep its current location" do
igniter =
test_project()
|> Igniter.create_new_file("lib/test_web/controllers/foo_html.ex", """
defmodule TestWeb.FooHTML do
use TestWeb, :html
end
""")
assert :keep =
Igniter.Extensions.Phoenix.proper_location(igniter, TestWeb.FooHTML, [])
end
test "returns a json location" do
igniter =
test_project()
|> Igniter.create_new_file("test_web/controllers/foo_controller.ex", """
defmodule TestWeb.FooController do
use TestWeb, :controller
end
""")
|> Igniter.create_new_file("lib/test_web/controllers/foo_json.ex", """
defmodule TestWeb.FooJSON do
def render(_), do: %{foo: "bar"}
end
""")
assert Igniter.Extensions.Phoenix.proper_location(igniter, TestWeb.FooJSON, []) == :keep
end
end
end

View file

@ -0,0 +1,27 @@
defmodule Igniter.Libs.PhoenixTest do
use ExUnit.Case
import Igniter.Test
describe "controller?/2" do
test "detects a phoenix controller" do
igniter =
assert test_project()
|> Igniter.create_new_file("lib/test_web/controllers/foo_controller.ex", """
defmodule TestWeb.FooController do
use TestWeb, :controller
end
""")
|> Igniter.create_new_file("lib/test_web/controllers/foo_view.ex", """
defmodule TestWeb.ThingView do
use TestWeb, :view
end
""")
|> apply_igniter!()
assert Igniter.Libs.Phoenix.controller?(igniter, TestWeb.FooController)
refute Igniter.Libs.Phoenix.controller?(igniter, TestWeb.ThingView)
end
end
end

View file

@ -0,0 +1,18 @@
defmodule Igniter.Project.IgniterConfigTest do
use ExUnit.Case
import Igniter.Test
describe "add_extension/2" do
test "adds an extension to the list" do
test_project()
|> Igniter.Project.IgniterConfig.add_extension(Foobar)
|> assert_has_patch(".igniter.exs", """
11 11 | dont_move_files: [
12 12 | ~r"lib/mix"
13 - | ]
13 + | ],
14 + | extensions: [{Foobar, []}]
""")
end
end
end

View file

@ -1,4 +1,4 @@
defmodule Igniter.Mix.Tasks.Igniter.Gen.TaskTest do
defmodule Mix.Tasks.Igniter.Gen.TaskTest do
use ExUnit.Case
import Igniter.Test

View file

@ -1,4 +1,4 @@
defmodule Igniter.Mix.Tasks.InstallTest do
defmodule Mix.Tasks.Igniter.InstallTest do
use ExUnit.Case
setup do