lib/hologram/compiler/pruner.ex

defmodule Hologram.Compiler.Pruner do
  def prune(page_module, module_defs, call_graph) do
    find_reachable_code(page_module, module_defs, call_graph)
    |> remove_redundant_funs(module_defs)
    |> remove_redundant_modules()
  end

  defp find_reachable_code(page_module, module_defs, call_graph) do
    layout_module = page_module.layout()
    component_modules = find_component_modules(module_defs)
    page_modules = find_page_modules(module_defs)

    []
    |> include_code_reachable_from_page_actions(call_graph, page_module)
    |> include_code_reachable_from_page_template(call_graph, page_module)
    |> include_code_reachable_from_page_layout_fun(call_graph, page_module)
    |> include_code_reachable_from_page_custom_layout_fun(call_graph, page_module)
    |> include_code_reachable_from_page_info_fun(call_graph, page_module)
    |> include_code_reachable_from_layout_init_fun(call_graph, layout_module)
    |> include_code_reachable_from_layout_actions(call_graph, layout_module)
    |> include_code_reachable_from_layout_template(call_graph, layout_module)
    |> include_code_reachable_from_component_init_fun(call_graph, component_modules)
    |> include_code_reachable_from_component_actions(call_graph, component_modules)
    |> include_code_reachable_from_component_templates(call_graph, component_modules)
    |> include_page_routes(page_modules)
    |> Enum.uniq()
  end

  defp find_component_modules(module_defs) do
    Enum.filter(module_defs, fn {_, module_def} -> module_def.component? end)
    |> Enum.map(fn {module, _} -> module end)
  end

  defp find_page_modules(module_defs) do
    Enum.filter(module_defs, fn {_, module_def} -> module_def.page? end)
    |> Enum.map(fn {module, _} -> module end)
  end

  defp include_code_reachable_from_component_actions(acc, call_graph, component_modules) do
    Enum.reduce(component_modules, acc, fn module, acc ->
      Graph.reachable(call_graph, [{module, :action}])
      |> maybe_include_reachable_code(acc)
    end)
  end

  defp include_code_reachable_from_component_init_fun(acc, call_graph, component_modules) do
    Enum.reduce(component_modules, acc, fn module, acc ->
      Graph.reachable(call_graph, [{module, :init}])
      |> maybe_include_reachable_code(acc)
    end)
  end

  defp include_code_reachable_from_component_templates(acc, call_graph, component_modules) do
    Enum.reduce(component_modules, acc, fn module, acc ->
      Graph.reachable(call_graph, [{module, :template}])
      |> maybe_include_reachable_code(acc)
    end)
  end

  defp include_code_reachable_from_layout_actions(acc, call_graph, layout_module) do
    Graph.reachable(call_graph, [{layout_module, :action}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_code_reachable_from_layout_init_fun(acc, call_graph, layout_module) do
    Graph.reachable(call_graph, [{layout_module, :init}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_code_reachable_from_layout_template(acc, call_graph, layout_module) do
    Graph.reachable(call_graph, [{layout_module, :template}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_code_reachable_from_page_actions(acc, call_graph, page_module) do
    Graph.reachable(call_graph, [{page_module, :action}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_code_reachable_from_page_custom_layout_fun(acc, call_graph, page_module) do
    Graph.reachable(call_graph, [{page_module, :custom_layout}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_code_reachable_from_page_info_fun(acc, call_graph, page_module) do
    Graph.reachable(call_graph, [{page_module, :__info__}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_code_reachable_from_page_layout_fun(acc, call_graph, page_module) do
    Graph.reachable(call_graph, [{page_module, :layout}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_code_reachable_from_page_template(acc, call_graph, page_module) do
    Graph.reachable(call_graph, [{page_module, :template}])
    |> maybe_include_reachable_code(acc)
  end

  defp include_page_routes(acc, page_modules) do
    Enum.reduce(page_modules, acc, fn module, acc ->
      acc ++ [{module, :route}]
    end)
  end

  defp maybe_include_reachable_code(reachable_code, acc) do
    case reachable_code do
      [nil] ->
        acc

      reachable_code ->
        acc ++ reachable_code
    end
  end

  defp remove_module_redundant_funs(
         %{functions: funs, module: module} = module_def,
         reachable_code_hash_table
       ) do
    funs = Enum.filter(funs, &reachable_code_hash_table[{module, &1.name}])
    %{module_def | functions: funs}
  end

  defp remove_redundant_funs(reachable_code, module_defs) do
    reachable_code_hash_table =
      reachable_code
      |> Enum.map(&{&1, true})
      |> Enum.into(%{})

    Enum.map(module_defs, fn {module, module_def} ->
      {module, remove_module_redundant_funs(module_def, reachable_code_hash_table)}
    end)
    |> Enum.into(%{})
  end

  defp remove_redundant_modules(module_defs) do
    Enum.filter(module_defs, fn {_, module_def} ->
      Enum.any?(module_def.functions)
    end)
    |> Enum.into(%{})
  end
end