Current section

Files

Jump to
reach lib reach plugin inference.ex
Raw

lib/reach/plugin/inference.ex

defmodule Reach.Plugin.Inference do
@moduledoc false
def infer(paths) do
paths = paths |> List.wrap() |> Enum.reject(&is_nil/1)
files = source_files(paths)
(infer_from_mix_files(paths) ++ infer_from_sources(files))
|> Enum.uniq()
end
def infer_from_mix_file(path) do
with {:ok, source} <- File.read(path),
{:ok, ast} <- Code.string_to_quoted(source, emit_warnings: false) do
deps = dependency_names(ast)
Reach.Plugin.built_in_plugins()
|> Enum.filter(&dependency_match?(&1, deps))
|> Enum.uniq()
else
_error -> []
end
end
def infer_from_sources(files) do
files
|> Enum.flat_map(&source_plugins/1)
|> Enum.uniq()
end
defp infer_from_mix_files(paths) do
paths
|> Enum.flat_map(&nearest_mix_files/1)
|> Enum.uniq()
|> Enum.flat_map(&infer_from_mix_file/1)
end
defp nearest_mix_files(path) do
path
|> root_candidate()
|> ancestor_dirs()
|> Enum.find_value([], fn dir ->
mix = Path.join(dir, "mix.exs")
if File.regular?(mix), do: [mix]
end)
end
defp root_candidate(path) do
expanded = Path.expand(path)
if File.dir?(expanded), do: expanded, else: Path.dirname(expanded)
end
defp ancestor_dirs(dir) do
dir
|> Path.expand()
|> Stream.iterate(&Path.dirname/1)
|> Enum.reduce_while([], fn current, acc ->
next = Path.dirname(current)
acc = [current | acc]
if next == current, do: {:halt, Enum.reverse(acc)}, else: {:cont, acc}
end)
end
defp source_files(paths) do
paths
|> Enum.flat_map(&source_files_for_path/1)
|> Enum.uniq()
end
defp source_files_for_path(path) do
cond do
File.regular?(path) ->
[path]
File.dir?(path) ->
Path.wildcard(Path.join(path, "**/*.{ex,exs,erl,hrl,gleam,heex,js,jsx,ts,tsx}"))
true ->
Path.wildcard(path)
end
end
defp dependency_names(ast) do
{_ast, names} =
Macro.prewalk(ast, [], fn
{:{}, _meta, [dep | _rest]} = node, names when is_atom(dep) ->
{node, [dep | names]}
{dep, _meta, _args} = node, names when is_atom(dep) ->
{node, [dep | names]}
tuple, names when is_tuple(tuple) ->
{tuple, dependency_names_from_tuple(tuple) ++ names}
node, names ->
{node, names}
end)
Enum.uniq(names)
end
defp dependency_names_from_tuple(tuple) do
tuple
|> Tuple.to_list()
|> Enum.chunk_every(2)
|> Enum.flat_map(fn
[dep, _version] when is_atom(dep) -> [dep]
_entry -> []
end)
end
defp dependency_match?(plugin, deps) do
hints(plugin)
|> Map.get(:deps, [])
|> Enum.any?(&(&1 in deps))
end
defp source_match?(plugin, source) do
hints(plugin)
|> Map.get(:source, [])
|> Enum.any?(&String.contains?(source, &1))
end
defp hints(plugin) do
if Code.ensure_loaded?(plugin) and function_exported?(plugin, :inference_hints, 0) do
plugin.inference_hints()
else
%{}
end
end
defp source_plugins(file) do
case File.read(file) do
{:ok, source} ->
Reach.Plugin.built_in_plugins()
|> Enum.filter(&source_match?(&1, source))
{:error, _reason} ->
[]
end
end
end