Current section
Files
Jump to
Current section
Files
lib/why_recompile.ex
defmodule WhyRecompile do
@moduledoc """
API to interact with the dependency graph
"""
@type manifest_path :: binary()
@type file_path :: binary()
@spec parse_manifest() :: WhyRecompile.ManifestParseResult.t()
def parse_manifest() do
# Add the app code path to the code path so modules can be loaded
app_code_path = Mix.Project.app_path() |> Path.join("ebin")
:code.add_path(String.to_charlist(app_code_path))
%{graph: graph, modules_map: modules_map, source_files_map: source_files_map} =
__MODULE__.Graph.build(manifest_path())
graph_with_recompile_dependencies =
for vertex <- __MODULE__.Graph.summarize(graph) do
dependencies =
__MODULE__.Dependency.recompile_dependencies(graph, vertex.id)
|> Enum.flat_map(fn {reason, dependents} ->
for {file, chain} <- dependents do
%{
path: file,
reason: reason,
dependency_chain: format_dependency_chain(chain, vertex.id)
}
end
end)
|> Enum.sort_by(& &1.path)
Map.put(vertex, :recompile_dependencies, dependencies)
end
%__MODULE__.ManifestParseResult{
graph: graph_with_recompile_dependencies,
modules_map: modules_map,
source_files_map: source_files_map
}
end
# From this:
# chain = [
# [:compile, "lib/fixtures/D1.ex"],
# [:compile, "lib/fixtures/D2.ex"]
# ]
# recompile_source = "lib/fixtures/D3.ex"
#
# To this:
# chain = [
# [:compile, "lib/fixtures/D1.ex", "lib/fixtures/D2.ex"],
# [:compile, "lib/fixtures/D2.ex", "lib/fixtures/D3.ex"]
# ]
defp format_dependency_chain([], _), do: []
defp format_dependency_chain(chain, recompile_source) do
Enum.zip(chain, tl(chain) ++ [{nil, recompile_source}])
|> Enum.map(fn {item, {_, next_file}} ->
Tuple.append(item, next_file)
end)
end
# Expand the dependency path to a detailed explanation
def get_detailed_explanation(
%WhyRecompile.ManifestParseResult{} = parsed_manifest,
dependency_chain
),
do: get_detailed_explanation(parsed_manifest, dependency_chain, [])
defp get_detailed_explanation(_parsed_manifest, [], result), do: result
defp get_detailed_explanation(parsed_manifest, [{:runtime, source, sink} | tail], result) do
new_entry = %{
type: :runtime,
source: source,
sink: sink,
snippets: []
}
get_detailed_explanation(parsed_manifest, tail, result ++ [new_entry])
end
defp get_detailed_explanation(parsed_manifest, [{type, source, sink} | tail], result)
when type in [:exports, :compile] do
snippets =
WhyRecompile.Dependency.dependency_causes(%{
modules_map: parsed_manifest.modules_map,
source_files_map: parsed_manifest.source_files_map,
source_file: source,
sink_file: sink,
dependency_type: type
})
|> Enum.map(&extract_snippet/1)
new_entry = %{
type: type,
source: source,
sink: sink,
snippets: snippets
}
result = result ++ [new_entry]
get_detailed_explanation(parsed_manifest, tail, result)
end
@lines_span_padding 5
defp extract_snippet(%WhyRecompile.Dependency.Cause{origin_file: file, lines_span: lines_span}) do
# We want to add some paddings to the lines span
{from, to} = lines_span
from = max(from - @lines_span_padding, 1)
to = to + @lines_span_padding
lines =
File.stream!(file, [], :line)
|> Stream.drop(from - 1)
|> Stream.take(to - from + 1)
|> Enum.to_list()
# to may exceeds the file lines count
to = min(from + length(lines) - 1, to)
%{
file: file,
content: Enum.join(lines),
lines_span: {from, to},
highlight: lines_span
}
end
defp manifest_path() do
manifest_path = Mix.Project.manifest_path()
Path.join(manifest_path, "compile.elixir")
end
end