View Source Rewrite.Source (rewrite v1.1.2)

A representation of some source in a project.

The %Source{} contains the content of the file given by path. The module contains update/3 to update the path and/or the content. The changes are recorded in the history list.

The struct also holds issues for the source.

The different versions of content and path are available via get/3.

A source is extensible via filetype, see Rewrite.Filetype.

Summary

Types

t()

The struct representing a source.

A timestamp as integer seconds since epoch.

The version of a %Source{}. The version 1 indicates that the source has no changes.

Functions

Adds the given issue to the source.

Adds the given issues to the source.

The default path for the source.

Returns iodata showing all diffs of the given source.

Returns true if the file has been modified since it was read.

Sets the filetype for the source.

Formats the given source.

Same as format/2, but raises an exception in case of failure.

Returns true when from matches to value for key :from.

Creates a new %Source{} from the given string.

Gets the value for :content, :path in source or a specific key in filetype.

Gets the value for :content, :path in source or a specific key in filetype for the given version.

Returns true if the source has issues for the given version.

Calculates the current hash from the given source.

Returns all issues of the given source.

Returns the owner of the given source.

Assigns a private key and value to the source.

Creates a new %Source{} from the given path.

Tries to delete the file source.

Same as rm/1, but raises a Rewrite.SourceError exception in case of failure. Otherwise :ok.

Sets the timestamp to the current POSIX timestamp.

Sets the timestamp of the given source to the given timestamp.

Undoes the given number of changes.

Updates the content or the path of a source.

Returns true if the source was updated.

Returns the version of the given source. The value 1 indicates that the source has no changes.

Writes the source to disk.

Same as write/1, but raises a Rewrite.SourceError exception in case of failure.

Types

by()

@type by() :: module()

content()

@type content() :: String.t()

extension()

@type extension() :: String.t()

filetype()

@type filetype() :: map()

from()

@type from() :: :file | :string

issue()

@type issue() :: term()

key()

@type key() :: atom()

kind()

@type kind() :: :content | :path

opts()

@type opts() :: keyword()

owner()

@type owner() :: module()

t()

@type t() :: %Rewrite.Source{
  content: String.t(),
  filetype: filetype(),
  from: from(),
  hash: String.t(),
  history: [{kind(), by(), String.t()}],
  issues: [{version(), issue()}],
  owner: owner(),
  path: Path.t() | nil,
  private: map(),
  timestamp: timestamp()
}

The struct representing a source.

Fields

  • content - of the source.

  • filetype - a struct implementing the behaviour Rewrite.Filetype. The filetype is nil when no additional implementation for the filetype is available.

  • from - contains :file or :string depending on whether the source is created from a file or a string.

  • hash - of the source. The hash is built from the content and path.

  • history - of the source.

  • issues - of the source.

  • owner - of the source.

  • path - of the source. Can be nil if the source was created by a string.

  • private - a field for user defined data.

  • timestamp - is set to the timestamp of the last modification of the file on disk at the time it was read.

    If the source was created by a string, the timestamp is the creation time.

    The timestamp will be updated when the source is updated.

timestamp()

@type timestamp() :: integer()

A timestamp as integer seconds since epoch.

updater()

@type updater() :: (term() -> term())

value()

@type value() :: term()

version()

@type version() :: pos_integer()

The version of a %Source{}. The version 1 indicates that the source has no changes.

Functions

add_issue(source, issue)

@spec add_issue(t(), issue()) :: t()

Adds the given issue to the source.

add_issues(source, issues)

@spec add_issues(t(), [issue()]) :: t()

Adds the given issues to the source.

default_path(source)

@spec default_path(t()) :: Path.t()

The default path for the source.

diff(source, opts \\ [])

@spec diff(t(), opts()) :: iodata()

Returns iodata showing all diffs of the given source.

See TextDiff.format/3 for options.

Examples

