Current section

Files

Jump to
patches lib patches.ex
Raw

lib/patches.ex

defmodule Patches do
alias Patches.Abstract
defmacro __using__(opts \\ []) do
quote do
import unquote(__MODULE__)
Module.register_attribute(__MODULE__, :wrappers, accumulate: true)
Module.register_attribute(__MODULE__, :replacers, accumulate: true)
Module.register_attribute(__MODULE__, :exports, accumulate: true)
Module.register_attribute(__MODULE__, :module_patches, accumulate: true)
@patcher_opts unquote(Macro.escape(opts))
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(env) do
wrappers = Module.get_attribute(env.module, :wrappers)
replacers = Module.get_attribute(env.module, :replacers)
exports = Module.get_attribute(env.module, :exports)
module_patches = Module.get_attribute(env.module, :module_patches)
_patcher_opts =
Module.get_attribute(env.module, :patcher_opts)
modules_and_modifications =
[wrappers, replacers, exports]
|> Enum.flat_map(fn rule -> Enum.map(rule, &elem(&1, 0)) end)
|> Enum.uniq()
|> Enum.map(fn module ->
wrappers = Enum.filter(wrappers, &(elem(&1, 0) == module))
replacers = Enum.filter(replacers, &(elem(&1, 0) == module))
exports =
Enum.filter(exports, &(elem(&1, 0) == module))
|> Enum.map(fn {_, func, arity} -> {func, arity} end)
{module, %{wrap: wrappers, replace: replacers, export: exports}}
end)
|> Enum.into(Map.from_keys(module_patches, %{}))
namespaced_modules =
for {module, _} <- modules_and_modifications, into: %{} do
{module, namespace(module, env.module)}
end
modules_and_binary =
for {module, modifications} <- modules_and_modifications, into: %{} do
binary =
abstract_code(module)
|> wrap_function(modifications[:wrap], env.module)
|> replace_function(modifications[:replace], env.module)
|> export_functions(modifications[:export])
|> Abstract.rewrite(&Map.get(namespaced_modules, &1, &1))
|> compile()
{module, binary}
end
[
quote do
def patched(module) do
for {module, binary} <- unquote(Macro.escape(modules_and_binary)) do
modname = Patches.namespace(module, unquote(env.module))
{:module, _loaded_module} = :code.load_binary(modname, [], binary)
end
Patches.namespace(module, unquote(env.module))
end
end
] ++ wrappers(modules_and_modifications)
end
defmacro patch(module) do
quote do
@module_patches unquote(module)
end
end
defmacro export(module, function, arity) do
quote do
@exports {unquote(module), unquote(function), unquote(arity)}
end
end
defmacro replace(module, function, arity, replacement_function \\ nil)
defmacro replace(module, function, arity, nil) do
quote do
@replacers {unquote(module), unquote(function), unquote(arity), unquote(function)}
end
end
defmacro replace(module, function, arity, replacement_function) do
quote do
@replacers {unquote(module), unquote(function), unquote(arity),
unquote(replacement_function)}
end
end
defmacro wrap(module, function, wrapping_function) when is_atom(wrapping_function) do
quote do
@wrappers {unquote(module), unquote(function), nil, unquote(wrapping_function)}
end
end
defmacro wrap(module, function, arity, wrapping_function) do
quote do
@wrappers {unquote(module), unquote(function), unquote(arity), unquote(wrapping_function)}
end
end
@doc false
def namespace(module, suffix) do
Module.concat(module, suffix)
end
@doc """
Returns the abstract code from the given module.
"""
def abstract_code(module) do
{_, beam, _} = :code.get_object_code(module)
{:ok, {_, [{:abstract_code, {:raw_abstract_v1, code}}]}} =
:beam_lib.chunks(beam, ~w/abstract_code/a)
code
end
@doc """
Manipulates abstract code to exports the given functions.
"""
def export_functions(code, nil), do: code
def export_functions(code, functions) do
{:attribute, line, :export, exports} = List.keyfind(code, :export, 2)
attr = {:attribute, line, :export, exports ++ functions}
List.keyreplace(code, :export, 2, attr)
end
@doc """
Manipulates abstract code to replace a function in a module with call to the
wrapping module
"""
def replace_function(code, nothing, _) when is_nil(nothing) or nothing == [], do: code
def replace_function(code, replacers, wrapping_module) when is_list(replacers),
do: Enum.reduce(replacers, code, &replace_function(&2, &1, wrapping_module))
def replace_function(code, {_module, function, arity, remote_function}, wrapping_module) do
modify_function(code, function, arity, &replace_body(&1, wrapping_module, remote_function))
end
@doc false
def wrap_function(code, nothing, _) when is_nil(nothing) or nothing == [], do: code
def wrap_function(code, mfws, wrapping_module) when is_list(mfws) do
Enum.reduce(mfws, code, &wrap_function(&2, &1, wrapping_module))
end
def wrap_function(code, {_module, function, _}, wrapping_module) do
do_wrap_function(code, function, nil, wrapping_module)
end
def wrap_function(code, {_module, function, arity, _}, wrapping_module) do
do_wrap_function(code, function, arity, wrapping_module)
end
defp do_wrap_function(code, function, arity, wrapping_module) do
code
|> modify_function(function, arity, &wrap_abstract_function(&1, wrapping_module))
end
defp modify_function(code, function, arity, func) do
Enum.reduce(code, [], fn
{:function, _, ^function, func_arity, _} = function_to_wrap, acc
when is_nil(arity) or arity == func_arity ->
[func.(function_to_wrap) | acc]
line, acc ->
[line | acc]
end)
|> Enum.reverse()
end
defp wrap_abstract_function(function_tuple, wrapping_module) do
{:function, line, function, arity, clauses} = function_tuple
clauses =
for {:clause, clause_line, args, guards, body} <- clauses do
new_body = [
{:call, line, {:remote, line, {:atom, line, wrapping_module}, {:atom, line, :wrap}},
[
{:tuple, line, [{:atom, line, function}, {:integer, line, arity}]},
{:fun, line, {:clauses, [{:clause, line, [], [], body}]}}
]}
]
{:clause, clause_line, args, guards, new_body}
end
{:function, line, function, arity, clauses}
end
defp replace_body(function_tuple, remote_module, remote_function) do
{:function, line, function, arity, clauses} = function_tuple
new_clauses =
for {:clause, clause_line, args, _guards, _body} <- clauses do
# If pattern matching is used in the arguments, the args cannot be transplanted in the function call of the replacing function.
# instead we unmatch the arguments and create variables with the names of their Structs.
# To avoid conflicts if two identical modules are used, we append the names with a counter.
# For example:
# def foo(%Bar{buz: buz}, %Bar{})
# will be normalized to:
# def foo(bar1)
args = normalize_args(args)
body = [
{:call, clause_line,
{:remote, clause_line, {:atom, clause_line, remote_module},
{:atom, clause_line, remote_function}}, args}
]
{:clause, clause_line, args, [], body}
end
{:function, line, function, arity, new_clauses}
end
defp normalize_args(args) do
args
|> Enum.with_index()
|> Enum.map(fn
{{_, line, _}, index} ->
{:var, line, :"arg#{index}@1"}
{{_, line, _, _}, index} ->
{:var, line, :"arg#{index}@1"}
end)
end
@doc false
def compile(code) do
case :compile.forms(code) do
{:ok, _modname, binary} ->
binary
:error ->
File.write(
"/tmp/punkix_compile",
code
)
:error
end
end
defp wrappers(module_map) do
for {_module, %{wrap: modifications}} <- module_map do
Enum.map(modifications, fn
{_, function, arity, wrapper} ->
quote do
def wrap({unquote(function), unquote(arity)}, fun) do
result = unquote(wrapper)(fun.())
result
end
end
{_, function, wrapper} ->
quote do
def wrap({unquote(function)}, fun) do
result = unquote(wrapper)(fun.())
result
end
end
end)
end
end
end