Current section

Files

Jump to
cracker lib cache.ex
Raw

lib/cache.ex

defmodule Cracker.Cache do
@moduledoc """
Handles caching of module definitions, AST, and source file paths.
"""
def with_tables(fun) do
tables = create_tables()
try do
fun.(tables)
after
cleanup_tables(tables)
end
end
defp create_tables do
suffix = :erlang.unique_integer([:positive])
tables = %{
module_cache: :"module_definitions_cache_#{suffix}",
ast_cache: :"ast_cache_#{suffix}",
source_file_cache: :"source_file_cache_#{suffix}"
}
:ets.new(tables.module_cache, [:named_table, :set, :public])
:ets.new(tables.ast_cache, [:named_table, :set, :public])
:ets.new(tables.source_file_cache, [:named_table, :set, :public])
tables
end
defp cleanup_tables(tables) do
:ets.delete(tables.module_cache)
:ets.delete(tables.ast_cache)
:ets.delete(tables.source_file_cache)
end
def get_or_find_source_file(module_name, state) do
cache_key = {:source_file, module_name}
case :ets.lookup(state.tables.source_file_cache, cache_key) do
[{^cache_key, path}] ->
path
[] ->
path = find_source_file_with_rg(module_name, state.project_dir)
:ets.insert(state.tables.source_file_cache, {cache_key, path})
path
end
end
def get_or_extract_module_definitions(file_path, module_name, state) do
cache_key = {:module_defs, module_name}
Process.get(cache_key) ||
case :ets.lookup(state.tables.module_cache, cache_key) do
[{^cache_key, result}] ->
Process.put(cache_key, result)
result
[] ->
result = extract_module_definitions(file_path, module_name)
:ets.insert(state.tables.module_cache, {cache_key, result})
Process.put(cache_key, result)
result
end
end
def find_source_file_with_rg(module_name, project_dir) do
module_string = module_name |> Atom.to_string() |> String.trim_leading("Elixir.")
pattern = "defmodule\\s+#{module_string}\\s+"
case System.cmd("rg", ["--type", "elixir", "--files-with-matches", pattern, project_dir]) do
{output, 0} ->
output
|> String.split("\n", trim: true)
|> List.first()
{_, _} ->
nil
end
end
def extract_module_definitions(file_path, module_name) do
ast =
file_path
|> File.read!()
|> Code.string_to_quoted!(columns: false)
{_ast, acc} =
Macro.prewalk(ast, {module_name, %{}}, fn
{:defmodule, _, [{:__aliases__, _, module_parts}, _]} = node, {_, defs} ->
{node, {Module.concat(module_parts), defs}}
{def_type, _, [{name, _, args} = fun_head, _body]}, {current_module, defs}
when def_type in [:def, :defp] ->
arity = if is_list(args), do: length(args), else: 0
new_defs = Map.update(defs, current_module, %{{name, arity} => true}, &Map.put(&1, {name, arity}, true))
{fun_head, {current_module, new_defs}}
node, acc ->
{node, acc}
end)
{elem(acc, 1), ast}
end
end