iex> code = """
...> def foo( x ) do
...>   {:x,
...>     x}
...> end
...> """
iex> formatted = code |> Code.format_string!() |> IO.iodata_to_binary()
iex> source = Source.Ex.from_string(code)
iex> source |> Source.diff() |> IO.iodata_to_binary()
""
iex> source
...> |> Source.update(:content, formatted)
...> |> Source.diff(color: false)
...> |> IO.iodata_to_binary()
"""
1   - |def foo( x ) do
2   - |  {:x,
3   - |    x}
  1 + |def foo(x) do
  2 + |  {:x, x}
4 3   |end
5 4   |
"""

file_changed?(source)

@spec file_changed?(t()) :: boolean()

Returns true if the file has been modified since it was read.

If the key :from does not contain :file the function returns false.

Examples

iex> File.write("tmp/hello.txt", "hello")
iex> source = Source.read!("tmp/hello.txt")
iex> Source.file_changed?(source)
false
iex> File.write("tmp/hello.txt", "Hello, world!")
iex> Source.file_changed?(source)
true
iex> source = Source.update(source, :path, nil)
iex> Source.file_changed?(source)
true
iex> File.write("tmp/hello.txt", "hello")
iex> Source.file_changed?(source)
false
iex> File.rm!("tmp/hello.txt")
iex> Source.file_changed?(source)
true

iex> source = Source.from_string("hello")
iex> Source.file_changed?(source)
false

filetype(source, filetype)

@spec filetype(t(), filetype()) :: t()

Sets the filetype for the source.

format(source, opts \\ [])

@spec format(t(), opts()) :: {:ok, t()} | {:errror, term()}
@spec format(t(), opts()) :: t()

Formats the given source.

If the source was formatted the source gets a new :history entry, otherwise the unchanged source is returned.

Options

Examples

iex> source = Source.Ex.from_string("""
...> defmodule    Foo do
...>     def   foo(x),   do:    bar x
...>    end
...> """)
iex> {:ok, formatted} = Source.format(source, force_do_end_blocks: true)
iex> formatted.content
"""
defmodule Foo do
  def foo(x) do
    bar(x)
  end
end
"""
iex> dot_formatter = DotFormatter.from_formatter_opts(locals_without_parens: [bar: 1])
iex> {:ok, formatted} = Source.format(source, 
...>   dot_formatter: dot_formatter, force_do_end_blocks: true
...> )
iex> formatted.content
"""
defmodule Foo do
  def foo(x) do
    bar x
  end
end
"""

format!(source, opts \\ [])

Same as format/2, but raises an exception in case of failure.

from?(source, from)

@spec from?(t(), :file | :string) :: boolean()

Returns true when from matches to value for key :from.

Examples

iex> source = Source.from_string("hello")
iex> Source.from?(source, :file)
false
iex> Source.from?(source, :string)
true

from_string(content, opts \\ [])

@spec from_string(String.t(), opts()) :: t()

Creates a new %Source{} from the given string.

Options

  • :owner - an association to the module that owns the source.

  • :dot_formatter - a fromatter for the source.

  • path - the path of the source.

Examples

iex> source = Source.from_string("hello")
iex> source.content
"hello"
iex> source.path
nil
iex> source.owner
Rewrite

iex> source = Source.from_string("hello", path: "hello.md", owner: MyApp)
iex> source.path
"hello.md"
iex> source.owner
MyApp

get(source, key)

@spec get(t(), key()) :: value()

Gets the value for :content, :path in source or a specific key in filetype.

Raises Rewrite.SourceKeyError if the key can't be found.

get(source, key, version)

@spec get(t(), key(), version()) :: value()

Gets the value for :content, :path in source or a specific key in filetype for the given version.

Raises Rewrite.SourceKeyError if the key can't be found.

Examples

iex> bar =
...>   """
...>   defmodule Bar do
...>      def bar, do: :bar
...>   end
...>   """
iex> foo =
...>   """
...>   defmodule Foo do
...>      def foo, do: :foo
...>   end
...>   """
iex> source = Source.Ex.from_string(bar)
iex> source = Source.update(source, :content, foo)
iex> Source.get(source, :content) == foo
true
iex> Source.get(source, :content, 2) == foo
true
iex> Source.get(source, :content, 1) == bar
true

