Packages

A mix task to help you identify if you're using private functions.

Current section

Files

Jump to
no_touching lib no_touching.ex
Raw

lib/no_touching.ex

defmodule NoTouching do
def build_graph(call_data) do
Enum.reduce(call_data, %{}, fn data, acc ->
%{callee: callee, caller_module: caller, file: file, line: line} = data
caller_data = Map.get(acc, caller, %{})
callee_data = Map.get(caller_data, callee, [])
new_callee_data = [{file, line} | callee_data]
new_caller_data = Map.put(caller_data, callee, new_callee_data)
Map.put(acc, caller, new_caller_data)
end)
end
# These are things that are technically private but called all the time
# through macro expansions and such. Because of that, we need to ignore these
# or come up with our own way of detecting function calls.
@default_ignore [
{Module, :put_attribute, 4},
{Kernel.Typespec, :deftypespec, 6},
{Kernel.LexicalTracker, :read_cache, 2},
{Protocol, :__derive__, 3},
{Module, :get_attribute, 3},
{Kernel.Utils, :defstruct, 2},
{Kernel.Utils, :announce_struct, 1},
{ArgumentError, :exception, 1}
]
def filter_graph(graph, path \\ Path.join(File.cwd!(), ".ignore_private")) do
case File.read(path) do
{:ok, file} ->
{list, _} = Code.eval_string(file)
filter(graph, list ++ @default_ignore)
_ ->
filter(graph, @default_ignore)
end
end
def filter(graph, ignore) do
graph
|> Enum.map(fn {caller, callees} ->
{caller, filter_callees(callees, ignore)}
end)
|> Map.new()
end
def filter_callees(callees, ignore) do
callees
|> Enum.reject(&reject_callee?(&1, ignore))
|> Map.new()
end
def reject_callee?({{m, _, _} = callee, _}, ignore) do
Enum.any?(ignore, fn
^callee -> true
module when is_atom(module) -> inspect(module) == top_level_module(m)
_ -> false
end)
end
def add_private_info(graph) do
private =
Enum.reduce(graph, %{}, fn {caller, callee_data}, acc ->
acc = add_private_info(caller, acc)
Enum.reduce(callee_data, acc, fn {{callee, _, _}, _}, acc2 ->
add_private_info(callee, acc2)
end)
end)
{graph, private}
end
defp add_private_info(module, acc) do
if module in Map.keys(acc) do
acc
else
docs = Code.fetch_docs(module)
Map.put(acc, module, parse_docs(docs))
end
end
defp parse_docs({:error, _}), do: %{}
defp parse_docs({_, _, _, _, _, %{private: true}, function_docs}) do
Enum.reduce(function_docs, %{}, &parse_function(&1, &2, true))
end
defp parse_docs({_, _, _, _, :hidden, _, function_docs}) do
Enum.reduce(function_docs, %{}, &parse_function(&1, &2, true))
end
defp parse_docs({_, _, _, _, _, _, function_docs}) do
[
{{:function, :db_app_private, 0}, 31, ["db_app_private()"], :none, %{}},
{{:function, :db_app_public, 0}, 29, ["db_app_public()"], :none, %{}},
{{:function, :private, 0}, 10, ["private()"], :none, %{}},
{{:function, :public, 0}, 15, ["public()"], :none, %{private: false}},
{{:function, :repo_private, 0}, 19, ["repo_private()"], :none, %{}},
{{:function, :repo_public, 0}, 17, ["repo_public()"], :none, %{}},
{{:function, :view_private, 0}, 27, ["view_private()"], :none, %{}},
{{:function, :view_public, 0}, 25, ["view_public()"], :none, %{}},
{{:function, :web_app_private, 0}, 23, ["web_app_private()"], :none, %{}},
{{:function, :web_app_public, 0}, 21, ["web_app_public()"], :none, %{}}
]
Enum.reduce(function_docs, %{}, &parse_function(&1, &2, false))
end
defp parse_function({{:function, name, arity}, _, _, _, %{private: private?}}, acc, _) do
Map.put(acc, {name, arity}, private?)
end
defp parse_function({{:function, name, arity}, _, _, :hidden, _}, acc, _) do
Map.put(acc, {name, arity}, true)
end
defp parse_function({{:function, name, arity}, _, _, _, _}, acc, module_private?) do
Map.put(acc, {name, arity}, module_private?)
end
defp parse_function(_, acc, _), do: acc
def analyze({graph, private}) do
graph
|> Enum.reduce([], fn {caller, callees}, errors ->
Enum.reduce(callees, errors, fn {{callee, func, arity}, _} = callee_data, acc ->
private? = get_in(private, [callee, {func, arity}])
[errors_for(caller, callee_data, private?) | acc]
end)
end)
|> List.flatten()
end
defp errors_for(caller, {{caller, _, _}, _}, _), do: []
defp errors_for(_, _, nil), do: []
defp errors_for(caller, {{callee, func, arity}, files_and_lines}, private?) do
if private? and not (top_level_module(caller) == top_level_module(callee)) do
error(caller, callee, func, arity, files_and_lines)
else
[]
end
end
defp error(caller, callee, func, arity, files_and_lines) do
Enum.map(files_and_lines, fn
{_, {file, line}} ->
func = "#{inspect(callee)}.#{func}/#{arity}"
" * #{inspect(caller)} called private function #{func} at:\n #{file}:#{line}\n\n"
{file, line} ->
func = "#{inspect(callee)}.#{func}/#{arity}"
" * #{inspect(caller)} called private function #{func} at:\n #{file}:#{line}\n\n"
end)
end
defp top_level_module(module) do
module
|> inspect()
|> String.split(".")
|> hd()
end
end