defmodule Recode.Context do
@moduledoc ~S'''
This module provides functions to traverse an AST with a `%Context{}`.
## Examples
The following example shows the `%Context{}` for the definition of
`MyApp.Bar.bar/1`.
iex> alias Rewrite.Source
...> alias Recode.Context
...> """
...> defmodule MyApp.Foo do
...> def foo, do: :foo
...> end
...>
...> defmodule MyApp.Bar do
...> alias MyApp.Foo
...>
...> def bar(x) do
...> {Foo.foo(), x}
...> end
...> end
...> """
...> |> Source.Ex.from_string()
...> |> Source.get(:quoted)
...> |> Zipper.zip()
...> |> Context.traverse(nil, fn
...> zipper, context, nil ->
...> case context.definition do
...> {{:def, :bar, 1}, _meta} -> {zipper, context, context}
...> _def -> {zipper, context, nil}
...> end
...> zipper, context, acc ->
...> {zipper, context, acc}
...> end)
...> |> elem(1) |> Map.put(:node, nil)
%Context{
node: nil,
aliases: [
{MyApp.Foo,
[
trailing_comments: [],
leading_comments: [],
end_of_expression: [newlines: 2, line: 6, column: 18],
line: 6,
column: 3
], nil}
],
assigns: %{},
definition:
{{:def, :bar, 1},
[
trailing_comments: [],
leading_comments: [],
do: [line: 8, column: 14],
end: [line: 10, column: 3],
line: 8,
column: 3
]},
imports: [],
module:
{MyApp.Bar,
[
trailing_comments: [],
leading_comments: [],
do: [line: 5, column: 21],
end: [line: 11, column: 1],
line: 5,
column: 1
]},
requirements: [],
usages: []
}
'''
alias Recode.Context
alias Sourceror.Zipper
defstruct module: nil,
aliases: [],
imports: [],
usages: [],
requirements: [],
definition: nil,
assigns: %{},
moduledoc: nil,
node: nil,
doc: nil,
spec: nil,
impl: nil
@type t :: %Context{
module: term() | nil,
aliases: list(),
imports: list(),
usages: list(),
requirements: list(),
definition: term(),
assigns: map(),
moduledoc: Macro.t() | nil,
node: term() | nil,
doc: {term() | nil, Macro.t()} | nil,
spec: {term() | nil, Macro.t()} | nil,
impl: {term() | nil, Macro.t()} | nil
}
@type zipper :: Zipper.t()
@doc """
Returns the current module of a context.
"""
@spec module(t()) :: module() | nil
def module(%Context{module: nil}), do: nil
def module(%Context{module: {name, _meta}}), do: name
@doc """
Expands the module alias for the given `mfa`.
"""
@spec expand_mfa(t(), mfa()) :: {:ok, mfa()} | :error
def expand_mfa(%Context{aliases: aliases}, {module, fun, arity}) do
with {:ok, alias} <- find_alias(aliases, module) do
{:ok, {alias, fun, arity}}
end
end
@doc """
Assigns the given `value` under `key` to the `context`.
"""
@spec assign(t(), atom(), term()) :: t()
def assign(%Context{assigns: assigns} = context, key, value) when is_atom(key) do
%{context | assigns: Map.put(assigns, key, value)}
end
@doc """
Returns `true` if a `spec` is available.
"""
@spec spec?(t()) :: boolean
def spec?(%Context{spec: spec}), do: not is_nil(spec)
@doc """
Returns `true` if an `impl` is available.
"""
@spec impl?(t()) :: boolean
def impl?(%Context{impl: impl}), do: not is_nil(impl)
@doc """
Returns true if `definition` satisfies the assumption.
"""
@spec definition?(t(), :public | :visible) :: boolean()
def definition?(%Context{definition: definition}, :public)
when not is_nil(definition) do
{{kind, _name, _arity}, _meta} = definition
kind in [:def, :defmacro]
end
def definition?(%Context{definition: definition} = context, :visible)
when not is_nil(definition) do
with true <- definition?(context, :public) do
not (doc?(context, false) or moduledoc?(context, false))
end
end
@doc """
Returns `true` if `@doc` is available and not set to `@doc false`.
"""
@spec doc?(t()) :: boolean()
def doc?(%Context{doc: nil}), do: false
def doc?(%Context{doc: {_to, doc}}) do
case attribute_args(doc) do
[{_name, _meta, [false]}] -> false
_else -> true
end
end
@doc """
Returns `true` if `@doc false`.
"""
@spec doc?(t(), false) :: boolean()
def doc?(%Context{doc: nil}, false), do: false
def doc?(%Context{doc: {_to, doc}}, false) do
case attribute_args(doc) do
[{_name, _meta, [false]}] -> true
_else -> false
end
end
@doc """
Returns `true` if `@moduledoc` is available and not set to `@moduledoc false`.
"""
@spec moduledoc?(t()) :: boolean()
def moduledoc?(%Context{moduledoc: moduledoc}) do
case attribute_args(moduledoc) do
nil -> false
[{_name, _meta, [false]}] -> false
_else -> true
end
end
@doc """
Returns `true` if `@moduledoc false`.
"""
@spec moduledoc?(t(), false) :: boolean()
def moduledoc?(%Context{moduledoc: nil}, false), do: false
def moduledoc?(%Context{moduledoc: moduledoc}, false) do
case attribute_args(moduledoc) do
[{_name, _meta, [false]}] -> true
_else -> false
end
end
@doc """
Merges the given `map` to the assigns of the `context`.
"""
@spec assigns(t(), map()) :: t()
def assigns(%Context{assigns: assigns} = context, map) when is_map(map) do
%{context | assigns: Map.merge(assigns, map)}
end
@doc """
Traverses the given `zipper` and applies `fun` on each node.
The `fun` gets the current `zipper` and `context` as arguments.
"""
@spec traverse(zipper(), fun) :: zipper()
when fun: (zipper(), t() -> {zipper(), t()})
def traverse(zipper, fun) when is_function(fun, 2) do
zipper
|> run_traverse(%Context{}, fun)
|> elem(0)
end
@doc """
Traverses the given `zipper` with an `acc` and applies `fun` on each node.
"""
@spec traverse(zipper(), acc, fun) :: {zipper(), acc}
when acc: term(),
fun: (zipper(), t(), acc -> {zipper(), t(), acc})
def traverse(zipper, acc, fun) when is_function(fun, 3) do
{zipper, {_context, acc}} = run_traverse(zipper, %Context{}, acc, fun)
{zipper, acc}
end
# helpers for traverse/2
defp run_traverse(zipper, context, fun) do
Zipper.traverse_while(zipper, context, fn zipper, context ->
do_traverse(zipper, context, fun)
end)
end
defp do_traverse(%Zipper{node: {:@, _meta, _args} = attribute} = zipper, context, fun) do
context = add_attribute(context, attribute)
cont(zipper, context, fun)
end
defp do_traverse(%Zipper{node: {:alias, meta, args}} = zipper, context, fun)
when not is_nil(args) do
aliases = get_aliases(args, meta, context)
context = add_aliases(context, aliases)
cont(zipper, context, fun)
end
defp do_traverse(%Zipper{node: {:import, meta, [arg, opts]}} = zipper, context, fun) do
import = get_alias(arg, context)
context = add_import(context, {import, meta, opts})
cont(zipper, context, fun)
end
defp do_traverse(%Zipper{node: {:import, meta, args}} = zipper, context, fun)
when not is_nil(args) do
imports = get_aliases(args, meta, context)
context = add_imports(context, imports)
cont(zipper, context, fun)
end
defp do_traverse(%Zipper{node: {:use, meta, [arg, opts]}} = zipper, context, fun) do
use = get_alias(arg, context)
context = add_use(context, {use, meta, opts})
cont(zipper, context, fun)
end
defp do_traverse(%Zipper{node: {:use, meta, [arg]}} = zipper, context, fun) do
use = get_alias(arg, context)
context = add_use(context, {use, meta, nil})
cont(zipper, context, fun)
end
defp do_traverse(%Zipper{node: {:require, meta, [arg, opts]}} = zipper, context, fun) do
require = get_alias(arg, context)
context = add_require(context, {require, meta, opts})
cont(zipper, context, fun)
end
defp do_traverse(%Zipper{node: {:require, meta, [arg]}} = zipper, context, fun) do
require = get_alias(arg, context)
context = add_require(context, {require, meta, nil})
cont(zipper, context, fun)
end
defp do_traverse(
%Zipper{node: {:defmodule, meta, [{:__aliases__, _, name} | _]}} = zipper,
context,
fun
) do
case module(context, :meta) == meta do
true ->
cont(zipper, context, fun)
false ->
module = module_concat(context, Module.concat(name))
do_traverse_sub(zipper, context, fun, module: {module, meta})
end
end
defp do_traverse(%Zipper{node: {:defimpl, _meta, _args}} = zipper, context, _fun) do
{:skip, zipper, context}
end
defp do_traverse(%Zipper{node: {definition, meta, args}} = zipper, context, fun)
when definition in [:def, :defp, :defmacro, :defmacrop] and not is_nil(args) do
case definition(context, :meta) == meta do
true ->
cont(zipper, context, fun)
false ->
definition = get_definition(definition, args)
context = update_attributes(context, definition)
do_traverse_sub(zipper, context, fun, definition: {definition, meta})
end
end
defp do_traverse(zipper, context, fun) do
cont(zipper, context, fun)
end
defp do_traverse_sub(zipper, context, fun, [{key, value}]) do
sub_context = Map.put(context, key, value)
{%Zipper{node: ast}, %Context{assigns: assigns}} = run_traverse(zipper, sub_context, fun)
zipper = Zipper.replace(zipper, ast)
context = Context.assigns(context, assigns)
{:skip, zipper, context}
end
# helpers for traverse/3
defp run_traverse(zipper, context, acc, fun) do
Zipper.traverse_while(zipper, {context, acc}, fn zipper, {context, acc} ->
do_traverse(zipper, context, acc, fun)
end)
end
defp do_traverse(%Zipper{node: {:@, _meta, _args} = attribute} = zipper, context, acc, fun) do
context = add_attribute(context, attribute)
cont(zipper, context, acc, fun)
end
defp do_traverse(%Zipper{node: {:alias, meta, args}} = zipper, context, acc, fun)
when not is_nil(args) do
aliases = get_aliases(args, meta, context)
context = add_aliases(context, aliases)
cont(zipper, context, acc, fun)
end
defp do_traverse(%Zipper{node: {:import, meta, [arg, opts]}} = zipper, context, acc, fun) do
import = get_alias(arg, context)
context = add_import(context, {import, meta, opts})
cont(zipper, context, acc, fun)
end
defp do_traverse(%Zipper{node: {:import, meta, args}} = zipper, context, acc, fun)
when not is_nil(args) do
imports = get_aliases(args, meta, context)
context = add_imports(context, imports)
cont(zipper, context, acc, fun)
end
defp do_traverse(%Zipper{node: {:use, meta, [arg, opts]}} = zipper, context, acc, fun) do
use = get_alias(arg, context)
context = add_use(context, {use, meta, opts})
cont(zipper, context, acc, fun)
end
defp do_traverse(%Zipper{node: {:use, meta, [arg]}} = zipper, context, acc, fun) do
use = get_alias(arg, context)
context = add_use(context, {use, meta, nil})
cont(zipper, context, acc, fun)
end
defp do_traverse(%Zipper{node: {:require, meta, [arg, opts]}} = zipper, context, acc, fun) do
require = get_alias(arg, context)
context = add_require(context, {require, meta, opts})
cont(zipper, context, acc, fun)
end
defp do_traverse(%Zipper{node: {:require, meta, [arg]}} = zipper, context, acc, fun) do
require = get_alias(arg, context)
context = add_require(context, {require, meta, nil})
cont(zipper, context, acc, fun)
end
defp do_traverse(
%Zipper{node: {:defmodule, meta, [{:__aliases__, _, name} | _]}} = zipper,
context,
acc,
fun
) do
case module(context, :meta) == meta do
true ->
cont(zipper, context, acc, fun)
false ->
module = module_concat(context, Module.concat(name))
do_traverse_sub(zipper, context, acc, fun, module: {module, meta})
end
end
defp do_traverse(%Zipper{node: {:defimpl, _meta, _args}} = zipper, context, acc, _fun) do
{:skip, zipper, {context, acc}}
end
defp do_traverse(%Zipper{node: {definition, meta, args}} = zipper, context, acc, fun)
when definition in [:def, :defp, :defmacro, :defmacrop] and length(args) == 2 do
case definition(context, :meta) == meta do
true ->
cont(zipper, context, acc, fun)
false ->
definition = get_definition(definition, args)
context = update_attributes(context, definition)
do_traverse_sub(zipper, context, acc, fun, definition: {definition, meta})
end
end
defp do_traverse(zipper, context, acc, fun) do
cont(zipper, context, acc, fun)
end
defp do_traverse_sub(zipper, context, acc, fun, [{key, value}]) do
sub_context = Map.put(context, key, value)
{%Zipper{node: ast}, {%Context{assigns: assigns}, acc}} =
run_traverse(zipper, sub_context, acc, fun)
zipper = Zipper.replace(zipper, ast)
context = Context.assigns(context, assigns)
{:skip, zipper, {context, acc}}
end
# helpers for traverse/2/3
defp add_attribute(context, {:@, _meta, [arg]} = attribute) do
case arg do
{:moduledoc, _meta, _args} ->
%{context | moduledoc: attribute}
{:doc, _meta, _args} ->
%{context | doc: {nil, attribute}}
{:spec, _meta, _args} ->
%{context | spec: {nil, attribute}}
{:impl, _meta, _args} ->
%{context | impl: {nil, attribute}}
_args ->
context
end
end
defp update_attributes(context, definition) do
context
|> update_attribute(definition, :doc)
|> update_attribute(definition, :spec)
|> update_attribute(definition, :impl)
end
defp update_attribute(context, definition, key) do
Map.update!(context, key, fn
{nil, value} -> {definition, value}
{^definition, value} -> {definition, value}
_spec -> nil
end)
end
# other helpers
defp get_definition(kind, [{:when, _meta1, [{name, _meta2, args}, _expr]}, _block]) do
{kind, name, length(args)}
end
defp get_definition(kind, [{name, _meta, args}]) do
{kind, name, length(args)}
end
defp get_definition(kind, [{name, _meta, nil}, _expr]) do
{kind, name, 0}
end
defp get_definition(kind, [{name, _meta, args}, _expr]) do
{kind, name, length(args)}
end
defp get_alias({:__aliases__, _meta, name}, _context) when is_list(name) do
Module.concat(name)
rescue
_error -> :unknown
end
defp get_alias({:__block__, _meta, [name]}, _context) when is_atom(name) do
name
end
defp get_alias({:unquote, _meta, _args} = expr, _context) do
expr
end
defp get_alias({:alias, _meta, [args]}, %Context{} = context) do
get_alias(args, context)
end
defp get_alias({:alias, meta, args}, %Context{} = context) do
get_aliases(args, meta, context)
end
defp get_alias({:require, _meta, [[arg]]}, %Context{} = context) do
get_alias(arg, context)
end
defp get_alias({:require, _meta, [arg]}, %Context{} = context) do
get_alias(arg, context)
end
defp get_alias({:__MODULE__, _meta1, _args}, %Context{} = context) do
module(context)
end
defp get_aliases([{{:., _, [alias, :{}]}, _, aliases}], meta, %Context{} = context) do
base = get_alias(alias, context)
Enum.map(aliases, fn alias ->
{Module.concat(base, get_alias(alias, context)), meta, nil}
end)
end
defp get_aliases([arg], meta, context), do: [{get_alias(arg, context), meta, nil}]
defp get_aliases([arg, opts], meta, context), do: [{get_alias(arg, context), meta, opts}]
defp cont(%Zipper{node: node} = zipper, context, fun) do
{zipper, context} = fun.(zipper, %Context{context | node: node})
{:cont, zipper, context}
end
defp cont(%Zipper{node: node} = zipper, context, acc, fun) do
{zipper, context, acc} = fun.(zipper, %Context{context | node: node}, acc)
{:cont, zipper, {context, acc}}
end
defp find_alias(aliases, module) do
Enum.find_value(aliases, :error, fn
{alias, _meta, nil} ->
case ends_with?(alias, module) do
false -> false
true -> {:ok, alias}
end
{alias, _meta, opts} ->
case get_as(opts) == module do
false -> false
true -> {:ok, alias}
end
end)
end
defp get_as([{{:__block__, _meta_block, [:as]}, {:__aliases__, _meta_aliases, as}}]) do
Module.concat(as)
end
defp get_as(_ast), do: :error
defp definition(%Context{definition: nil}, :meta), do: nil
defp definition(%Context{definition: {_definition, meta}}, :meta), do: meta
defp add_aliases(%Context{aliases: aliases} = context, list) do
%Context{context | aliases: aliases ++ list}
end
defp add_import(%Context{imports: imports} = context, import) do
%Context{context | imports: imports ++ [import]}
end
defp add_imports(%Context{imports: imports} = context, list) do
%Context{context | imports: imports ++ list}
end
defp add_use(%Context{usages: usages} = context, use) do
%Context{context | usages: usages ++ [use]}
end
defp add_require(%Context{requirements: requirements} = context, require) do
%Context{context | requirements: requirements ++ [require]}
end
defp module(%Context{module: nil}, :meta), do: nil
defp module(%Context{module: {_name, meta}}, :meta), do: meta
defp module_concat(%Context{module: nil}, module), do: module
defp module_concat(%Context{module: {module1, _meta}}, module2) do
Module.concat(module1, module2)
end
defp attribute_args(nil), do: nil
defp attribute_args({:@, _meta1, [{_name, _meta2, args}]}), do: args
defp ends_with?(value, suffix) when is_atom(value) and is_atom(suffix) do
list = Module.split(value)
suffix = Module.split(suffix)
ends_with?(list, suffix)
end
defp ends_with?(list, suffix) when is_list(list) and is_list(suffix) do
case length(list) - length(suffix) do
diff when diff < 0 -> false
diff -> Enum.drop(list, diff) == suffix
end
end
end