defmodule Rewrite do
@moduledoc """
`Rewrite` is a tool for modifying, adding and removing files in a `Mix` project.
The package is intended for use in `Mix` tasks. `Rewrite` itself uses functions
provided by `Mix`.
With `Rewrite.read!/2` you can load the whole project. Then you can modify the
project with a number of functions provided by `Rewrite` and `Rewrite.Source`
without writing any changes back to the file system. All changes are stored in
the source structs. Any version of a source is available in the project. To
write the whole project back to the file system, the `Rewrite.write_all/2` can
be used.
Elixir source files can be modified by modifying the AST. For this `Rewrite`
uses the `Sourceror` package to create the AST and to convert it back. The
`Sourceror` package also provides all the utilities needed to manipulate the
AST.
Sources can also receive a `Rewrite.Issue` to document problems or information
with the source.
`Rewrite` respects the `.formatter.exs` in the project when rewriting sources.
To do this, the formatter can be read by `Rewrite.DotFormatter` and the
resulting DotFormatter struct can be used in the function to update the
sources.
"""
alias Rewrite.DotFormatter
alias Rewrite.Error
alias Rewrite.Source
alias Rewrite.SourceError
alias Rewrite.UpdateError
defstruct sources: %{},
extensions: %{},
hooks: [],
dot_formatter: nil,
excluded: []
@type t :: %Rewrite{
sources: %{Path.t() => Source.t()},
extensions: %{String.t() => [module()]},
hooks: [module()],
dot_formatter: DotFormatter.t() | nil,
excluded: [Path.t()]
}
@type input :: Path.t() | wildcard() | GlobEx.t()
@type wildcard :: IO.chardata()
@type opts :: keyword()
@type by :: module()
@type key :: atom()
@type updater :: (term() -> term())
@doc """
Creates an empty project.
## Options
* `:filetypes` - a list of modules implementing the behavior
`Rewrite.Filetype`. This list is used to add the `filetype` to the
`sources` of the corresponding files. The list can contain modules
representing a file type or a tuple of `{module(), keyword()}`. Rewrite
uses the keyword list from the tuple as the options argument when a file
is read.
Defaults to `[Rewrite.Source, Rewrite.Source.Ex]`.
* `:dot_formatter` - a `%DotFormatter{}` that is used to format sources.
To get and update a dot formatter see `dot_formatter/2` and to create one
see `Rewrite.DotFormatter`.
## Examples
iex> project = Rewrite.new()
iex> path = "test/fixtures/source/hello.txt"
iex> project = Rewrite.read!(project, path)
iex> project |> Rewrite.source!(path) |> Source.get(:content)
"hello\\n"
iex> project |> Rewrite.source!(path) |> Source.owner()
Rewrite
iex> project = Rewrite.new(filetypes: [{Rewrite.Source, owner: MyApp}])
iex> path = "test/fixtures/source/hello.txt"
iex> project = Rewrite.read!(project, path)
iex> project |> Rewrite.source!(path) |> Source.owner()
MyApp
"""
@spec new(keyword()) :: t()
def new(opts \\ []) do
Rewrite
|> struct!(
extensions: extensions(opts),
hooks: Keyword.get(opts, :hooks, [])
)
|> dot_formatter(Keyword.get(opts, :dot_formatter))
|> handle_hooks(:new)
end
@doc """
Creates a `%Rewrite{}` from the given `inputs`.
## Options
* Accepts the same options as `new/1`.
* 'exclude' - a list of paths and/or glob expressions to exclude sources
from the project. The option also accepts a predicate function which is
called for each source path. The exclusion takes place before the file is
read.
"""
@spec new!(input() | [input()], opts) :: t()
def new!(inputs, opts \\ []) do
opts |> new() |> read!(inputs, opts)
end
@doc """
Reads the given `input`/`inputs` and adds the source/sources to the `project`
when not already readed.
## Options
* `:force`, default: `false` - forces the reading of sources. With
`force: true` updates and issues for an already existing source are
deleted.
* `:exclude` - a list of paths and/or glob expressions to exclude sources
from the project. The option also accepts a predicate function which is
called for each source path. The exclusion takes place before the file is
read.
"""
@spec read!(t(), input() | [input()], opts()) :: t()
def read!(%Rewrite{} = rewrite, inputs, opts \\ []) do
reader = rewrite.sources |> Map.keys() |> reader(rewrite.extensions, opts)
inputs = expand(inputs)
{added, excluded, sources} =
Rewrite.TaskSupervisor
|> Task.Supervisor.async_stream_nolink(inputs, reader)
|> Enum.reduce({[], [], rewrite.sources}, fn
{:ok, {path, :excluded}}, {added, excluded, sources} ->
{added, [path | excluded], sources}
{:ok, {path, source}}, {added, excluded, sources} ->
{[path | added], excluded, Map.put(sources, path, source)}
{:exit, {error, _stacktrace}}, _sources when is_exception(error) ->
raise error
end)
rewrite
|> Map.put(:sources, sources)
|> Map.update!(:excluded, fn list -> list |> Enum.concat(excluded) |> Enum.uniq() end)
|> handle_hooks({:added, added})
end
defp reader(paths, extensions, opts) do
force = Keyword.get(opts, :force, false)
exclude? = opts |> Keyword.get(:exclude) |> exclude()
fn path ->
Logger.disable(self())
if exclude?.(path) || File.dir?(path) || (!force && path in paths) do
{path, :excluded}
else
source = read_source!(path, extensions)
{path, source}
end
end
end
defp exclude(nil) do
fn _path -> false end
end
defp exclude(list) when is_list(list) do
globs =
Enum.map(list, fn
%GlobEx{} = glob -> glob
path -> GlobEx.compile!(path)
end)
fn path ->
Enum.any?(globs, fn glob -> GlobEx.match?(glob, path) end)
end
end
defp exclude(fun) when is_function(fun, 1), do: fun
defp read_source!(path, extensions) when not is_nil(path) do
{source, opts} = extension_for_file(extensions, path)
source.read!(path, opts)
end
@doc """
Returns the extension of the given `file`.
"""
@spec extension_for_file(t() | map(), Path.t() | nil) :: {module(), opts()}
def extension_for_file(%Rewrite{extensions: extensions}, path) do
extension_for_file(extensions, path)
end
def extension_for_file(extensions, path) do
ext = if path, do: Path.extname(path)
default = Map.fetch!(extensions, "default")
case Map.get(extensions, ext, default) do
{module, opts} -> {module, opts}
module -> {module, []}
end
end
@doc """
Puts the given `source` to the given `rewrite` project.
Returns `{:ok, rewrite}` if successful, `{:error, reason}` otherwise.
## Examples
iex> project = Rewrite.new()
iex> {:ok, project} = Rewrite.put(project, Source.from_string(":a", path: "a.exs"))
iex> map_size(project.sources)
1
iex> Rewrite.put(project, Source.from_string(":b"))
{:error, %Rewrite.Error{reason: :nopath}}
iex> Rewrite.put(project, Source.from_string(":a", path: "a.exs"))
{:error, %Rewrite.Error{reason: :overwrites, path: "a.exs"}}
"""
@spec put(t(), Source.t()) :: {:ok, t()} | {:error, Error.t()}
def put(%Rewrite{}, %Source{path: nil}), do: {:error, Error.exception(reason: :nopath)}
def put(%Rewrite{sources: sources} = rewrite, %Source{path: path} = source) do
case Map.has_key?(sources, path) do
true ->
{:error, Error.exception(reason: :overwrites, path: path)}
false ->
rewrite = %{rewrite | sources: Map.put(sources, path, source)}
rewrite = handle_hooks(rewrite, {:added, [path]})
{:ok, rewrite}
end
end
@doc """
Same as `put/2`, but raises a `Rewrite.Error` exception in case of failure.
"""
@spec put!(t(), Source.t()) :: t()
def put!(%Rewrite{} = rewrite, %Source{} = source) do
case put(rewrite, source) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Deletes the source for the given `path` from the `rewrite`.
The file system files are not removed, even if the project is written. Use
`rm/2` or `rm!/2` to delete a file and source.
If the source is not part of the `rewrite` project the unchanged `rewrite` is
returned.
## Examples
iex> {:ok, project} = Rewrite.from_sources([
...> Source.from_string(":a", path: "a.exs"),
...> Source.from_string(":b", path: "b.exs"),
...> Source.from_string(":a", path: "c.exs")
...> ])
iex> Rewrite.paths(project)
["a.exs", "b.exs", "c.exs"]
iex> project = Rewrite.delete(project, "a.exs")
iex> Rewrite.paths(project)
["b.exs", "c.exs"]
iex> project = Rewrite.delete(project, "b.exs")
iex> Rewrite.paths(project)
["c.exs"]
iex> project = Rewrite.delete(project, "b.exs")
iex> Rewrite.paths(project)
["c.exs"]
"""
@spec delete(t(), Path.t()) :: t()
def delete(%Rewrite{sources: sources} = rewrite, path) when is_binary(path) do
%{rewrite | sources: Map.delete(sources, path)}
end
@doc """
Drops the sources with the given `paths` from the `rewrite` project.
The file system files are not removed, even if the project is written. Use
`rm/2` or `rm!/2` to delete a file and source.
If `paths` contains paths that are not in `rewrite`, they're simply ignored.
## Examples
iex> {:ok, project} = Rewrite.from_sources([
...> Source.from_string(":a", path: "a.exs"),
...> Source.from_string(":b", path: "b.exs"),
...> Source.from_string(":a", path: "c.exs")
...> ])
iex> project = Rewrite.drop(project, ["a.exs", "b.exs", "z.exs"])
iex> Rewrite.paths(project)
["c.exs"]
"""
@spec drop(t(), [Path.t()]) :: t()
def drop(%Rewrite{} = rewrite, paths) when is_list(paths) do
Enum.reduce(paths, rewrite, fn source, rewrite -> delete(rewrite, source) end)
end
@doc """
Tries to delete the `source` file in the file system and removes the `source`
from the `rewrite` project.
Returns `{:ok, rewrite}` if successful, or `{:error, error}` if an error
occurs.
Note the file is deleted even if in read-only mode.
"""
@spec rm(t(), Source.t() | Path.t()) ::
{:ok, t()} | {:error, Error.t() | SourceError.t()}
def rm(%Rewrite{} = rewrite, %Source{} = source) do
with :ok <- Source.rm(source) do
{:ok, delete(rewrite, source.path)}
end
end
def rm(%Rewrite{} = rewrite, source) when is_binary(source) do
with {:ok, source} <- source(rewrite, source) do
rm(rewrite, source)
end
end
@doc """
Same as `source/2`, but raises a `Rewrite.Error` exception in case of failure.
"""
@spec rm!(t(), Source.t() | Path.t()) :: t()
def rm!(%Rewrite{} = rewrite, source) when is_binary(source) or is_struct(source, Source) do
case rm(rewrite, source) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Moves a source from one path to another.
"""
@spec move(t(), Source.t() | Path.t(), Path.t(), module()) :: {:ok, t()} | {:error, term()}
def move(rewrite, from, to, by \\ Rewrite)
def move(%Rewrite{} = rewrite, from, to, by)
when is_struct(from, Source) and is_binary(to) and is_atom(by) do
case Map.has_key?(rewrite.sources, to) do
true ->
{:error, UpdateError.exception(reason: :overwrites, path: to, source: from)}
false ->
update(rewrite, from.path, fn source ->
Source.update(source, :path, to, by: by)
end)
end
end
def move(%Rewrite{} = rewrite, from, to, by)
when is_binary(from) and is_binary(to) and is_atom(by) do
with {:ok, source} <- source(rewrite, from) do
move(rewrite, source, to, by)
end
end
@doc """
Same as `move/4`, but raises an exception in case of failure.
"""
@spec move!(t(), Source.t() | Path.t(), Path.t(), module()) :: t()
def move!(%Rewrite{} = rewrite, from, to, by \\ Rewrite) do
case move(rewrite, from, to, by) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Returns a sorted list of all paths in the `rewrite` project.
"""
@spec paths(t()) :: [Path.t()]
def paths(%Rewrite{sources: sources}) do
sources |> Map.keys() |> Enum.sort()
end
@doc """
Returns `true` if any source in the `rewrite` project returns `true` for
`Source.updated?/1`.
## Examples
iex> {:ok, project} = Rewrite.from_sources([
...> Source.Ex.from_string(":a", path: "a.exs"),
...> Source.Ex.from_string(":b", path: "b.exs"),
...> Source.Ex.from_string("c", path: "c.txt")
...> ])
iex> Rewrite.updated?(project)
false
iex> project = Rewrite.update!(project, "a.exs", fn source ->
...> Source.update(source, :quoted, ":z")
...> end)
iex> Rewrite.updated?(project)
true
"""
@spec updated?(t()) :: boolean()
def updated?(%Rewrite{} = rewrite) do
rewrite.sources |> Map.values() |> Enum.any?(fn source -> Source.updated?(source) end)
end
@doc ~S"""
Creates a `%Rewrite{}` from the given sources.
Returns `{:ok, rewrite}` for a list of regular sources.
Returns `{:error, error}` for sources with a missing path and/or duplicated
paths.
"""
@spec from_sources([Source.t()], opts()) :: {:ok, t()} | {:error, term()}
def from_sources(sources, opts \\ []) when is_list(sources) do
{sources, missing, duplicated} =
Enum.reduce(sources, {%{}, [], []}, fn %Source{} = source, {sources, missing, duplicated} ->
cond do
is_nil(source.path) ->
{sources, [source | missing], duplicated}
Map.has_key?(sources, source.path) ->
{sources, missing, [source | duplicated]}
true ->
{Map.put(sources, source.path, source), missing, duplicated}
end
end)
if Enum.empty?(missing) && Enum.empty?(duplicated) do
rewrite = new(opts)
rewrite = %{rewrite | sources: sources}
rewrite =
rewrite
|> Map.put(:sources, sources)
|> handle_hooks({:added_sources, sources})
{:ok, rewrite}
else
{:error,
Error.exception(
reason: :invalid_sources,
missing_paths: missing,
duplicated_paths: duplicated
)}
end
end
@doc """
Same as `from_sources/2`, but raises a `Rewrite.Error` exception in case of
failure.
"""
@spec from_sources!([Source.t()], opts()) :: t()
def from_sources!(sources, opts \\ []) when is_list(sources) do
case from_sources(sources, opts) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Returns all sources sorted by path.
"""
@spec sources(t()) :: [Source.t()]
def sources(%Rewrite{sources: sources}) do
sources
|> Map.values()
|> Enum.sort_by(fn source -> source.path end)
end
@doc """
Returns the `%Rewrite.Source{}` for the given `path`.
Returns an `:ok` tuple with the found source, if not exactly one source is
available an `:error` is returned.
See also `sources/2` to get a list of sources for a given `path`.
"""
@spec source(t(), Path.t()) :: {:ok, Source.t()} | {:error, Error.t()}
def source(%Rewrite{sources: sources}, path) when is_binary(path) do
with :error <- Map.fetch(sources, path) do
{:error, Error.exception(reason: :nosource, path: path)}
end
end
@doc """
Same as `source/2`, but raises a `Rewrite.Error` exception in case of
failure.
"""
@spec source!(t(), Path.t()) :: Source.t()
def source!(%Rewrite{} = rewrite, path) do
case source(rewrite, path) do
{:ok, source} -> source
{:error, error} -> raise error
end
end
@doc """
Updates the given `source` in the `rewrite` project.
This function will be usually used if the `path` for the `source` has not
changed.
Returns `{:ok, rewrite}` if successful, `{:error, error}` otherwise.
"""
@spec update(t(), Source.t()) ::
{:ok, t()} | {:error, Error.t()}
def update(%Rewrite{}, %Source{path: nil}),
do: {:error, Error.exception(reason: :nopath)}
def update(%Rewrite{} = rewrite, %Source{} = source) do
update(rewrite, source.path, source)
end
@doc """
The same as `update/2` but raises a `Rewrite.Error` exception in case
of an error.
"""
@spec update!(t(), Source.t()) :: t()
def update!(%Rewrite{} = rewrite, %Source{} = source) do
case update(rewrite, source) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Updates a source for the given `path` in the `rewrite` project.
If `source` a `Rewrite.Source` struct the struct is used to update the
`rewrite` project.
If `source` is a function the source for the given `path` is passed to the
function and the result is used to update the `rewrite` project.
Returns `{:ok, rewrite}` if the update was successful, `{:error, error}`
otherwise.
## Examples
iex> a = Source.Ex.from_string(":a", path: "a.exs")
iex> b = Source.Ex.from_string(":b", path: "b.exs")
iex> {:ok, project} = Rewrite.from_sources([a, b])
iex> {:ok, project} = Rewrite.update(project, "a.exs", Source.Ex.from_string(":foo", path: "a.exs"))
iex> project |> Rewrite.source!("a.exs") |> Source.get(:content)
":foo"
iex> {:ok, project} = Rewrite.update(project, "a.exs", fn s -> Source.update(s, :content, ":baz") end)
iex> project |> Rewrite.source!("a.exs") |> Source.get(:content)
":baz"
iex> {:ok, project} = Rewrite.update(project, "a.exs", fn s -> Source.update(s, :path, "c.exs") end)
iex> Rewrite.paths(project)
["b.exs", "c.exs"]
iex> Rewrite.update(project, "no.exs", Source.from_string(":foo", path: "x.exs"))
{:error, %Rewrite.Error{reason: :nosource, path: "no.exs"}}
iex> Rewrite.update(project, "c.exs", Source.from_string(":foo"))
{:error, %Rewrite.UpdateError{reason: :nopath, source: "c.exs"}}
iex> Rewrite.update(project, "c.exs", fn _ -> b end)
{:error, %Rewrite.UpdateError{reason: :overwrites, path: "b.exs", source: "c.exs"}}
"""
@spec update(t(), Path.t(), Source.t() | function()) ::
{:ok, t()} | {:error, Error.t() | UpdateError.t()}
def update(%Rewrite{}, path, %Source{path: nil}) when is_binary(path) do
{:error, UpdateError.exception(reason: :nopath, source: path)}
end
def update(%Rewrite{} = rewrite, path, %Source{} = source)
when is_binary(path) do
with {:ok, _stored} <- source(rewrite, path) do
do_update(rewrite, path, source)
end
end
def update(%Rewrite{} = rewrite, path, fun) when is_binary(path) and is_function(fun, 1) do
with {:ok, stored} <- source(rewrite, path),
{:ok, source} <- apply_update!(stored, fun) do
do_update(rewrite, path, source)
end
end
defp do_update(rewrite, path, source) do
case path == source.path do
true ->
rewrite = %{rewrite | sources: Map.put(rewrite.sources, path, source)}
rewrite = handle_hooks(rewrite, {:updated, path})
{:ok, rewrite}
false ->
case Map.has_key?(rewrite.sources, source.path) do
true ->
{:error, UpdateError.exception(reason: :overwrites, path: source.path, source: path)}
false ->
sources = rewrite.sources |> Map.delete(path) |> Map.put(source.path, source)
{:ok, %{rewrite | sources: sources}}
end
end
end
defp apply_update!(source, fun) do
case fun.(source) do
%Source{path: nil} ->
{:error, UpdateError.exception(reason: :nopath, source: source.path)}
%Source{} = source ->
{:ok, source}
got ->
raise RuntimeError, """
expected %Source{} from anonymous function given to Rewrite.update/3, got: #{inspect(got)}\
"""
end
end
@doc """
The same as `update/3` but raises a `Rewrite.Error` exception in case
of an error.
"""
@spec update!(t(), Path.t(), Source.t() | function()) :: t()
def update!(%Rewrite{} = rewrite, path, new) when is_binary(path) do
case update(rewrite, path, new) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Updates the source for the given `path` and `key` with the given `fun`.
The function combines `update/3` and `Source.update/4` in one call.
## Examples
iex> project =
...> Rewrite.new()
...> |> Rewrite.new_source!("test.md", "foo")
...> |> Rewrite.update_source!("test.md", :content, fn content ->
...> content <> "bar"
...> end)
...> |> Rewrite.update_source!("test.md", :content, &String.upcase/1, by: MyApp)
iex> source = Rewrite.source!(project, "test.md")
iex> source.content
"FOOBAR"
iex> source.history
[{:content, MyApp, "foobar"}, {:content, Rewrite, "foo"}]
"""
@spec update_source(t(), Path.t(), key(), updater(), opts()) ::
{:ok, t()} | {:error, term()}
def update_source(%Rewrite{} = rewrite, path, key, fun, opts \\ []) do
update(rewrite, path, fn source ->
Source.update(source, key, fun, opts)
end)
end
@doc """
The same as `update_source/5` but raises a `Rewrite.Error` exception in case
of an error.
"""
@spec update_source!(t(), Path.t(), key(), updater(), opts()) :: t()
def update_source!(%Rewrite{} = rewrite, path, key, fun, opts \\ []) do
case update_source(rewrite, path, key, fun, opts) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Returns `true` when the `%Rewrite{}` contains a `%Source{}` with the given
`path`.
## Examples
iex> {:ok, project} = Rewrite.from_sources([
...> Source.from_string(":a", path: "a.exs")
...> ])
iex> Rewrite.has_source?(project, "a.exs")
true
iex> Rewrite.has_source?(project, "b.exs")
false
"""
@spec has_source?(t(), Path.t()) :: boolean()
def has_source?(%Rewrite{sources: sources}, path) when is_binary(path) do
Map.has_key?(sources, path)
end
@doc """
Returns `true` if any source has one or more issues.
"""
@spec issues?(t) :: boolean
def issues?(%Rewrite{sources: sources}) do
sources
|> Map.values()
|> Enum.any?(fn %Source{issues: issues} -> not Enum.empty?(issues) end)
end
@doc """
Counts the sources with the given `extname` in the `rewrite` project.
"""
@spec count(t, String.t()) :: non_neg_integer
def count(%Rewrite{sources: sources}, extname) when is_binary(extname) do
sources
|> Map.keys()
|> Enum.count(fn path -> Path.extname(path) == extname end)
end
@doc """
Invokes `fun` for each `source` in the `rewrite` project and updates the
`rewirte` project with the result of `fun`.
Returns a `{:ok, rewrite}` if any update is successful.
Returns `{:error, errors, rewrite}` where `rewrite` is updated for all sources
that are updated successful. The `errors` are the `errors` of `update/3`.
"""
@spec map(t(), (Source.t() -> Source.t())) ::
{:ok, t()} | {:error, [{:nosource | :overwrites | :nopath, Source.t()}]}
def map(%Rewrite{} = rewrite, fun) when is_function(fun, 1) do
{rewrite, errors} =
Enum.reduce(rewrite, {rewrite, []}, fn source, {rewrite, errors} ->
with {:ok, updated} <- apply_update!(source, fun),
{:ok, rewrite} <- do_update(rewrite, source.path, updated) do
{rewrite, errors}
else
{:error, error} -> {rewrite, [error | errors]}
end
end)
if Enum.empty?(errors) do
{:ok, rewrite}
else
{:error, errors, rewrite}
end
end
@doc """
Return a `rewrite` project where each `source` is the result of invoking
`fun` on each `source` of the given `rewrite` project.
"""
@spec map!(t(), (Source.t() -> Source.t())) :: t()
def map!(%Rewrite{} = rewrite, fun) when is_function(fun, 1) do
Enum.reduce(rewrite, rewrite, fn source, rewrite ->
with {:ok, updated} <- apply_update!(source, fun),
{:ok, rewrite} <- do_update(rewrite, source.path, updated) do
rewrite
else
{:error, error} -> raise error
end
end)
end
@doc """
Writes a source to disk.
The function expects a path or a `%Source{}` as first argument.
Returns `{:ok, rewrite}` if the file was written successful. See also
`Source.write/2`.
If the given `source` is not part of the `rewrite` project then it is added.
"""
@spec write(t(), Path.t() | Source.t(), nil | :force) ::
{:ok, t()} | {:error, Error.t() | SourceError.t()}
def write(rewrite, path, force \\ nil)
def write(%Rewrite{} = rewrite, path, force) when is_binary(path) and force in [nil, :force] do
with {:ok, source} <- source(rewrite, path) do
write(rewrite, source, force)
end
end
def write(%Rewrite{} = rewrite, %Source{} = source, force) when force in [nil, :force] do
with {:ok, source} <- Source.write(source) do
{:ok, Rewrite.update!(rewrite, source)}
end
end
@doc """
The same as `write/3` but raises an exception in case of an error.
"""
@spec write!(t(), Path.t() | Source.t(), nil | :force) :: t()
def write!(%Rewrite{} = rewrite, source, force \\ nil) do
case write(rewrite, source, force) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Writes all sources in the `rewrite` project to disk.
This function calls `Rewrite.Source.write/1` on all sources in the `rewrite`
project.
Returns `{:ok, rewrite}` if all sources are written successfully.
Returns `{:error, reasons, rewrite}` where `rewrite` is updated for all
sources that are written successfully.
## Options
+ `exclude` - a list paths to exclude form writting.
+ `force`, default: `false` - forces the writting of unchanged files.
"""
@spec write_all(t(), opts()) ::
{:ok, t()} | {:error, [SourceError.t()], t()}
def write_all(%Rewrite{} = rewrite, opts \\ []) do
exclude = Keyword.get(opts, :exclude, [])
force = if Keyword.get(opts, :force, false), do: :force, else: nil
write_all(rewrite, exclude, force)
end
defp write_all(%Rewrite{sources: sources} = rewrite, exclude, force)
when force in [nil, :force] do
sources = for {path, source} <- sources, path not in exclude, do: source
writer = fn source -> Source.write(source, force: force) end
{rewrite, errors} =
Rewrite.TaskSupervisor
|> Task.Supervisor.async_stream_nolink(sources, writer)
|> Enum.reduce({rewrite, []}, fn {:ok, result}, {rewrite, errors} ->
case result do
{:ok, source} -> {Rewrite.update!(rewrite, source), errors}
{:error, error} -> {rewrite, [error | errors]}
end
end)
if Enum.empty?(errors) do
{:ok, rewrite}
else
{:error, errors, rewrite}
end
end
@doc """
Formats the given `rewrite` project with the given `dot_formatter`.
Uses the formatter from `dot_formatter/2` if no formatter ist set by
`:dot_formatter` in the options. The other options are the same as for
`DotFormatter.read!/2`.
"""
@spec format(t(), opts()) :: {:ok, t()} | {:error, term()}
def format(%Rewrite{} = rewrite, opts \\ []) do
dot_formatter = Keyword.get(opts, :dot_formatter, dot_formatter(rewrite))
DotFormatter.format_rewrite(dot_formatter, rewrite, opts)
end
@doc """
The same as `format/2` but raises an exception in case of an error.
"""
@spec format!(t(), opts()) :: t()
def format!(rewrite, opts \\ []) do
case format(rewrite, opts) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Formats a source in a `rewrite` project.
Uses the formatter from `dot_formatter/2` if no formatter ist set by
`:dot_formatter` in the options. The other options are the same as for
`Code.format_string!/2`.
"""
@spec format_source(t(), Path.t() | Source.t(), keyword()) :: {:ok, t()} | {:error, term()}
def format_source(rewrite, file, opts \\ [])
def format_source(%Rewrite{} = rewrite, %Source{path: path}, opts) when is_binary(path) do
format_source(rewrite, path, opts)
end
def format_source(%Rewrite{} = rewrite, file, opts) do
dot_formatter = Keyword.get_lazy(opts, :dot_formatter, fn -> dot_formatter(rewrite) end)
DotFormatter.format_source(dot_formatter, rewrite, file, opts)
end
@doc """
The same as `format_source/3` but raises an exception in case of an error.
"""
@spec format_source!(t(), Path.t() | Source.t(), keyword()) :: t()
def format_source!(rewrite, file, opts \\ []) do
case format_source(rewrite, file, opts) do
{:ok, source} -> source
{:error, error} -> raise error
end
end
@doc """
Returns the `DotFormatter` for the given `rewrite` project.
When no formatter is set, the default formatter from
`Rewrite.DotFormatter.default/0` is returned. A dot formatter can be set with
`dot_formatter/2`.
"""
@spec dot_formatter(t()) :: DotFormatter.t()
def dot_formatter(%Rewrite{dot_formatter: nil}), do: DotFormatter.default()
def dot_formatter(%Rewrite{dot_formatter: dot_formatter}), do: dot_formatter
@doc """
Sets a `dot_formatter` for the given `rewrite` project.
"""
@spec dot_formatter(t(), DotFormatter.t() | nil) :: t()
def dot_formatter(%Rewrite{} = rewrite, dot_formatter)
when is_struct(dot_formatter, DotFormatter) or is_nil(dot_formatter) do
%{rewrite | dot_formatter: dot_formatter}
end
@doc """
Creates a new `%Source{}` and puts the source to the `%Rewrite{}` project.
The `:filetypes` option of the project is used to create the source. If
options have been specified for the file type, the given options will be
merged into those options.
Use `create_source/4` if the source is not to be inserted directly into the
project.
"""
@spec new_source(t(), Path.t(), String.t(), opts()) :: {:ok, t()} | {:error, Error.t()}
def new_source(%Rewrite{sources: sources} = rewrite, path, content, opts \\ [])
when is_binary(path) do
case Map.has_key?(sources, path) do
true ->
{:error, Error.exception(reason: :overwrites, path: path)}
false ->
source = create_source(rewrite, path, content, opts)
put(rewrite, source)
end
end
@doc """
Same as `new_source/4`, but raises a `Rewrite.Error` exception in case of failure.
"""
@spec new_source!(t(), Path.t(), String.t(), opts()) :: t()
def new_source!(%Rewrite{} = rewrite, path, content, opts \\ []) do
case new_source(rewrite, path, content, opts) do
{:ok, rewrite} -> rewrite
{:error, error} -> raise error
end
end
@doc """
Creates a new `%Source{}` without putting it to the `%Rewrite{}` project.
The `:filetypes` option of the project is used to create the source. If
options have been specified for the file type, the given options will be
merged into those options. If no `path` is given, the default file type is
created.
The function does not check whether the `%Rewrite{}` project already has a
`%Source{}` with the specified path.
Use `new_source/4` if the source is to be inserted directly into the project.
"""
@spec create_source(t(), Path.t() | nil, String.t(), opts()) :: Source.t()
def create_source(%Rewrite{} = rewrite, path, content, opts \\ []) do
{source, source_opts} = extension_for_file(rewrite, path)
opts = source_opts |> Keyword.merge(opts) |> Keyword.put(:path, path)
source.from_string(content, opts)
end
defp extensions(opts) do
opts
|> Keyword.get(:filetypes, [Source, Source.Ex])
|> Enum.flat_map(fn
Source ->
[{"default", Source}]
{Source, opts} ->
[{"default", {Source, opts}}]
{module, opts} ->
Enum.map(module.extensions(), fn extension -> {extension, {module, opts}} end)
module ->
Enum.map(module.extensions(), fn extension -> {extension, module} end)
end)
|> Map.new()
|> Map.put_new("default", Source)
end
defp expand(inputs) do
inputs
|> List.wrap()
|> Stream.map(&compile_globs!/1)
|> Stream.flat_map(&GlobEx.ls/1)
|> Stream.uniq()
end
defp compile_globs!(str) when is_binary(str), do: GlobEx.compile!(str, match_dot: true)
defp compile_globs!(glob) when is_struct(glob, GlobEx), do: glob
defimpl Enumerable do
def count(rewrite) do
{:ok, map_size(rewrite.sources)}
end
def member?(rewrite, %Source{} = source) do
member? = Map.get(rewrite.sources, source.path) == source
{:ok, member?}
end
def member?(_rewrite, _other) do
{:ok, false}
end
def slice(rewrite) do
sources = rewrite.sources |> Map.values() |> Enum.sort_by(fn source -> source.path end)
length = length(sources)
{:ok, length,
fn
start, count when start + count == length -> Enum.drop(sources, start)
start, count -> sources |> Enum.drop(start) |> Enum.take(count)
end}
end
def reduce(rewrite, acc, fun) do
sources = Map.values(rewrite.sources)
Enumerable.List.reduce(sources, acc, fun)
end
end
defp handle_hooks(%{hooks: []} = rewrite, _action), do: rewrite
defp handle_hooks(rewrite, {:added_sources, sources}) do
paths = Enum.map(sources, fn {path, _source} -> path end)
handle_hooks(rewrite, {:added, paths})
end
defp handle_hooks(%{hooks: hooks} = rewrite, action) do
Enum.reduce(hooks, rewrite, fn hook, rewrite ->
case hook.handle(action, rewrite) do
:ok ->
rewrite
{:ok, rewrite} ->
rewrite
unexpected ->
raise Error.exception(
reason: :unexpected_hook_response,
message: """
unexpected response from hook, got: #{inspect(unexpected)}\
"""
)
end
end)
end
defimpl Inspect do
def inspect(rewrite, _opts) do
"#Rewrite<#{Enum.count(rewrite.sources)} source(s)>"
end
end
end