iex> source =
...>   "hello"
...>   |> Source.from_string(path: "some/where/hello.txt")
...>   |> Source.update(:path, "some/where/else/hello.txt")
...> Source.get(source, :path, 1)
"some/where/hello.txt"
iex> Source.get(source, :path, 2)
"some/where/else/hello.txt"

has_issues?(source, version \\ :actual)

@spec has_issues?(t(), version() | :actual | :all) :: boolean()

Returns true if the source has issues for the given version.

The version argument also accepts :actual and :all to check whether the source has problems for the actual version or if there are problems at all.

Examples

iex> source =
...>   "a + b"
...>   |> Source.Ex.from_string(path: "some/where/plus.exs")
...>   |> Source.add_issue(%{issue: :foo})
...>   |> Source.update(:path, "some/where/else/plus.exs")
...>   |> Source.add_issue(%{issue: :bar})
iex> Source.has_issues?(source)
true
iex> Source.has_issues?(source, 1)
true
iex> Source.has_issues?(source, :all)
true
iex> source = Source.update(source, :content, "a - b")
iex> Source.has_issues?(source)
false
iex> Source.has_issues?(source, 2)
true
iex> Source.has_issues?(source, :all)
true

hash(source)

@spec hash(t()) :: non_neg_integer()

Calculates the current hash from the given source.

issues(source)

@spec issues(t()) :: [issue()]

Returns all issues of the given source.

owner(source)

@spec owner(t()) :: module()

Returns the owner of the given source.

put_private(source, key, value)

@spec put_private(t(), key(), value()) :: t()

Assigns a private key and value to the source.

This is not used or accessed by Rewrite, but is intended as private storage for users or libraries that wish to store additional data about a source.

Examples

iex> source =
...>   "a + b"
...>   |> Source.from_string()
...>   |> Source.put_private(:origin, :example)
iex> source.private[:origin]
:example

read!(path, opts \\ [])

@spec read!(Path.t(), opts()) :: t()

Creates a new %Source{} from the given path.

Examples

iex> source = Source.read!("test/fixtures/source/hello.txt")
iex> source.content
"""
hello
"""

rm(source)

@spec rm(t()) :: :ok | {:error, Rewrite.SourceError.t()}

Tries to delete the file source.

Returns :ok if successful, or {:error, reason} if an error occurs.

Note the file is deleted even if in read-only mode.

rm!(source)

@spec rm!(t()) :: :ok

Same as rm/1, but raises a Rewrite.SourceError exception in case of failure. Otherwise :ok.

touch(source)

@spec touch(t()) :: t()

Sets the timestamp to the current POSIX timestamp.

Does not touch the underlying file.

touch(source, timestamp)

@spec touch(t(), timestamp()) :: t()

Sets the timestamp of the given source to the given timestamp.

Does not touch the underlying file.

undo(source, number \\ 1)

@spec undo(t(), non_neg_integer()) :: t()

Undoes the given number of changes.

Examples

iex> a = Source.from_string("test-a", path: "test/foo.txt")
iex> b = Source.update(a, :content, "test-b")
iex> c = Source.update(b, :path, "test/bar.txt")
iex> d = Source.update(c, :content, "test-d")
iex> d |> Source.undo() |> Source.get(:content)
"test-b"
iex> d |> Source.undo(1) |> Source.get(:content)
"test-b"
iex> d |> Source.undo(2) |> Source.get(:path)
"test/foo.txt"
iex> d |> Source.undo(3) |> Source.get(:content)
"test-a"
iex> d |> Source.undo(9) |> Source.get(:content)
"test-a"
iex> d |> Source.undo(9) |> Source.updated?()
false
iex> d |> Source.undo(-9) |> Source.get(:content)
"test-d"

update(source, key, value, opts \\ [])

@spec update(t(), key(), value() | updater(), opts()) :: t()

Updates the content or the path of a source.

The given value can be of type value/0 or an updater function that gets the current value and returns the new value.

Examples

iex> source =
...>   "foo"
...>   |> Source.from_string()
...>   |> Source.update(:path, "test/fixtures/new.exs", by: Example)
...>   |> Source.update(:content, "bar")
iex> source.history
[{:content, Rewrite, "foo"}, {:path, Example, nil}]
iex> source.content
"bar"

