Packages
sourceror
0.3.0
1.12.2
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.2
1.8.0
1.7.1
1.7.0
1.6.0
1.5.0
1.4.0
1.3.0
1.2.1
1.2.0
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.14.1
0.14.0
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.0
0.4.0
0.3.0
0.2.2
Utilities to work with Elixir source code.
Current section
Files
Jump to
Current section
Files
lib/sourceror.ex
defmodule Sourceror do
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
alias Sourceror.PostwalkState
@line_fields [
:closing,
:do,
:end,
:end_of_expression
]
@type postwalk_function :: (Macro.t(), PostwalkState.t() -> {Macro.t(), PostwalkState.t()})
code_module =
if Version.match?(System.version(), "~> 1.13.0-dev") do
Code
else
Sourceror.Code
end
@code_module code_module
@doc """
A wrapper around `Code.string_to_quoted_with_comments!/2` for compatibility
with pre 1.13 Elixir versions.
"""
defmacro string_to_quoted!(string, opts) do
quote bind_quoted: [code_module: @code_module, string: string, opts: opts], location: :keep do
code_module.string_to_quoted_with_comments!(string, opts)
end
end
@doc """
A wrapper around `Code.string_to_quoted_with_comments/2` for compatibility
with pre 1.13 Elixir versions.
"""
defmacro string_to_quoted(string, opts) do
quote bind_quoted: [code_module: @code_module, string: string, opts: opts], location: :keep do
code_module.string_to_quoted_with_comments(string, opts)
end
end
@doc """
A wrapper around `Code.quoted_to_algebra/2` for compatibility with pre 1.13
Elixir versions.
"""
defmacro quoted_to_algebra(quoted, opts) do
quote bind_quoted: [code_module: @code_module, quoted: quoted, opts: opts], location: :keep do
code_module.quoted_to_algebra(quoted, opts)
end
end
@doc """
Parses the source code into an extended AST suitable for source manipulation
as described in `Code.quoted_to_algebra/2`.
Two additional fields are added to nodes metadata:
* `:leading_comments` - a list holding the comments found *before* the node.
* `:trailing_comments` - a list holding the comments found before the end of
the node. For example, comments right before the `end` keyword.
Comments are the same maps returned by `Code.string_to_quoted_with_comments/2`.
"""
@spec parse_string(String.t()) :: {:ok, Macro.t()} | {:error, term()}
def parse_string(source) do
to_quoted_opts = [
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
token_metadata: true,
unescape: false
]
with {:ok, quoted, comments} <- string_to_quoted(source, to_quoted_opts) do
{:ok, Sourceror.Comments.merge_comments(quoted, comments)}
end
end
@doc """
Same as `parse_string/1` but raises on error.
"""
@spec parse_string!(String.t()) :: Macro.t()
def parse_string!(source) do
case parse_string(source) do
{:ok, quoted} ->
quoted
{:error, {location, error, token}} ->
:sourceror_errors.parse_error(location, "nofile", error, token)
end
end
@doc """
Parses a single expression from the given string. It returns the parsed
expression and the rest of the string on success.
## Examples
iex> ~S"\""
...> 42
...>
...> :ok
...> "\"" |> Sourceror.parse_expression()
{:ok, {:__block__, [trailing_comments: [], leading_comments: [],
token: "42", line: 2], [42]}, "\\n:ok"}
## Options
* `:from_line` - The line at where the parsing should start. Defaults to `1`.
"""
def parse_expression(string, opts \\ []) do
from_line = Keyword.get(opts, :from_line, 1)
lines =
Regex.split(~r/\r\n|\r|\n/, String.trim(string))
|> Enum.drop(from_line - 1)
do_parse_expression(lines, "")
end
defp do_parse_expression([], acc), do: {:error, acc}
defp do_parse_expression([line | rest], acc) do
string = Enum.join([acc, line], "\n")
case parse_string(string) do
# Skip empty lines
{:ok, {:__block__, _, []}} ->
do_parse_expression(rest, string)
{:ok, quoted} ->
{:ok, quoted, Enum.join(rest, "\n")}
{:error, _reason} ->
do_parse_expression(rest, string)
end
end
@doc """
Converts a quoted expression to a string.
The comments line number will be ignored and the line number of the associated
node will be used when formatting the code.
## Options
* `:line_length` - The max line length for the formatted code.
* `:indent` - how many indentations to insert at the start of each line.
Note that this only prepends the indents without checking the indentation
of nested blocks. Defaults to `0`.
* `:indent_type` - the type of indentation to use. It can be one of `:spaces`,
`:single_space` or `:tabs`. Defaults to `:spaces`;
"""
@spec to_string(Macro.t(), keyword) :: String.t()
def to_string(quoted, opts \\ []) do
indent = Keyword.get(opts, :indent, 0)
line_length = Keyword.get(opts, :line_length, 98)
indent_str =
case Keyword.get(opts, :indent_type, :spaces) do
:spaces -> "\s\s"
:single_space -> "\s"
:tabs -> "\t"
end
{quoted, comments} = Sourceror.Comments.extract_comments(quoted)
quoted
|> quoted_to_algebra(comments: comments)
|> Inspect.Algebra.format(line_length)
|> IO.iodata_to_binary()
|> String.split("\n")
|> Enum.map_join("\n", fn line ->
String.duplicate(indent_str, indent) <> line
end)
end
@doc """
Performs a depth-first post-order traversal of a quoted expression, correcting
line numbers as it goes.
See `postwalk/3` for more information.
"""
@spec postwalk(Macro.t(), postwalk_function) ::
Macro.t()
def postwalk(quoted, fun) do
{quoted, _} = postwalk(quoted, nil, fun)
quoted
end
@doc """
Performs a depth-first post-order traversal of a quoted expression with an accumulator, correcting
line numbers as it goes.
`fun` is a function that will receive the current node as a first argument and
the traversal state as the second one. It must return a `{quoted, state}`,
in the same way it would return `{quoted, acc}` when using `Macro.postwalk/3`.
Before calling `fun` in a node, its line numbers will be corrected by the
`state.line_correction`. If you need to manually correct the line number of
a node, use `correct_lines/2`.
The state is a map with the following keys:
* `:line_correction` - an integer representing how many lines subsequent
nodes should be shifted. If the function *adds* more nodes to the tree
that should go in a new line, the line numbers of the subsequent nodes
need to be updated in order for comments to be correctly placed during the
formatting process. If the function does this kind of change, it must
update the `:line_correction` field by adding the amount of lines that
should be shifted. Note that this field is cumulative, setting it to 0 will
reset it for the whole traversal. Starts at `0`.
* `:acc` - The accumulator. Defaults to `nil` if none is given.
"""
@spec postwalk(Macro.t(), term, postwalk_function) ::
{Macro.t(), term}
def postwalk(quoted, acc, fun) do
{quoted, %{acc: acc}} =
Macro.postwalk(quoted, %PostwalkState{acc: acc}, fn
{_, _, _} = quoted, state ->
quoted = Macro.update_meta(quoted, &correct_lines(&1, state.line_correction))
fun.(quoted, state)
quoted, state ->
fun.(quoted, state)
end)
{quoted, acc}
end
@doc """
Shifts the line numbers of the node by the given `line_correction`.
This function will update the `:line`, `:closing`, `:do`, `:end` and
`:end_of_expression` line numbers of the node metadata if such fields are
present.
"""
@spec correct_lines(keyword, integer) :: keyword
def correct_lines(meta, line_correction) do
meta =
if line = meta[:line] do
Keyword.put(meta, :line, line + line_correction)
else
meta
end
corrections = Enum.map(@line_fields, &correct_line(meta, &1, line_correction))
Enum.reduce(corrections, meta, fn correction, meta ->
Keyword.merge(meta, correction)
end)
end
defp correct_line(meta, key, line_correction) do
case Keyword.get(meta, key, []) do
value when value != [] ->
value = put_in(value, [:line], value[:line] + line_correction)
[{key, value}]
_ ->
[]
end
end
end