iex> source =
...>   "foo"
...>   |> Source.from_string()
...>   |> Source.update(:content, fn content -> content <> "bar" end)
iex> source.content
"foobar"

With a Rewrite.Source.Ex. Note that the AST is generated by Sourceror.

iex> source =
...>   ":a"
...>   |> Source.Ex.from_string()
...>   |> Source.update(:quoted, fn quoted ->
...>     {:__block__, meta, [atom]} = quoted 
...>     {:__block__, meta, [{:ok, atom}]}
...>   end)
iex> source.content
"{:ok, :a}\n"

If the new value is equal to the current value, no history will be added.

iex> source =
...>   "42"
...>   |> Source.from_string()
...>   |> Source.update(:content, "21", by: Example)
...>   |> Source.update(:content, "21", by: Example)
iex> source.history
[{:content, Example, "42"}]

updated?(source, kind \\ :any)

@spec updated?(t(), kind :: :content | :path | :any) :: boolean()

Returns true if the source was updated.

The optional argument kind specifies whether only :code changes or :path changes are considered. Defaults to :any.

Examples

iex> source = Source.from_string("foo")
iex> Source.updated?(source)
false
iex> source = Source.update(source, :content, "bar")
iex> Source.updated?(source)
true
iex> Source.updated?(source, :path)
false
iex> Source.updated?(source, :content)
true

version(source)

@spec version(t()) :: version()

Returns the version of the given source. The value 1 indicates that the source has no changes.

write(source, opts \\ [])

@spec write(t(), opts()) :: {:ok, t()} | {:error, Rewrite.SourceError.t()}

Writes the source to disk.

Returns {:ok, source} when the file was written successfully. The returned source does not include any previous changes or issues.

If there's an error, this function returns {:error, error} where error is a Rewrite.SourceError. You can raise it manually with raise/1.

Returns {:error, error} with reason: :nopath if the current path is nil.

Returns {:error, error} with reason: :changed if the file was changed since reading. See also file_changed?/1. The option force: true forces overwriting a changed file.

If the source :path was updated then the old file will be deleted.

Missing directories are created.

Options

  • :force, default: false - forces the saving to overwrite changed files.

  • :rm, default: true - prevents file deletion when set to false.

Examples

iex> ":test" |> Source.from_string() |> Source.write()
{:error, %SourceError{reason: :nopath, path: nil, action: :write}}

iex> path = "tmp/foo.txt"
iex> File.write(path, "foo")
iex> source = path |> Source.read!() |> Source.update(:content, "bar")
iex> Source.updated?(source)
true
iex> {:ok, source} = Source.write(source)
iex> File.read(path)
{:ok, "bar\n"}
iex> Source.updated?(source)
false

iex> source = Source.from_string("bar")
iex> Source.write(source)
{:error, %SourceError{reason: :nopath, path: nil, action: :write}}
iex> source |> Source.update(:path, "tmp/bar.txt") |> Source.write()
iex> File.read("tmp/bar.txt")
{:ok, "bar\n"}

iex> path = "tmp/ping.txt"
iex> File.write(path, "ping")
iex> source = Source.read!(path)
iex> new_path = "tmp/pong.ex"
iex> source = Source.update(source, :path, new_path)
iex> Source.write(source)
iex> File.exists?(path)
false
iex> File.read(new_path)
{:ok, "ping\n"}

iex> path = "tmp/ping.txt"
iex> File.write(path, "ping")
iex> source = Source.read!(path)
iex> new_path = "tmp/pong.ex"
iex> source = Source.update(source, :path, new_path)
iex> Source.write(source, rm: false)
iex> File.exists?(path)
true

iex> path = "tmp/ping.txt"
iex> File.write(path, "ping")
iex> source = path |> Source.read!() |> Source.update(:content, "peng")
iex> File.write(path, "pong")
iex> Source.write(source)
{:error, %SourceError{reason: :changed, path: "tmp/ping.txt", action: :write}}
iex> {:ok, _source} = Source.write(source, force: true)

write!(source)

@spec write!(t()) :: t()

Same as write/1, but raises a Rewrite.SourceError exception in case of failure.